diff --git a/scripts/prepare-extension-package-boundary-artifacts.mjs b/scripts/prepare-extension-package-boundary-artifacts.mjs index 0399dc5fbe7..8a0761ea115 100644 --- a/scripts/prepare-extension-package-boundary-artifacts.mjs +++ b/scripts/prepare-extension-package-boundary-artifacts.mjs @@ -33,11 +33,19 @@ export function createPrefixedOutputWriter(label, target) { }; } -function runNodeStep(label, args, timeoutMs) { +function abortSiblingSteps(abortController) { + if (abortController && !abortController.signal.aborted) { + abortController.abort(); + } +} + +export function runNodeStep(label, args, timeoutMs, params = {}) { + const abortController = params.abortController; return new Promise((resolvePromise, rejectPromise) => { const child = spawn(process.execPath, args, { cwd: repoRoot, env: process.env, + signal: abortController?.signal, stdio: ["ignore", "pipe", "pipe"], }); @@ -52,6 +60,7 @@ function runNodeStep(label, args, timeoutMs) { settled = true; stdoutWriter.flush(); stderrWriter.flush(); + abortSiblingSteps(abortController); rejectPromise(new Error(`${label} timed out after ${timeoutMs}ms`)); }, timeoutMs); @@ -71,6 +80,11 @@ function runNodeStep(label, args, timeoutMs) { settled = true; stdoutWriter.flush(); stderrWriter.flush(); + if (error.name === "AbortError" && abortController?.signal.aborted) { + rejectPromise(new Error(`${label} canceled after sibling failure`)); + return; + } + abortSiblingSteps(abortController); rejectPromise(new Error(`${label} failed to start: ${error.message}`)); }); child.on("close", (code) => { @@ -85,24 +99,36 @@ function runNodeStep(label, args, timeoutMs) { resolvePromise(); return; } + abortSiblingSteps(abortController); rejectPromise(new Error(`${label} failed with exit code ${code ?? 1}`)); }); }); } +export async function runNodeStepsInParallel(steps) { + const abortController = new AbortController(); + const results = await Promise.allSettled( + steps.map((step) => runNodeStep(step.label, step.args, step.timeoutMs, { abortController })), + ); + const firstFailure = results.find((result) => result.status === "rejected"); + if (firstFailure) { + throw firstFailure.reason; + } +} + export async function main() { try { - await Promise.all([ - runNodeStep( - "plugin-sdk boundary dts", - [tscBin, "-p", "tsconfig.plugin-sdk.dts.json"], - 300_000, - ), - runNodeStep( - "plugin-sdk package boundary dts", - [tscBin, "-p", "packages/plugin-sdk/tsconfig.json"], - 300_000, - ), + await runNodeStepsInParallel([ + { + label: "plugin-sdk boundary dts", + args: [tscBin, "-p", "tsconfig.plugin-sdk.dts.json"], + timeoutMs: 300_000, + }, + { + label: "plugin-sdk package boundary dts", + args: [tscBin, "-p", "packages/plugin-sdk/tsconfig.json"], + timeoutMs: 300_000, + }, ]); await runNodeStep( "plugin-sdk boundary root shims", diff --git a/test/scripts/prepare-extension-package-boundary-artifacts.test.ts b/test/scripts/prepare-extension-package-boundary-artifacts.test.ts index 4f4b8fdcbf7..09dbd6ab984 100644 --- a/test/scripts/prepare-extension-package-boundary-artifacts.test.ts +++ b/test/scripts/prepare-extension-package-boundary-artifacts.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { createPrefixedOutputWriter } from "../../scripts/prepare-extension-package-boundary-artifacts.mjs"; +import { + createPrefixedOutputWriter, + runNodeStepsInParallel, +} from "../../scripts/prepare-extension-package-boundary-artifacts.mjs"; describe("prepare-extension-package-boundary-artifacts", () => { it("prefixes each completed line and flushes the trailing partial line", () => { @@ -16,4 +19,25 @@ describe("prepare-extension-package-boundary-artifacts", () => { expect(output).toBe("[boundary] first line\n[boundary] second line\n[boundary] third"); }); + + it("aborts sibling steps after the first failure", async () => { + const startedAt = Date.now(); + + await expect( + runNodeStepsInParallel([ + { + label: "fail-fast", + args: ["--eval", "setTimeout(() => process.exit(2), 10)"], + timeoutMs: 5_000, + }, + { + label: "slow-step", + args: ["--eval", "setTimeout(() => {}, 10_000)"], + timeoutMs: 5_000, + }, + ]), + ).rejects.toThrow("fail-fast failed with exit code 2"); + + expect(Date.now() - startedAt).toBeLessThan(2_000); + }); });