diff --git a/README.md b/README.md index 70df5cc5..4c01ce12 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ If the proposal is _pending_ and does not yet have a number, use a letter. The p - `package.json` specifies what kind of proposal it is in a `agoricProposal` field. If it's a "Software Upgrade Proposal" it also includes additional parameters. - `use.sh` is the script that will be run in the USE stage of the build - `test.sh` is the script that will be _included_ in the TEST stage of the build, and run in CI +- `prepare-test.sh` is an optional script which can be used to run setup steps _inside_ the container ## Development diff --git a/packages/synthetic-chain/src/cli/cli.ts b/packages/synthetic-chain/src/cli/cli.ts index f8cf983c..67b6e32f 100755 --- a/packages/synthetic-chain/src/cli/cli.ts +++ b/packages/synthetic-chain/src/cli/cli.ts @@ -5,6 +5,7 @@ import chalk from 'chalk'; import assert from 'node:assert'; import { execSync } from 'node:child_process'; +import { statSync } from 'node:fs'; import path from 'node:path'; import { parseArgs } from 'node:util'; import { @@ -83,6 +84,53 @@ const prepareDockerBuild = () => { ); }; +const testProposals = () => { + const fileExists = (name: string) => + !!statSync(name, { throwIfNoEntry: false }); + + // Always rebuild all test images to keep it simple. With the "use" stages + // cached, these are pretty fast building doesn't run agd. + prepareDockerBuild(); + + if (values.debug) { + assert(match, '--debug requires -m'); + assert(proposals.length > 0, 'no proposals match'); + assert(proposals.length === 1, 'too many proposals match'); + const proposal = proposals[0]; + console.log(chalk.yellow.bold(`Debugging ${proposal.proposalName}`)); + bakeTarget(imageNameForProposal(proposal, 'test').target, values.dry); + debugTestImage(proposal); + // don't bother to delete the test image because there's just one + // and the user probably wants to run it again. + } else { + for (const proposal of proposals) { + console.log(chalk.cyan.bold(`Testing ${proposal.proposalName}`)); + const image = imageNameForProposal(proposal, 'test'); + bakeTarget(image.target, values.dry); + + const proposalPath = `${root}/proposals/${proposal.path}`; + + if (fileExists(`${proposalPath}/pre_test.sh`)) + execSync(`/bin/bash ${proposalPath}/pre_test.sh`, { + stdio: 'inherit', + }); + + runTestImage(proposal); + + if (fileExists(`${proposalPath}/post_test.sh`)) + execSync(`/bin/bash ${proposalPath}/post_test.sh`, { + stdio: 'inherit', + }); + + // delete the image to reclaim disk space. The next build + // will use the build cache. + execSync('docker system df', { stdio: 'inherit' }); + execSync(`docker rmi ${image.name}`, { stdio: 'inherit' }); + execSync('docker system df', { stdio: 'inherit' }); + } + } +}; + switch (cmd) { case 'prepare-build': prepareDockerBuild(); @@ -98,33 +146,7 @@ switch (cmd) { break; } case 'test': - // Always rebuild all test images to keep it simple. With the "use" stages - // cached, these are pretty fast building doesn't run agd. - prepareDockerBuild(); - - if (values.debug) { - assert(match, '--debug requires -m'); - assert(proposals.length > 0, 'no proposals match'); - assert(proposals.length === 1, 'too many proposals match'); - const proposal = proposals[0]; - console.log(chalk.yellow.bold(`Debugging ${proposal.proposalName}`)); - bakeTarget(imageNameForProposal(proposal, 'test').target, values.dry); - debugTestImage(proposal); - // don't bother to delete the test image because there's just one - // and the user probably wants to run it again. - } else { - for (const proposal of proposals) { - console.log(chalk.cyan.bold(`Testing ${proposal.proposalName}`)); - const image = imageNameForProposal(proposal, 'test'); - bakeTarget(image.target, values.dry); - runTestImage(proposal); - // delete the image to reclaim disk space. The next build - // will use the build cache. - execSync('docker system df', { stdio: 'inherit' }); - execSync(`docker rmi ${image.name}`, { stdio: 'inherit' }); - execSync('docker system df', { stdio: 'inherit' }); - } - } + testProposals(); break; case 'doctor': runDoctor(allProposals); diff --git a/packages/synthetic-chain/src/cli/run.ts b/packages/synthetic-chain/src/cli/run.ts index 6cd4fc78..5845e3ad 100755 --- a/packages/synthetic-chain/src/cli/run.ts +++ b/packages/synthetic-chain/src/cli/run.ts @@ -2,28 +2,53 @@ import { execSync } from 'node:child_process'; import { realpathSync } from 'node:fs'; import { ProposalInfo, imageNameForProposal } from './proposals.js'; +const propagateMessageFilePath = () => { + const fileName = 'message-file-path.tmp'; + + const containerFilePath = `/root/${fileName}`; + const filePath = `$HOME/${fileName}`; + + execSync(`touch ${filePath}`); + + return [ + '--env', + `MESSAGE_FILE_PATH=${containerFilePath}`, + '--mount', + `"source=${filePath},target=${containerFilePath},type=bind"`, + ]; +}; + /** * Used to propagate a SLOGFILE environment variable into Docker containers. * Any file identified by such a variable will be created if it does not already * exist. - * - * @param {typeof process.env} env environment variables - * @returns {string[]} docker run options */ -const propagateSlogfile = env => { +const propagateSlogfile = (env: typeof process.env): string[] => { const { SLOGFILE } = env; if (!SLOGFILE) return []; execSync('touch "$SLOGFILE"'); - return ['-e', 'SLOGFILE', '-v', `"$SLOGFILE:${realpathSync(SLOGFILE)}"`]; + return [ + '--env', + 'SLOGFILE', + '--volume', + `"$SLOGFILE:${realpathSync(SLOGFILE)}"`, + ]; }; export const runTestImage = (proposal: ProposalInfo) => { console.log(`Running test image for proposal ${proposal.proposalName}`); const { name } = imageNameForProposal(proposal, 'test'); - const slogOpts = propagateSlogfile(process.env); - // 'rm' to remove the container when it exits - const cmd = `docker run ${slogOpts.join(' ')} --rm ${name}`; + const cmd = [ + 'docker', + 'run', + `--network "host"`, + '--rm', + `--user "root"`, + ...propagateSlogfile(process.env), + ...propagateMessageFilePath(), + name, + ].join(' '); execSync(cmd, { stdio: 'inherit' }); };