Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/verify-and-deploy-public-beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/release-manifest.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 12 additions & 7 deletions reports/release-manifest-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -522,8 +522,8 @@
},
{
"path": "scripts/observe-legal-sources.mjs",
"sha256": "ecbdcb7f83550bdfee2fafebb494d263de165ee2d7a6fb2a3f21cc886e849b58",
"sizeBytes": 1351
"sha256": "c5a9c7036a13c598f3d6e3d256d9af9f07b94da951d00032278776914aada8e2",
"sizeBytes": 2245
},
{
"path": "scripts/preflight-fly-images.sh",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
22 changes: 21 additions & 1 deletion scripts/check-final-completion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>]",
);
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");
Expand All @@ -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);
Expand Down Expand Up @@ -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;
38 changes: 36 additions & 2 deletions scripts/observe-legal-sources.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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;
21 changes: 21 additions & 0 deletions tests/baseline/ross-release-train.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
Expand Down
67 changes: 66 additions & 1 deletion tests/final/final-completion.test.mjs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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";
Expand Down