From 664400f4bd94e4cf879405f2a886090778241459 Mon Sep 17 00:00:00 2001 From: ranade-oss Date: Wed, 5 Aug 2026 07:40:06 +0900 Subject: [PATCH] Fix fresh legal-source release gate --- .../verify-and-deploy-public-beta.yml | 20 +++++- config/release-manifest.v1.json | 1 + reports/release-manifest-v1.json | 19 ++++-- scripts/check-final-completion.mjs | 22 +++++- scripts/observe-legal-sources.mjs | 38 ++++++++++- tests/baseline/ross-release-train.test.mjs | 21 ++++++ tests/final/final-completion.test.mjs | 67 ++++++++++++++++++- 7 files changed, 176 insertions(+), 12 deletions(-) diff --git a/.github/workflows/verify-and-deploy-public-beta.yml b/.github/workflows/verify-and-deploy-public-beta.yml index f169f18139..599c63e4f7 100644 --- a/.github/workflows/verify-and-deploy-public-beta.yml +++ b/.github/workflows/verify-and-deploy-public-beta.yml @@ -37,9 +37,27 @@ jobs: - name: Run complete engineering gate run: npm run check + - name: Observe live legal sources for production gate + if: inputs.promote_public + continue-on-error: true + run: | + mkdir -p artifacts + node scripts/observe-legal-sources.mjs \ + --output artifacts/release-train-legal-source-health.json + - name: Run production final-completion gate before public promotion if: inputs.promote_public - run: npm run final:check + run: npm run final:check -- \ + --source-report artifacts/release-train-legal-source-health.json + + - name: Upload production qualification legal-source report + if: always() && inputs.promote_public + uses: actions/upload-artifact@v7 + with: + name: ross-release-qualification-${{ github.run_id }}-${{ github.run_attempt }} + path: artifacts/release-train-legal-source-health.json + if-no-files-found: warn + retention-days: 90 - name: Build every Fly container path run: npm run preflight:fly diff --git a/config/release-manifest.v1.json b/config/release-manifest.v1.json index 6d2977b0bb..dee93cb2ae 100644 --- a/config/release-manifest.v1.json +++ b/config/release-manifest.v1.json @@ -117,6 +117,7 @@ "scripts/lib/professional-validation.mjs", "scripts/lib/release-identifier.mjs", "scripts/lib/final-completion.mjs", + "scripts/check-final-completion.mjs", "scripts/validate-release-id.mjs", "scripts/validate-staging-debug.mjs", "tests/baseline/ross-ci-toolchain.test.mjs", diff --git a/reports/release-manifest-v1.json b/reports/release-manifest-v1.json index 3133a32b2c..258947e959 100644 --- a/reports/release-manifest-v1.json +++ b/reports/release-manifest-v1.json @@ -3,7 +3,7 @@ "releaseId": "ross-public-beta-20260717-rc1", "generatedAt": "2026-07-26T10:39:57.000Z", "algorithm": "sha256", - "artifactCount": 127, + "artifactCount": 128, "artifacts": [ { "path": ".github/actionlint.yaml", @@ -37,8 +37,8 @@ }, { "path": ".github/workflows/verify-and-deploy-public-beta.yml", - "sha256": "f0480f0fcf997b1b5e1408c12d7704032a7222aef61d1011599d0ab72a7ecb6c", - "sizeBytes": 10071 + "sha256": "40030bf5f1c37cc6e72b10ceb71514595a217bcc8e20d42c0f2956ebaff14c7e", + "sizeBytes": 10830 }, { "path": ".github/workflows/verify-ontario-sources.yml", @@ -522,8 +522,8 @@ }, { "path": "scripts/observe-legal-sources.mjs", - "sha256": "ecbdcb7f83550bdfee2fafebb494d263de165ee2d7a6fb2a3f21cc886e849b58", - "sizeBytes": 1351 + "sha256": "c5a9c7036a13c598f3d6e3d256d9af9f07b94da951d00032278776914aada8e2", + "sizeBytes": 2245 }, { "path": "scripts/preflight-fly-images.sh", @@ -575,6 +575,11 @@ "sha256": "8f1cd40e89bc9c7551a2e464da29e452052eaa040cbaa3d2c9dece6621d5fef0", "sizeBytes": 3312 }, + { + "path": "scripts/check-final-completion.mjs", + "sha256": "59672ba849bb0bade25aedd58e1cee4a761c67657a2bb236cc977041c6624850", + "sizeBytes": 3128 + }, { "path": "scripts/validate-release-id.mjs", "sha256": "5eca3fbee3ddd6245497243e5f2690d351956af3d9d025afd5e2fadb10766cde", @@ -602,8 +607,8 @@ }, { "path": "tests/baseline/ross-release-train.test.mjs", - "sha256": "5b64b90d4496365264852feeba59556a1b35350488755dcb8d2fec4ad12379a1", - "sizeBytes": 29357 + "sha256": "35b00e64b5dc62f99007d4af24d08dc3d6a157227b9646c27572c07c6f2d5f11", + "sizeBytes": 30119 }, { "path": "tests/baseline/ross-staging-debug.test.mjs", diff --git a/scripts/check-final-completion.mjs b/scripts/check-final-completion.mjs index 5b6b251e85..680a664d37 100644 --- a/scripts/check-final-completion.mjs +++ b/scripts/check-final-completion.mjs @@ -11,6 +11,17 @@ import { evaluateSourceOperations } from "./lib/source-operations.mjs"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const readJson = (path) => JSON.parse(readFileSync(resolve(root, path), "utf8")); const production = process.argv.includes("--production"); +const sourceReportFlag = process.argv.indexOf("--source-report"); +const sourceReportPath = + sourceReportFlag >= 0 + ? process.argv[sourceReportFlag + 1] + : "reports/legal-source-health-v1.json"; +if (!sourceReportPath || sourceReportPath.startsWith("--")) { + console.error( + "Usage: check-final-completion.mjs [--production] [--source-report ]", + ); + process.exit(2); +} const plan = readJson("config/final-completion.v1.json"); const professionalRecord = readJson("config/professional-validation.v1.json"); const benchmark = readJson("tests/evaluation/ontario-benchmark.v1.json"); @@ -19,7 +30,7 @@ const approvals = readJson("config/release-approvals.v1.json"); const operations = readJson("config/operations-readiness.v1.json"); const launch = readJson("config/launch-readiness.v1.json"); const sourcePolicy = readJson("config/legal-source-operations.v1.json"); -const sourceReport = readJson("reports/legal-source-health-v1.json"); +const sourceReport = readJson(sourceReportPath); const manifestConfig = readJson("config/release-manifest.v1.json"); const sourceOperations = evaluateSourceOperations(sourcePolicy, sourceReport); @@ -51,7 +62,16 @@ const result = evaluateFinalCompletion( ); console.log(`${result.ready ? "PASS" : "BLOCKED"}: ${result.mode} final-completion gate.`); +console.log(`Source report: ${sourceReportPath}`); +if (sourceReport.status) console.log(`Live source observation: ${sourceReport.status}`); for (const item of result.pending) console.log(`- PENDING ${item.id}: ${item.ownerRole}`); for (const blocker of result.blockers) console.error(`- ${blocker}`); +for (const [label, details] of [ + ["Source operations", sourceOperations.blockers], + ["Professional validation", professionalValidation.blockers], + ["Release readiness", releaseReadiness.blockers], +]) { + for (const detail of details ?? []) console.error(`- ${label}: ${detail}`); +} if (!result.ready) process.exitCode = 1; diff --git a/scripts/observe-legal-sources.mjs b/scripts/observe-legal-sources.mjs index ff16ffea01..0c56131689 100644 --- a/scripts/observe-legal-sources.mjs +++ b/scripts/observe-legal-sources.mjs @@ -19,7 +19,41 @@ const output = resolve( : "artifacts/legal-source-health-live.json", ); -const report = await observeLiveLegalSources(); +let report; +let observerError = null; +try { + report = await observeLiveLegalSources(); +} catch (error) { + observerError = error; + const observedAt = new Date().toISOString(); + report = { + version: "1.1.0", + observedAt, + liveChecksPerformed: false, + status: "degraded", + requiredProviderIds, + optionalProviderIds, + providers: Object.fromEntries( + [...new Set([...requiredProviderIds, ...optionalProviderIds])].map((id) => [ + id, + { + state: "unavailable", + checkedAt: observedAt, + lastSuccessfulAt: null, + consecutiveFailures: 0, + consecutiveSuccesses: 0, + sourceVersion: null, + latencyClass: null, + reasonCode: "observer-failure", + attempts: 0, + }, + ]), + ), + }; + console.error( + `ROSS live legal-source observer failed before completing: ${error?.name ?? "unknown-error"}`, + ); +} await mkdir(dirname(output), { recursive: true }); await writeFile(output, `${JSON.stringify(report, null, 2)}\n`, "utf8"); @@ -37,4 +71,4 @@ for (const id of observedLiveProviderIds) { } console.log(`Sanitized report: ${output}`); -if (report.status !== "healthy") process.exitCode = 1; +if (observerError || report.status !== "healthy") process.exitCode = 1; diff --git a/tests/baseline/ross-release-train.test.mjs b/tests/baseline/ross-release-train.test.mjs index f585499798..520f807ccf 100644 --- a/tests/baseline/ross-release-train.test.mjs +++ b/tests/baseline/ross-release-train.test.mjs @@ -80,6 +80,12 @@ test("production and rehearsal app names are fixed, distinct, and isolated", () test("the supported workflow is one-button rehearsal with optional public promotion", () => { const workflow = read(".github/workflows/verify-and-deploy-public-beta.yml"); const fullGate = workflow.indexOf("name: Run complete engineering gate"); + const sourceGate = workflow.indexOf( + "name: Observe live legal sources for production gate", + ); + const sourceUpload = workflow.indexOf( + "name: Upload production qualification legal-source report", + ); const dockerGate = workflow.indexOf("name: Build every Fly container path"); const build = workflow.indexOf("name: Build and pin candidate images once"); const rehearsal = workflow.indexOf( @@ -101,6 +107,21 @@ test("the supported workflow is one-button rehearsal with optional public promot /release_id:|fly_organization:|api_app_name:|web_app_name:|confirm_deployment:/, ); assert.ok(fullGate < dockerGate); + assert.ok(fullGate < sourceGate); + assert.ok(sourceGate < sourceUpload); + assert.ok(sourceUpload < dockerGate); + assert.match( + workflow, + /continue-on-error: true[\s\S]*?observe-legal-sources\.mjs[\s\S]*?release-train-legal-source-health\.json/, + ); + assert.match( + workflow, + /final:check[\s\S]*?--source-report artifacts\/release-train-legal-source-health\.json/, + ); + assert.match( + workflow, + /if: always\(\) && inputs\.promote_public[\s\S]*?upload-artifact@v7[\s\S]*?release-train-legal-source-health\.json/, + ); assert.ok(dockerGate < build); assert.ok(build < rehearsal); assert.ok(rehearsal < releasePreflight); diff --git a/tests/final/final-completion.test.mjs b/tests/final/final-completion.test.mjs index 2fd1ec81c7..703ca5c72e 100644 --- a/tests/final/final-completion.test.mjs +++ b/tests/final/final-completion.test.mjs @@ -1,6 +1,13 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; +import { + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { spawnSync } from "node:child_process"; import { dirname, resolve } from "node:path"; +import { tmpdir } from "node:os"; import test from "node:test"; import { fileURLToPath } from "node:url"; import { evaluateFinalCompletion } from "../../scripts/lib/final-completion.mjs"; @@ -46,6 +53,64 @@ test("production final gate accepts the reserved ID but fails closed when depend assert.equal(result.blockers.some((item) => /operational-exercises/i.test(item)), false); }); +test("production final check consumes a fresh runtime source report and shows detail on failure", () => { + const committed = JSON.parse( + readFileSync(resolve(root, "reports/legal-source-health-v1.json"), "utf8"), + ); + const directory = mkdtempSync(resolve(tmpdir(), "ross-final-check-")); + const reportPath = resolve(directory, "runtime-source-report.json"); + const runtime = structuredClone(committed); + const checkedAt = new Date().toISOString(); + runtime.observedAt = checkedAt; + runtime.liveChecksPerformed = true; + runtime.status = "healthy"; + for (const item of Object.values(runtime.providers)) { + if (item.state === "disabled") continue; + item.state = "healthy"; + item.checkedAt = checkedAt; + item.lastSuccessfulAt = checkedAt; + item.consecutiveFailures = 0; + item.consecutiveSuccesses = 1; + } + writeFileSync(reportPath, `${JSON.stringify(runtime)}\n`); + + try { + const passed = spawnSync( + process.execPath, + [ + resolve(root, "scripts/check-final-completion.mjs"), + "--production", + "--source-report", + reportPath, + ], + { cwd: root, encoding: "utf8" }, + ); + assert.equal(passed.status, 0, passed.stderr); + assert.match(passed.stdout, /PASS: production final-completion gate/); + assert.match(passed.stdout, /Source report: .*runtime-source-report\.json/); + + runtime.status = "degraded"; + runtime.providers["ontario-elaws"].state = "degraded"; + runtime.providers["ontario-elaws"].reasonCode = "invalid-response"; + writeFileSync(reportPath, `${JSON.stringify(runtime)}\n`); + const blocked = spawnSync( + process.execPath, + [ + resolve(root, "scripts/check-final-completion.mjs"), + "--production", + "--source-report", + reportPath, + ], + { cwd: root, encoding: "utf8" }, + ); + assert.notEqual(blocked.status, 0); + assert.match(blocked.stderr, /Source operations: ontario-elaws:/); + assert.match(blocked.stderr, /Release readiness: Required legal-source health/); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + test("a coherent evidence-complete controlled-beta record can pass", () => { const completed = structuredClone(plan); completed.status = "completed-approved-for-controlled-beta";