diff --git a/scripts/drill-runbook.ts b/scripts/drill-runbook.ts index 2b0665b..f37fee1 100644 --- a/scripts/drill-runbook.ts +++ b/scripts/drill-runbook.ts @@ -292,13 +292,12 @@ export async function verifyRollbackReleaseCoherence( ) { const readArtifact = options.readArtifact ?? readJsonArtifact; const paths = options.paths ?? DEFAULT_ROLLBACK_PATHS; - const release = await releaseManifest(readArtifact, paths); - const runtimeValue = await readPrimaryOrFallback( + const artifactPair = await selectRollbackArtifactPair( readArtifact, - paths.primaryRuntime, - paths.fallbackRuntime, + paths, ); - const runtimeArtifact = parseRuntimeConfigArtifact(runtimeValue); + const release = parseReleaseArtifact(artifactPair.release); + const runtimeArtifact = parseRuntimeConfigArtifact(artifactPair.runtime); const runtime = { ...runtimeArtifact, BUILD_ID: requireIdentity(runtimeArtifact.BUILD_ID, "runtime BUILD_ID"), @@ -377,6 +376,75 @@ function requireIdentity(value: string | undefined, label: string): string { return value; } +async function selectRollbackArtifactPair( + readArtifact: JsonArtifactReader, + paths: RollbackArtifactPaths, +): Promise> { + const primary = await readArtifactPair( + readArtifact, + paths.primaryRelease, + paths.primaryRuntime, + ); + if ( + primary.release.status === "fulfilled" && + primary.runtime.status === "fulfilled" + ) { + return Object.freeze({ + release: primary.release.value, + runtime: primary.runtime.value, + }); + } + + const releaseMissing = + primary.release.status === "rejected" && + hasErrorCode(primary.release.reason, "ENOENT"); + const runtimeMissing = + primary.runtime.status === "rejected" && + hasErrorCode(primary.runtime.reason, "ENOENT"); + if (releaseMissing && runtimeMissing) { + const fallback = await readArtifactPair( + readArtifact, + paths.fallbackRelease, + paths.fallbackRuntime, + ); + if (fallback.release.status === "rejected") { + throw fallback.release.reason; + } + if (fallback.runtime.status === "rejected") { + throw fallback.runtime.reason; + } + return Object.freeze({ + release: fallback.release.value, + runtime: fallback.runtime.value, + }); + } + + if (primary.release.status === "rejected" && !releaseMissing) { + throw primary.release.reason; + } + if (primary.runtime.status === "rejected" && !runtimeMissing) { + throw primary.runtime.reason; + } + throw new Error("primary rollback artifact pair is incomplete"); +} + +async function readArtifactPair( + readArtifact: JsonArtifactReader, + releasePath: string, + runtimePath: string, +): Promise< + Readonly<{ + release: PromiseSettledResult; + runtime: PromiseSettledResult; + }> +> { + const [release, runtime] = await Promise.allSettled([ + Promise.resolve().then(() => readArtifact(releasePath)), + Promise.resolve().then(() => readArtifact(runtimePath)), + ]); + return Object.freeze({ release, runtime }); +} + async function readPrimaryOrFallback( readArtifact: JsonArtifactReader, primary: string, diff --git a/tests/unit/release-coherence.test.ts b/tests/unit/release-coherence.test.ts index e349473..b8745ef 100644 --- a/tests/unit/release-coherence.test.ts +++ b/tests/unit/release-coherence.test.ts @@ -100,8 +100,10 @@ async function releaseV2With( function artifactReader( entries: Readonly>, + reads: string[] = [], ): (path: string) => Promise { return async (path) => { + reads.push(path); const value = entries[path]; if (value === undefined) { throw Object.assign(new Error(`missing ${path}`), { code: "ENOENT" }); @@ -315,11 +317,54 @@ describe("release coherence", () => { }), ).resolves.toMatchObject({ coherence: { compatible: false } }); + const releaseOnlyMissingReads: string[] = []; await expect( verifyRollbackReleaseCoherence({ - readArtifact: artifactReader(validPublic), + readArtifact: artifactReader( + { + "dist/config.json": runtimeV2, + ...validPublic, + }, + releaseOnlyMissingReads, + ), + contractPackages, + }), + ).rejects.toThrow("primary rollback artifact pair"); + expect(releaseOnlyMissingReads).toEqual([ + "dist/release-manifest.json", + "dist/config.json", + ]); + + const runtimeOnlyMissingReads: string[] = []; + await expect( + verifyRollbackReleaseCoherence({ + readArtifact: artifactReader( + { + "dist/release-manifest.json": exactV2, + ...validPublic, + }, + runtimeOnlyMissingReads, + ), + contractPackages, + }), + ).rejects.toThrow("primary rollback artifact pair"); + expect(runtimeOnlyMissingReads).toEqual([ + "dist/release-manifest.json", + "dist/config.json", + ]); + + const missingPairReads: string[] = []; + await expect( + verifyRollbackReleaseCoherence({ + readArtifact: artifactReader(validPublic, missingPairReads), contractPackages, }), ).resolves.toMatchObject({ coherence: { compatible: true } }); + expect(missingPairReads).toEqual([ + "dist/release-manifest.json", + "dist/config.json", + "public/release-manifest.json", + "public/config.json", + ]); }); });