Ensure the sandbox exits cleanly if publishing

This commit is contained in:
Tom Coleman 2022-08-12 13:35:30 +10:00
parent a05bccefbf
commit ed2c21ec65
5 changed files with 83 additions and 17 deletions

View File

@ -147,6 +147,7 @@
"lint-staged": "^10.5.4",
"lodash": "^4.17.21",
"mocha-list-tests": "^1.0.5",
"node-abort-controller": "^3.0.1",
"node-cleanup": "^2.1.2",
"node-fetch": "^2.6.1",
"node-gyp": "^8.4.0",

View File

@ -10,6 +10,7 @@ import {
existsSync,
} from 'fs-extra';
import prompts from 'prompts';
import { AbortController } from 'node-abort-controller';
import { createOptions, getOptionsOrPrompt, OptionValues } from './utils/options';
import { executeCLIStep } from './utils/cli-step';
@ -246,6 +247,7 @@ export async function sandbox(optionValues: OptionValues<typeof options>) {
const { template, forceDelete, forceReuse, dryRun, debug } = optionValues;
await ensureDir(sandboxDir);
let publishController: AbortController;
const cwd = path.join(sandboxDir, template.replace('/', '-'));
@ -331,8 +333,15 @@ export async function sandbox(optionValues: OptionValues<typeof options>) {
}
if (publish || startVerdaccio) {
// NOTE: this is a background task and will run forever (TODO: sort out logging/exiting)
exec('CI=true yarn local-registry --open', { cwd: codeDir }, { dryRun, debug });
publishController = new AbortController();
exec(
'CI=true yarn local-registry --open',
{ cwd: codeDir },
{ dryRun, debug, signal: publishController.signal as AbortSignal }
).catch((err) => {
// If aborted, we want to make sure the rejection is handled.
if (!err.killed) throw err;
});
await exec('yarn wait-on http://localhost:6000', { cwd: codeDir }, { dryRun, debug });
}
@ -382,6 +391,9 @@ export async function sandbox(optionValues: OptionValues<typeof options>) {
}
// TODO start dev
// Cleanup
publishController.abort();
}
async function main() {

View File

@ -40,34 +40,61 @@ type TaskKey = keyof typeof tasks;
export const options = createOptions({
task: {
type: 'string',
values: Object.keys(tasks) as TaskKey[],
description: 'What task are you performing (corresponds to CI job)?',
values: Object.keys(tasks) as TaskKey[],
required: true,
},
template: {
type: 'string',
values: Object.keys(TEMPLATES) as TemplateKey[],
description: 'What template are you running against?',
values: Object.keys(TEMPLATES) as TemplateKey[],
required: true,
},
force: {
type: 'boolean',
description: 'The task must run, it is an error if it is already ready?',
},
before: {
type: 'boolean',
description: 'Run any required dependencies of the task?',
inverse: true,
},
});
const logger = console;
async function runTask(taskKey: TaskKey, templateKey: TemplateKey) {
async function runTask(
taskKey: TaskKey,
templateKey: TemplateKey,
{
mustNotBeReady,
mustBeReady,
before,
}: { mustNotBeReady: boolean; mustBeReady: boolean; before: boolean }
) {
const task = tasks[taskKey];
const template = TEMPLATES[templateKey];
const templateSandboxDir = join(sandboxDir, templateKey.replace('/', '-'));
const details = { template, sandboxDir: templateSandboxDir };
if (await task.ready(templateKey, details)) {
if (mustNotBeReady) throw new Error(`${taskKey} task has already run, this is unexpected!`);
logger.debug(`${taskKey} task not required!`);
return;
}
if (mustBeReady) {
throw new Error(`${taskKey} task has not already run, this is unexpected!`);
}
if (task.before?.length > 0) {
for (const beforeKey of task.before) {
await runTask(beforeKey, templateKey);
await runTask(beforeKey, templateKey, {
mustNotBeReady: false,
mustBeReady: !before,
before,
});
}
}
@ -75,15 +102,20 @@ async function runTask(taskKey: TaskKey, templateKey: TemplateKey) {
}
async function run() {
const { task: taskKey, template: templateKey } = await getOptionsOrPrompt('yarn task', options);
const {
task: taskKey,
template: templateKey,
force,
before,
} = await getOptionsOrPrompt('yarn task', options);
return runTask(taskKey, templateKey);
return runTask(taskKey, templateKey, { mustBeReady: force, mustNotBeReady: false, before });
}
if (require.main === module) {
run().catch((err) => {
logger.error(`🚨 An error occurred when executing task:`);
logger.error(err);
logger.error();
logger.error(err.message);
process.exit(1);
});
}

View File

@ -1,5 +1,5 @@
/* eslint-disable no-await-in-loop, no-restricted-syntax */
import execa, { Options } from 'execa';
import execa, { ExecaChildProcess, Options } from 'execa';
import chalk from 'chalk';
const logger = console;
@ -9,12 +9,13 @@ type StepOptions = {
errorMessage?: string;
dryRun?: boolean;
debug?: boolean;
signal?: AbortSignal;
};
export const exec = async (
command: string | string[],
options: Options = {},
{ startMessage, errorMessage, dryRun, debug }: StepOptions = {}
{ startMessage, errorMessage, dryRun, debug, signal }: StepOptions = {}
): Promise<void> => {
logger.info();
if (startMessage) logger.info(startMessage);
@ -28,20 +29,32 @@ export const exec = async (
shell: true,
stdout: debug ? 'inherit' : 'ignore',
};
let currentChild: ExecaChildProcess;
// Newer versions of execa have explicit support for abort signals, but this works
if (signal) {
signal.addEventListener('abort', () => currentChild.kill());
}
try {
if (typeof command === 'string') {
logger.debug(`> ${command}`);
await execa.command(command, { ...defaultOptions, ...options });
currentChild = execa.command(command, { ...defaultOptions, ...options });
await currentChild;
} else {
for (const subcommand of command) {
logger.debug(`> ${subcommand}`);
await execa.command(subcommand, { ...defaultOptions, ...options });
currentChild = execa.command(subcommand, { ...defaultOptions, ...options });
await currentChild;
}
}
} catch (err) {
logger.error(chalk.red(`An error occurred while executing: \`${command}\``));
logger.error(err);
logger.log(errorMessage);
if (!err.killed) {
logger.error(chalk.red(`An error occurred while executing: \`${command}\``));
logger.error(err);
logger.log(errorMessage);
}
throw err;
}

View File

@ -3334,6 +3334,7 @@ __metadata:
lint-staged: ^10.5.4
lodash: ^4.17.21
mocha-list-tests: ^1.0.5
node-abort-controller: ^3.0.1
node-cleanup: ^2.1.2
node-fetch: ^2.6.1
node-gyp: ^8.4.0
@ -14476,6 +14477,13 @@ __metadata:
languageName: node
linkType: hard
"node-abort-controller@npm:^3.0.1":
version: 3.0.1
resolution: "node-abort-controller@npm:3.0.1"
checksum: 37f895533f7a18a2d83fa4853da1cc00fcae1e0a71553f9ffc94d3153f5fc886d6d4ef3a33bf60c38be161fab78c5b2275cbbf2359351fb12f5edad68d88d8ca
languageName: node
linkType: hard
"node-cleanup@npm:^2.1.2":
version: 2.1.2
resolution: "node-cleanup@npm:2.1.2"