fix: select rollback artifact pairs atomically
This commit is contained in:
@@ -292,13 +292,12 @@ export async function verifyRollbackReleaseCoherence(
|
|||||||
) {
|
) {
|
||||||
const readArtifact = options.readArtifact ?? readJsonArtifact;
|
const readArtifact = options.readArtifact ?? readJsonArtifact;
|
||||||
const paths = options.paths ?? DEFAULT_ROLLBACK_PATHS;
|
const paths = options.paths ?? DEFAULT_ROLLBACK_PATHS;
|
||||||
const release = await releaseManifest(readArtifact, paths);
|
const artifactPair = await selectRollbackArtifactPair(
|
||||||
const runtimeValue = await readPrimaryOrFallback(
|
|
||||||
readArtifact,
|
readArtifact,
|
||||||
paths.primaryRuntime,
|
paths,
|
||||||
paths.fallbackRuntime,
|
|
||||||
);
|
);
|
||||||
const runtimeArtifact = parseRuntimeConfigArtifact(runtimeValue);
|
const release = parseReleaseArtifact(artifactPair.release);
|
||||||
|
const runtimeArtifact = parseRuntimeConfigArtifact(artifactPair.runtime);
|
||||||
const runtime = {
|
const runtime = {
|
||||||
...runtimeArtifact,
|
...runtimeArtifact,
|
||||||
BUILD_ID: requireIdentity(runtimeArtifact.BUILD_ID, "runtime BUILD_ID"),
|
BUILD_ID: requireIdentity(runtimeArtifact.BUILD_ID, "runtime BUILD_ID"),
|
||||||
@@ -377,6 +376,75 @@ function requireIdentity(value: string | undefined, label: string): string {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectRollbackArtifactPair(
|
||||||
|
readArtifact: JsonArtifactReader,
|
||||||
|
paths: RollbackArtifactPaths,
|
||||||
|
): Promise<Readonly<{ release: unknown; runtime: unknown }>> {
|
||||||
|
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<unknown>;
|
||||||
|
runtime: PromiseSettledResult<unknown>;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const [release, runtime] = await Promise.allSettled([
|
||||||
|
Promise.resolve().then(() => readArtifact(releasePath)),
|
||||||
|
Promise.resolve().then(() => readArtifact(runtimePath)),
|
||||||
|
]);
|
||||||
|
return Object.freeze({ release, runtime });
|
||||||
|
}
|
||||||
|
|
||||||
async function readPrimaryOrFallback(
|
async function readPrimaryOrFallback(
|
||||||
readArtifact: JsonArtifactReader,
|
readArtifact: JsonArtifactReader,
|
||||||
primary: string,
|
primary: string,
|
||||||
|
|||||||
@@ -100,8 +100,10 @@ async function releaseV2With(
|
|||||||
|
|
||||||
function artifactReader(
|
function artifactReader(
|
||||||
entries: Readonly<Record<string, unknown | Error>>,
|
entries: Readonly<Record<string, unknown | Error>>,
|
||||||
|
reads: string[] = [],
|
||||||
): (path: string) => Promise<unknown> {
|
): (path: string) => Promise<unknown> {
|
||||||
return async (path) => {
|
return async (path) => {
|
||||||
|
reads.push(path);
|
||||||
const value = entries[path];
|
const value = entries[path];
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
throw Object.assign(new Error(`missing ${path}`), { code: "ENOENT" });
|
throw Object.assign(new Error(`missing ${path}`), { code: "ENOENT" });
|
||||||
@@ -315,11 +317,54 @@ describe("release coherence", () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({ coherence: { compatible: false } });
|
).resolves.toMatchObject({ coherence: { compatible: false } });
|
||||||
|
|
||||||
|
const releaseOnlyMissingReads: string[] = [];
|
||||||
await expect(
|
await expect(
|
||||||
verifyRollbackReleaseCoherence({
|
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,
|
contractPackages,
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({ coherence: { compatible: true } });
|
).resolves.toMatchObject({ coherence: { compatible: true } });
|
||||||
|
expect(missingPairReads).toEqual([
|
||||||
|
"dist/release-manifest.json",
|
||||||
|
"dist/config.json",
|
||||||
|
"public/release-manifest.json",
|
||||||
|
"public/config.json",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user