fix: unify release runtime coherence verification

This commit is contained in:
DongHyeonka
2026-08-02 03:38:49 +09:00
parent 184bd98d92
commit 990603e24a
5 changed files with 406 additions and 98 deletions
+110 -70
View File
@@ -1,4 +1,5 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { shouldRetry } from "../src/adapters/http/retry-policy.ts";
import { createTelemetryAdapter } from "../src/adapters/telemetry/best-effort-telemetry.ts";
@@ -6,8 +7,17 @@ import { verifyCompatibilityTuple } from "../src/application/policies/compatibil
import type { StoragePort } from "../src/application/ports/storage-port.ts";
import { decideChunkRecovery } from "../src/application/use-cases/decide-chunk-recovery.ts";
import { validateRuntimeConfig } from "../src/bootstrap/runtime-config-schema.ts";
import { compareReleaseToRuntime } from "../src/contracts/release-tokens.ts";
import {
parseReleaseArtifact,
parseRuntimeConfigArtifact,
type ReleaseArtifact,
} from "../src/contracts/release-artifacts.ts";
import { projectTelemetryEvent } from "../src/contracts/telemetry.ts";
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../src/features/installed-contract-contributions.ts";
import {
verifyReleaseRuntimeCoherence,
type ReleaseRuntimeCoherenceInput,
} from "./lib/release-runtime-coherence.ts";
type RecoveryAssertion = Readonly<{
assertion: string;
@@ -38,35 +48,15 @@ type RunbookDocument = Readonly<{
runbooks: Record<string, RunbookSpecification>;
}>;
type ReleaseManifest = Record<string, unknown> & {
buildId: string;
configSchemaVersion: string;
apiContractVersion: string;
assetManifestHash: string;
releaseId: string;
};
const runbookId = process.argv
.slice(2)
.find((argument) => /^FE-RB-00[1-5]$/.test(argument));
const document = JSON.parse(
await readFile("config/runbooks/runbooks.json", "utf8"),
) as RunbookDocument;
const specification = runbookId ? document.runbooks[runbookId] : undefined;
if (!runbookId || !specification) {
process.stderr.write("Usage: drill:runbook -- FE-RB-001..FE-RB-005\n");
process.exit(2);
}
async function releaseManifest(): Promise<ReleaseManifest> {
async function releaseManifest(): Promise<ReleaseArtifact> {
for (const candidate of [
"dist/release-manifest.json",
"public/release-manifest.json",
]) {
try {
return JSON.parse(
await readFile(candidate, "utf8"),
) as ReleaseManifest;
return parseReleaseArtifact(
JSON.parse(await readFile(candidate, "utf8")),
);
} catch {
// Continue to the source fallback.
}
@@ -238,15 +228,29 @@ async function drillTelemetry(): Promise<DrillResult> {
async function drillRollback(): Promise<DrillResult> {
const release = await releaseManifest();
const runtime = JSON.parse(
await readFile(
(await access("dist/config.json").then(() => true).catch(() => false))
? "dist/config.json"
: "public/config.json",
"utf8",
const runtimeArtifact = parseRuntimeConfigArtifact(
JSON.parse(
await readFile(
(await access("dist/config.json").then(() => true).catch(() => false))
? "dist/config.json"
: "public/config.json",
"utf8",
),
),
);
const coherent = compareReleaseToRuntime(release, runtime);
const runtime = {
...runtimeArtifact,
BUILD_ID: requireIdentity(runtimeArtifact.BUILD_ID, "runtime BUILD_ID"),
RELEASE_ID: requireIdentity(
runtimeArtifact.RELEASE_ID,
"runtime RELEASE_ID",
),
};
const coherent = await verifyRollbackReleaseCoherence({
release,
runtime,
contractPackages: EXPECTED_CONTRACT_SET_PACKAGES,
});
const mixed = verifyCompatibilityTuple({
frontend: {
buildId: "build-a",
@@ -284,42 +288,78 @@ const drillById: Record<string, () => Promise<DrillResult>> = {
"FE-RB-004": drillTelemetry,
"FE-RB-005": drillRollback,
};
const drill = await drillById[runbookId]();
const escalationPathAsserted = specification.escalation.length >= 2;
const passed =
drill.triggerAsserted &&
drill.containmentAsserted &&
escalationPathAsserted &&
drill.recoveryAssertions.every((item) => item.passed) &&
drill.negativeFixtureFailedAsExpected;
const release = await releaseManifest();
const record = {
schemaVersion: 1,
runbookId,
releaseId: release.releaseId,
drillTimestamp: new Date().toISOString(),
triggerInjected: specification.triggerKinds[0],
triggerAsserted: drill.triggerAsserted,
containmentAsserted: drill.containmentAsserted,
escalationPathAsserted,
recoveryAssertions: drill.recoveryAssertions,
negativeFixtureFailedAsExpected: drill.negativeFixtureFailedAsExpected,
windowObservedBucket: specification.window,
providerVerificationRequired: drill.providerVerificationRequired,
passed,
};
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
await mkdir(artifactDirectory, { recursive: true });
await writeFile(
`${artifactDirectory}/record.json`,
`${JSON.stringify(record, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(`${runbookId} drill failed.\n`);
process.exit(1);
export async function verifyRollbackReleaseCoherence(
input: ReleaseRuntimeCoherenceInput,
) {
return verifyReleaseRuntimeCoherence(input);
}
async function main(): Promise<void> {
const runbookId = process.argv
.slice(2)
.find((argument) => /^FE-RB-00[1-5]$/.test(argument));
const document = JSON.parse(
await readFile("config/runbooks/runbooks.json", "utf8"),
) as RunbookDocument;
const specification = runbookId ? document.runbooks[runbookId] : undefined;
if (!runbookId || !specification) {
process.stderr.write("Usage: drill:runbook -- FE-RB-001..FE-RB-005\n");
process.exit(2);
}
const drill = await drillById[runbookId]();
const escalationPathAsserted = specification.escalation.length >= 2;
const passed =
drill.triggerAsserted &&
drill.containmentAsserted &&
escalationPathAsserted &&
drill.recoveryAssertions.every((item) => item.passed) &&
drill.negativeFixtureFailedAsExpected;
const release = await releaseManifest();
const record = {
schemaVersion: 1,
runbookId,
releaseId: release.releaseId,
drillTimestamp: new Date().toISOString(),
triggerInjected: specification.triggerKinds[0],
triggerAsserted: drill.triggerAsserted,
containmentAsserted: drill.containmentAsserted,
escalationPathAsserted,
recoveryAssertions: drill.recoveryAssertions,
negativeFixtureFailedAsExpected: drill.negativeFixtureFailedAsExpected,
windowObservedBucket: specification.window,
providerVerificationRequired: drill.providerVerificationRequired,
passed,
};
const artifactDirectory = `artifacts/runbooks/${runbookId}`;
await mkdir(artifactDirectory, { recursive: true });
await writeFile(
`${artifactDirectory}/record.json`,
`${JSON.stringify(record, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(`${runbookId} drill failed.\n`);
process.exit(1);
}
process.stdout.write(
`${runbookId} drill: PASS (${specification.gateId}; provider verification ${
drill.providerVerificationRequired ? "still required" : "not required"
})\n`,
);
}
function requireIdentity(value: string | undefined, label: string): string {
if (value === undefined || value.length === 0) {
throw new TypeError(`${label} must be a non-empty string`);
}
return value;
}
const invokedPath = process.argv[1];
if (
invokedPath !== undefined &&
import.meta.url === pathToFileURL(invokedPath).href
) {
await main();
}
process.stdout.write(
`${runbookId} drill: PASS (${specification.gateId}; provider verification ${
drill.providerVerificationRequired ? "still required" : "not required"
})\n`,
);