diff --git a/.github/scripts/candidate-archive-store.mjs b/.github/scripts/candidate-archive-store.mjs index 0febeed00..f76284c18 100644 --- a/.github/scripts/candidate-archive-store.mjs +++ b/.github/scripts/candidate-archive-store.mjs @@ -874,6 +874,7 @@ function publishStoreEntry(storeRoot, inputRoot, record) { removeOwnedTemporary(temporary, paths.parent, path.basename(paths.entry)); return { admitted: false, ...concurrent }; } + const prepared = lstatSync(temporary, { bigint: true }); try { renameSync(temporary, paths.entry); } catch (error) { @@ -884,6 +885,15 @@ function publishStoreEntry(storeRoot, inputRoot, record) { removeOwnedTemporary(temporary, paths.parent, path.basename(paths.entry)); return { admitted: false, ...concurrent }; } + const published = lstatSync(paths.entry, { bigint: true }); + if ( + !published.isDirectory() + || published.isSymbolicLink() + || published.dev !== prepared.dev + || published.ino !== prepared.ino + ) { + fail("candidate archive store entry was not published by atomic directory rename"); + } return { admitted: true, ...verifyStoreEntry(storeRoot, expected) }; } catch (error) { if (existsSync(temporary)) { diff --git a/.github/scripts/candidate-archive-store.test.mjs b/.github/scripts/candidate-archive-store.test.mjs index 41548c3ee..277c853cc 100644 --- a/.github/scripts/candidate-archive-store.test.mjs +++ b/.github/scripts/candidate-archive-store.test.mjs @@ -17,7 +17,7 @@ import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; import test from "node:test"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { admitCandidateArchive, @@ -291,6 +291,45 @@ test("admission and a later hit materialize the complete exact payload as fresh } }); +test("admission rejects sequential publication beneath the final store key", async () => { + const fixture = createFixture(); + try { + const source = readFileSync(SCRIPT, "utf8"); + const atomicPublication = " renameSync(temporary, paths.entry);"; + assert.equal( + source.split(atomicPublication).length - 1, + 1, + "atomic store publication must have one mutation target", + ); + const sequentialPublication = [ + " mkdirSync(paths.entry, { mode: 0o700 });", + " renameSync(temporaryPayload, paths.payload);", + " renameSync(path.join(temporary, RECORD_FILE), paths.recordFile);", + " rmSync(temporary, { recursive: true });", + ].join("\n"); + const mutantFile = path.join(fixture.root, "candidate-archive-store-mutant.mjs"); + writeFileSync( + mutantFile, + source.replace(atomicPublication, sequentialPublication), + { flag: "wx" }, + ); + const mutant = await import(pathToFileURL(mutantFile).href); + assert.throws( + () => mutant.admitCandidateArchive({ + inputRoot: fixture.inputRoot, + outputDir: outputDir(fixture), + outputRoot: fixture.outputRoot, + record: fixture.record, + storeRoot: fixture.storeRoot, + }), + /not published by atomic directory rename/u, + ); + assert.equal(statExists(outputDir(fixture)), false); + } finally { + cleanup(fixture); + } +}); + test("the public checksum companion pair is mandatory", () => { const fixture = createFixture(); try { diff --git a/.github/scripts/check-calibration-release-lineage.py b/.github/scripts/check-calibration-release-lineage.py index 8339f2867..149676663 100644 --- a/.github/scripts/check-calibration-release-lineage.py +++ b/.github/scripts/check-calibration-release-lineage.py @@ -24,12 +24,21 @@ def main() -> int: ) parser.add_argument("--repo", required=True, type=Path) parser.add_argument("--expected-sha", required=True) + parser.add_argument( + "--allow-promotion-commit", + action="store_true", + help=( + "Permit one tree-preserving main promotion commit whose release " + "parent is the direct constant-freeze child." + ), + ) arguments = parser.parse_args() repository_root = arguments.repo.resolve(strict=True) result = verify_release_head_calibration_lineage( repository_root, arguments.expected_sha, + allow_promotion_commit=arguments.allow_promotion_commit, ) print(json.dumps({"status": "passed", **result}, sort_keys=True)) return 0 diff --git a/.github/scripts/check-workflow-policy.mjs b/.github/scripts/check-workflow-policy.mjs index 5fdd4935b..ceded0134 100644 --- a/.github/scripts/check-workflow-policy.mjs +++ b/.github/scripts/check-workflow-policy.mjs @@ -336,6 +336,29 @@ function at(value, ...keys) { return current; } +function canonicalJson(value) { + if (Array.isArray(value)) { + return `[${value.map(canonicalJson).join(",")}]`; + } + if (value !== null && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +// A parsed job is not its complete execution contract. Workflow-level environment and run +// defaults execute inside every job, while triggers, permissions, concurrency, and future +// top-level fields can change when or with what authority it runs. Hash the entire parsed +// workflow except `jobs`; the acceptance manifest hashes those bodies separately. +function workflowExecutionContext(workflowValue) { + return Object.fromEntries( + Object.entries(object(workflowValue)).filter(([key]) => key !== "jobs"), + ); +} + function scalarStrings(value, found = []) { if (typeof value === "string") { found.push(value); @@ -896,6 +919,8 @@ export function qualificationDriverArtifactViolations( 'binary: "codestory_embedding_qualification.exe"', 'rustTarget: "x86_64-pc-windows-msvc"', "metadata.isSymbolicLink()\n || !metadata.isFile()\n || metadata.nlink !== 1", + "function regularBuildOutput(file, label)", + "!Number.isSafeInteger(metadata.nlink)\n || metadata.nlink < 1", "metadata.isSymbolicLink() || !metadata.isDirectory()", 'fail("qualification driver helper arguments changed")', "containedRelativePath(root, candidate, label)", @@ -904,7 +929,10 @@ export function qualificationDriverArtifactViolations( 'fail(`${label} must not traverse symbolic links`)', "`codestory-cli-v${version}-${assetTarget}.${contract.archiveExtension}`", 'targetDir,\n contract.rustTarget,\n "release",\n contract.binary', + 'const sourceMetadata = regularBuildOutput(', 'fail("qualification driver artifact directory must start empty")', + "copyFileSync(source, staged)", + 'const stagedMetadata = regularFile(staged, "staged qualification driver")', "archiveBytes: archiveMetadata.size", "archiveDigest: sha256(archivePath)", "archiveFile: expectedArchiveFile", @@ -965,15 +993,15 @@ const packagedPlatformWorkflowDigest = // made advisory, parked in dead code, or followed by a payload substitution // while leaving the expected tokens in place. const packagedPlatformCoordinatorWorkflowDigest = - "464906e3cd7ec0e2f7e9195d60de035fdba76172c25d8b0861d0982f9d7dcc3e"; + "29fdda15a93e6cf4526588bad2c746ef8e13afb58e74a7b2b44d9b1a656eb549"; const frozenCandidateQualityWorkflowDigest = "92d0a7ab0e0df63dacd5cc3ef0b58500a6578036494c329aa35279048734f173"; const macosMetalWorkflowDigest = - "e1c4a59b412ba3f2041e89177e2af5a6533cf5ef0371dc2fa18c0309fba5b1ca"; + "05b69d48238284b47b40c13bf15eb1f31370dea55bb77169553d41b46fda1a7f"; const windowsVulkanWorkflowDigest = - "f1123f11d430591380ed433d751c6fc110adfb13225106f50deecf232ee89f9b"; + "c2272dbf4c550ba4a21372e772a87f6df3307f5f4f709b216473f85958157ffe"; const linuxVulkanWorkflowDigest = - "935c5df20a2d57ed18824cca11dc9b8590d72a207e3d1677bc2ceea3048b3dff"; + "b2efe3dec20a466cb798752c714f50e64e265856e80ffafc15b28ea2390367d3"; // Linux owns its compiler server inside Docker, while macOS and Windows own one // in the host shell. Pin both executable programs so a swallowed stop or a // dead-code copy cannot satisfy the ownership fragments below. @@ -987,13 +1015,14 @@ const packagedHostCompilerFinalizerDigest = "b77d8bb12c2748bfe016ab65ccb2f4581356f3ccf1d666e747306caffd6c0c46"; // The companion qualification driver is intentionally retained only inside // the private Actions package artifact. This digest pins both sides of that -// contract: the producer copies only the selected target binary and binds it -// to the exact candidate archive, while the consumer rejects symlinks, extra -// files, identity drift, and byte drift before restoring execute permission. -// Any helper edit therefore requires a policy and mutation-test review in the -// same PR as the workflow change. +// contract: the producer may read Cargo's trusted hard-linked build output, +// but retains only a new singly linked copy bound to the exact candidate +// archive. The consumer rejects symlinks, retained hardlinks, extra files, +// identity drift, and byte drift before restoring execute permission. +// Any helper edit therefore requires policy and mutation-test review in the +// same PR. const qualificationDriverArtifactDigest = - "f7946e03fa6e272ca17f12616b82579da041de4d7c30b19d24ddbc5f1c7f0063"; + "efc5126e24162d52f9da8bac38c3414b3a7492fb17eed5ff19867fadad69623e"; const draftProofCommands = [ "cargo test --locked -p codestory-llama-sys --test native_staging", "cargo test --locked -p codestory-llama-sys --test model_staging", @@ -2147,13 +2176,12 @@ function validatePluginAndDraftWorkflows(workflows, violations, graph) { const sourceConcurrency = [ "source-proof-", promotion.proof_run_sha_expression, - "-${{ inputs.proof_key || inputs.pr_number || github.event.pull_request.number || github.ref }}-", - "${{ github.event.action == 'labeled' && github.event.label.name || 'dispatch' }}", + "-${{ inputs.proof_key || inputs.pr_number || github.ref }}", ].join(""); add( violations, - sameMembers(at(source, "on", "pull_request", "types"), promotion.required_events), - `${sourceFile} pull request trigger must be label-only`, + trigger(source, "pull_request") === undefined, + `${sourceFile} support PR labels must not trigger broad source proof`, ); add( violations, @@ -2170,8 +2198,8 @@ function validatePluginAndDraftWorkflows(workflows, violations, graph) { const resolve = requireJob(violations, sourceFile, source, "resolve"); add( violations, - resolve.if === "github.event_name != 'pull_request' || (github.event.action == 'labeled' && github.event.label.name == 'review-accepted')", - `${sourceFile} resolve job must execute dispatch/call runs and only review-accepted labeled PR runs`, + resolve.if === undefined, + `${sourceFile} resolve job must execute only explicit dispatch and reusable calls`, ); requireStepRun(violations, sourceFile, resolve, "Resolve trusted exact head", [ 'test "$EVENT_HEAD_REPO" = "$GITHUB_REPOSITORY"', @@ -2185,14 +2213,23 @@ function validatePluginAndDraftWorkflows(workflows, violations, graph) { requireExactResolverContract(violations, sourceFile, resolve, sourceResolverContractDigest); requireStepRun(violations, sourceFile, resolve, "Reuse a completed gate for this exact head", [ '.path == ".github/workflows/source-proof.yml"', - '(.event == "pull_request" or .event == "workflow_dispatch") and .conclusion == "success"', + '.event == "workflow_dispatch" and .conclusion == "success"', '.name == "full-source-gate" and .conclusion == "success"', + 'artifact_name="release-cell-prepublish-source-attempt-$run_attempt"', + ".expired == false", + 'test "$artifact_count" = 1 || continue', + ]); + requireStepRun(violations, sourceFile, resolve, "Require executable release freeze", [ + "repos/$GITHUB_REPOSITORY/git/commits/$HEAD_SHA", + "release-freeze-barrier.mjs", + "verify-status", + '--receipt-digest "$FREEZE_RECEIPT_DIGEST"', ]); const full = requireJob(violations, sourceFile, source, "full-source-gate"); add(violations, sameMembers(needs(full), ["resolve"]), `${sourceFile} full source gate must need resolve`); add( violations, - full.if === "needs.resolve.outputs.reuse != 'true'", + full.if === "${{ !inputs.acceptance_only && needs.resolve.outputs.reuse != 'true' }}", `${sourceFile} full source gate may skip only a completed exact-head proof`, ); const generalization = requireJob( @@ -2217,7 +2254,8 @@ function validatePluginAndDraftWorkflows(workflows, violations, graph) { violations, generalization.name === "retrieval-generalization" && sameMembers(needs(generalization), ["resolve"]) - && generalization.if === "needs.resolve.outputs.reuse != 'true'" + && generalization.if + === "${{ !inputs.acceptance_only && needs.resolve.outputs.reuse != 'true' }}" && generalization["runs-on"] === "ubuntu-latest" && generalization["timeout-minutes"] === 5 && generalization["continue-on-error"] === undefined, @@ -2283,7 +2321,8 @@ function validatePluginAndDraftWorkflows(workflows, violations, graph) { ]) && windowsNative.name === "windows-native-contracts" && sameMembers(needs(windowsNative), ["resolve"]) - && windowsNative.if === "needs.resolve.outputs.reuse != 'true'" + && windowsNative.if + === "${{ !inputs.acceptance_only && needs.resolve.outputs.reuse != 'true' }}" && windowsNative["runs-on"] === "windows-latest" && windowsNative["timeout-minutes"] === 15 && object(windowsNative.env).CMAKE_GENERATOR === "Ninja" @@ -2542,9 +2581,9 @@ function validatePluginAndDraftWorkflows(workflows, violations, graph) { add( violations, sourceCellUpload?.uses === "actions/upload-artifact@v7.0.1" - && String(sourceCellUpload?.if ?? "").includes("success()") - && String(sourceCellUpload?.if ?? "").includes("inputs.emit_release_cells"), - `${sourceFile} source release cell must be a success-only retained artifact`, + && sourceCellUpload?.if === "success()" + && !scalarStrings(source).some(value => value.includes("emit_release_cells")), + `${sourceFile} source release cell must be an unconditional success-only retained artifact`, ); } } @@ -2651,7 +2690,11 @@ function validateReleaseCoordinator(workflows, violations, graph) { JSON.stringify(releaseCallers) === JSON.stringify(["auto-release.yml"]), `${releaseFile} publication authority must have only the trusted auto-release.yml caller`, ); - add(violations, object(release.permissions).actions === "read", `${releaseFile} must read prior-run evidence`); + add( + violations, + object(release.permissions).actions === "write", + `${releaseFile} must cancel superseded proof runs before starting release work`, + ); add( violations, object(release.permissions)["pull-requests"] === "read", @@ -2669,6 +2712,14 @@ function validateReleaseCoordinator(workflows, violations, graph) { callPublish.required === false && callPublish.type === "boolean" && callPublish.default === false, `${releaseFile} workflow_call publish_release must be a fail-closed boolean`, ); + for (const input of ["calibration_bundle_artifact", "calibration_bundle_run_id"]) { + add( + violations, + at(release, "on", "workflow_call", "inputs", input) === undefined + && at(release, "on", "workflow_dispatch", "inputs", input) === undefined, + `${releaseFile} must not accept calibration bundle inputs; lineage comes from the frozen constant set`, + ); + } const dispatchExpectedHead = object(at(release, "on", "workflow_dispatch", "inputs", "expected_head_sha")); add( violations, @@ -2686,12 +2737,6 @@ function validateReleaseCoordinator(workflows, violations, graph) { release.env === undefined && release.defaults === undefined, `${releaseFile} release workflow must not override the release-head calibration execution environment`, ); - requireNoCalibrationReferences( - violations, - releaseFile, - release, - [["preflight", releaseLineageStepName]], - ); const policy = requireJob(violations, releaseFile, release, "workflow-policy"); // The reuse-binding contracts resolve real release commits, which a depth-1 // clone does not carry: it answered only while the referenced commit happened @@ -2757,26 +2802,25 @@ function validateReleaseCoordinator(workflows, violations, graph) { && preflight["continue-on-error"] === undefined && hasExactKeys( releaseLineage, - ["name", "env", "shell", "working-directory", "run"], + ["name", "id", "env", "shell", "working-directory", "run"], ) - && hasExactKeys(object(releaseLineage?.env), ["BASH_ENV"]) + && releaseLineage?.id === "lineage" + && hasExactKeys(object(releaseLineage?.env), ["BASH_ENV", "PUBLISH_RELEASE"]) && object(releaseLineage?.env).BASH_ENV === "/dev/null" + && object(releaseLineage?.env).PUBLISH_RELEASE === "${{ inputs.publish_release }}" && releaseLineage?.shell === "/bin/bash --noprofile --norc -e -o pipefail {0}" && releaseLineage?.["working-directory"] === "${{ github.workspace }}", `${releaseFile} release-head calibration lineage must be unconditional and fail closed`, ); - add( - violations, - sameStrings( - nonCommentLines(releaseLineage?.run), - [ - "/usr/bin/python3 -E -s " - + '"$GITHUB_WORKSPACE/.github/scripts/check-calibration-release-lineage.py" ' - + '--repo "$GITHUB_WORKSPACE" --expected-sha "$GITHUB_SHA"', - ], - ), - `${releaseFile} release-head calibration lineage must use the pinned interpreter on the exact release checkout`, - ); + requireStepRun(violations, releaseFile, preflight, releaseLineageStepName, [ + "/usr/bin/python3 -E -s", + '"$GITHUB_WORKSPACE/.github/scripts/check-calibration-release-lineage.py"', + '--repo "$GITHUB_WORKSPACE"', + '--expected-sha "$GITHUB_SHA"', + "--allow-promotion-commit", + "selection_commit", + "selection_tree", + ]); const preflightCheckout = namedStep(preflight, "Checkout"); add( violations, @@ -2789,7 +2833,8 @@ function validateReleaseCoordinator(workflows, violations, graph) { add( violations, stepIndex(preflight, "Checkout") === 0 - && stepIndex(preflight, releaseLineageStepName) === 1 + && stepIndex(preflight, "Cancel superseded proof runs") === 1 + && stepIndex(preflight, releaseLineageStepName) === 2 && stepIndex(preflight, releaseLineageStepName) < stepIndex(preflight, "Validate release authority") && stepIndex(preflight, releaseLineageStepName) @@ -2857,12 +2902,27 @@ function validateReleaseCoordinator(workflows, violations, graph) { `${releaseFile} source proof may be skipped only when preflight resolved reusable evidence`, ); requireStepRun(violations, releaseFile, requireJob(violations, releaseFile, release, "preflight"), "Resolve reusable prior evidence", [ - 'git rev-parse "$GITHUB_SHA^{tree}"', - "merge-base --is-ancestor", + 'release_tree="$(git rev-parse "$GITHUB_SHA^{tree}")"', + 'test "$(git rev-parse "$head_sha^{tree}")" = "$release_tree"', + 'git merge-base --is-ancestor "$head_sha" "$GITHUB_SHA"', "full-source-gate", '.path == ".github/workflows/source-proof.yml"', - '.head_repository.full_name == $repo and .conclusion == "success"', + '.event == "workflow_dispatch" and .conclusion == "success"', + "The release workflow will not start a broad proof", + 'artifact_name="release-cell-prepublish-source-attempt-$run_attempt"', + ".expired == false", + 'test "$artifact_count" = 1 || continue', ]); + forbidStepRun( + violations, + releaseFile, + requireJob(violations, releaseFile, release, "preflight"), + "Resolve reusable prior evidence", + [ + "release-freeze-barrier.mjs verify-status", + "freeze_receipt_digest", + ], + ); const closeout = requireJob(violations, releaseFile, release, "pre-publish-closeout"); requireStepRun(violations, releaseFile, closeout, "Authenticate pre-publish Actions provenance", [ '--reuse "$REUSE_SELECTION"', @@ -2873,7 +2933,13 @@ function validateReleaseCoordinator(workflows, violations, graph) { && String(closeout.if ?? "").includes("needs.preflight.result == 'success'"), `${releaseFile} closeout must accept a skipped source gate only alongside a successful preflight`, ); - add(violations, object(source.with).version === "${{ needs.preflight.outputs.version }}" && object(source.with).emit_release_cells === true, `${releaseFile} source proof must emit its authenticated release cell`); + add( + violations, + object(source.with).version === "${{ needs.preflight.outputs.version }}" + && object(source.with).freeze_receipt_digest === "" + && object(source.with).emit_release_cells === undefined, + `${releaseFile} unreachable source fallback must fail closed without a post-calibration freeze`, + ); const packaged = requireJob(violations, releaseFile, release, "packaged-proof"); add(violations, packaged.uses === "./.github/workflows/packaged-platform-proof.yml", `${releaseFile} packaged-proof must call the package workflow`); @@ -5073,13 +5139,12 @@ function validatePackagedCoordinator(workflows, violations, graph) { const expectedConcurrency = [ "proof-", promotion.proof_run_sha_expression, - "-${{ inputs.mode || 'platform' }}-${{ inputs.pr_number || github.event.pull_request.number || 'dev' }}-", - "${{ github.event.action == 'labeled' && github.event.label.name || 'dispatch' }}", + "-${{ inputs.mode || 'platform' }}-${{ inputs.pr_number || 'dev' }}", ].join(""); add( violations, - sameMembers(at(workflow, "on", "pull_request", "types"), promotion.required_events), - `${file} pull request trigger must be label-only`, + trigger(workflow, "pull_request") === undefined, + `${file} support PR labels must not trigger package or hardware proof`, ); add( violations, @@ -5109,13 +5174,17 @@ function validatePackagedCoordinator(workflows, violations, graph) { `${file} dispatch scopes changed`, ); add(violations, trigger(workflow, "pull_request_target") === undefined, `${file} must not use pull_request_target`); - add(violations, object(workflow.permissions).actions === "read", `${file} must read source-proof runs`); + add( + violations, + object(workflow.permissions).actions === "write", + `${file} must cancel superseded proof runs before package or hardware work`, + ); add(violations, object(workflow.permissions).contents === "read", `${file} must use read-only contents permission`); const route = requireJob(violations, file, workflow, "route"); add( violations, - route.if === "github.event_name != 'pull_request' || (github.event.action == 'labeled' && github.event.label.name == 'platform-proof')", - `${file} route job must execute dispatch runs and only platform-proof labeled PR runs`, + route.if === undefined, + `${file} route job must execute only explicit dispatches`, ); requireStepRun(violations, file, route, "Resolve trusted exact head", [ 'test "$head_repo" = "$GITHUB_REPOSITORY"', @@ -5139,10 +5208,35 @@ function validatePackagedCoordinator(workflows, violations, graph) { INPUT_CALIBRATION_RUN_ID: "${{ inputs.calibration_bundle_run_id }}", }); requireExactResolverContract(violations, file, route, platformResolverContractDigest); + add( + violations, + namedStep(route, "Require executable release freeze")?.if === undefined, + `${file} every broad proof mode must authenticate its exact candidate head`, + ); + requireStepRun(violations, file, route, "Require executable release freeze", [ + "repos/$GITHUB_REPOSITORY/git/commits/$HEAD_SHA", + "release-freeze-barrier.mjs verify-status", + '--commit "$HEAD_SHA"', + 'if [ "$RESOLVED_MODE" = calibration ]; then', + "freeze_phase=calibration_source", + "freeze_phase=frozen_candidate", + '--phase "$freeze_phase"', + '--receipt-digest "$FREEZE_RECEIPT_DIGEST"', + ]); + requireStepEnv(violations, file, route, "Require executable release freeze", { + RESOLVED_MODE: "${{ steps.resolve.outputs.mode }}", + }); + const exactHeadSourceProof = namedStep(route, "Require successful exact-head source proof"); + add( + violations, + exactHeadSourceProof?.if + === "steps.resolve.outputs.mode != 'integration' && steps.resolve.outputs.mode != 'calibration'", + `${file} calibration must precede the sole frozen-candidate source proof`, + ); requireStepRun(violations, file, route, "Require successful exact-head source proof", [ "actions/runs?head_sha=$HEAD_SHA", '.path == ".github/workflows/source-proof.yml"', - '(.event == "pull_request" or .event == "workflow_dispatch") and .conclusion == "success"', + '.event == "workflow_dispatch" and .conclusion == "success"', '.name == "full-source-gate" and .conclusion == "success"', ]); requireStepRun(violations, file, route, "Select change-aware proof scope", [ @@ -5848,7 +5942,11 @@ function validateRemainingWorkflows(workflows, violations) { add(violations, release.uses === "./.github/workflows/release.yml", `${autoFile} must call the release workflow`); add(violations, sameMembers(needs(release), ["detect-version"]), `${autoFile} release must need version detection`); add(violations, object(release.permissions).contents === "write", `${autoFile} release caller must grant contents write`); - add(violations, object(release.permissions).actions === "read", `${autoFile} release caller must grant actions read`); + add( + violations, + object(release.permissions).actions === "write", + `${autoFile} release caller must grant actions write for superseded-run cancellation`, + ); add( violations, object(release.permissions)["pull-requests"] === "read", @@ -5920,8 +6018,13 @@ function validateRemainingWorkflows(workflows, violations) { === macosMetalWorkflowDigest, `${metalFile} must match the reviewed protected Metal workflow structure`, ); - add(violations, trigger(metal, "workflow_call") !== undefined && trigger(metal, "workflow_dispatch") !== undefined, `${metalFile} must support reusable and manual proof`); - for (const event of ["workflow_call", "workflow_dispatch"]) { + add( + violations, + trigger(metal, "workflow_call") !== undefined + && trigger(metal, "workflow_dispatch") === undefined, + `${metalFile} must be coordinator-only and not directly dispatchable`, + ); + for (const event of ["workflow_call"]) { for (const key of ["calibration_bundle_artifact", "calibration_bundle_run_id"]) { requireOptionalStringInput(violations, metalFile, metal, event, key); } @@ -6563,8 +6666,13 @@ function validateRemainingWorkflows(workflows, violations) { === windowsVulkanWorkflowDigest, `${vulkanFile} must match the reviewed protected Windows Vulkan workflow structure`, ); - add(violations, trigger(vulkan, "workflow_call") !== undefined && trigger(vulkan, "workflow_dispatch") !== undefined, `${vulkanFile} must support reusable and manual proof`); - for (const event of ["workflow_call", "workflow_dispatch"]) { + add( + violations, + trigger(vulkan, "workflow_call") !== undefined + && trigger(vulkan, "workflow_dispatch") === undefined, + `${vulkanFile} must be coordinator-only and not directly dispatchable`, + ); + for (const event of ["workflow_call"]) { for (const key of ["calibration_bundle_artifact", "calibration_bundle_run_id"]) { requireOptionalStringInput(violations, vulkanFile, vulkan, event, key); } @@ -7194,10 +7302,10 @@ function validateRemainingWorkflows(workflows, violations) { add( violations, trigger(linuxVulkan, "workflow_call") !== undefined - && trigger(linuxVulkan, "workflow_dispatch") !== undefined, - `${linuxVulkanFile} must support reusable and manual proof`, + && trigger(linuxVulkan, "workflow_dispatch") === undefined, + `${linuxVulkanFile} must be coordinator-only and not directly dispatchable`, ); - for (const event of ["workflow_call", "workflow_dispatch"]) { + for (const event of ["workflow_call"]) { for (const key of ["calibration_bundle_artifact", "calibration_bundle_run_id"]) { requireOptionalStringInput(violations, linuxVulkanFile, linuxVulkan, event, key); } @@ -7231,44 +7339,21 @@ function validateRemainingWorkflows(workflows, violations) { const optionalCalibrationInput = object(at( linuxVulkan, "on", - "workflow_dispatch", + "workflow_call", "inputs", "constant_calibration_mode", )); add( violations, - at(linuxVulkan, "on", "workflow_call", "inputs", "constant_calibration_mode") - === undefined - && optionalCalibrationInput.required === false + optionalCalibrationInput.required === false && optionalCalibrationInput.type === "boolean" && optionalCalibrationInput.default === false, - `${linuxVulkanFile} optional constant calibration must be manual-only and off by default`, + `${linuxVulkanFile} optional constant calibration must be coordinator-only and off by default`, ); - const manualPackageRunInput = object(at( - linuxVulkan, - "on", - "workflow_dispatch", - "inputs", - "package_run_id", - )); add( violations, - manualPackageRunInput.required === false - && manualPackageRunInput.type === "string" - && manualPackageRunInput.default === "", - `${linuxVulkanFile} standalone constant calibration must not require an upstream package run`, - ); - add( - violations, - at( - linuxVulkan, - "on", - "workflow_dispatch", - "inputs", - "candidate_producer_workflow_path", - "default", - ) === ".github/workflows/packaged-platform-pr.yml", - `${linuxVulkanFile} manual candidate proof must trust the package-producing workflow`, + trigger(linuxVulkan, "workflow_dispatch") === undefined, + `${linuxVulkanFile} standalone proof must not bypass the coordinator`, ); const route = requireJob(violations, linuxVulkanFile, linuxVulkan, "route"); add( @@ -7718,11 +7803,11 @@ function validateRemainingWorkflows(workflows, violations) { add( violations, optionalCalibration.if - === "${{ github.event_name == 'workflow_dispatch' && inputs.constant_calibration_mode }}" + === "${{ inputs.constant_calibration_mode }}" && JSON.stringify(optionalCalibration["runs-on"]) === JSON.stringify(["self-hosted", "Linux", "X64", "codestory-linux-vulkan"]) && optionalCalibration.environment === "linux-vulkan-proof", - `${linuxVulkanFile} optional calibration must be a standalone protected manual Vulkan job`, + `${linuxVulkanFile} optional calibration must be a standalone protected coordinator-only Vulkan job`, ); requireStepRun( violations, @@ -7736,7 +7821,7 @@ function validateRemainingWorkflows(workflows, violations) { ); const optionalCollectorName = "Collect optional Linux Vulkan constant calibration"; requireStepRun(violations, linuxVulkanFile, optionalCalibration, optionalCollectorName, [ - 'test "$GITHUB_EVENT_NAME" = workflow_dispatch', + 'test "$CONSTANT_CALIBRATION_MODE" = true', "--engine-policy accelerated", "--expected-backend Vulkan", "--proof-tier calibration", @@ -8089,6 +8174,798 @@ export function releaseProofCpuSelectorViolations( return violations; } +export function releaseFreezeBarrierWorkflowViolations( + workflows, + graph = loadReleaseClaimGraph(repositoryRoot), + barrierSource = fs.readFileSync( + path.join(repositoryRoot, ".github", "scripts", "release-freeze-barrier.mjs"), + "utf8", + ), + acceptanceManifestSource = fs.readFileSync( + path.join( + repositoryRoot, + ".github", + "scripts", + "release-freeze-acceptance-jobs.json", + ), + "utf8", + ), +) { + const violations = []; + for (const [file, workflow] of workflows) { + add( + violations, + !scalarStrings(workflow).some(value => value.includes("verify-pending")), + `[freeze_barrier] ${file} must never trust a caller-authored pending freeze`, + ); + } + const freeze = object(graph.workflow_policy.release_freeze_barrier); + const acceptance = object(freeze.acceptance); + const acceptancePhases = object(acceptance.phases); + const calibrationSourcePhase = object(acceptancePhases.calibration_source); + const frozenCandidatePhase = object(acceptancePhases.frozen_candidate); + let acceptanceManifest = {}; + try { + acceptanceManifest = object(JSON.parse(acceptanceManifestSource)); + } catch { + violations.push( + "[freeze_barrier] canonical acceptance job manifest must be valid JSON", + ); + } + const acceptanceManifestJobs = object(acceptanceManifest.jobs); + const acceptanceJobNames = [ + "resolve", + "freeze-hostile-mutations", + "freeze-windows-native-probe", + "freeze-acceptance", + ]; + const acceptanceManifestDigest = createHash("sha256") + .update(acceptanceManifestSource) + .digest("hex"); + add( + violations, + freeze.schema === 3 + && freeze.script === ".github/scripts/release-freeze-barrier.mjs" + && freeze.status_context_prefix === "codestory/release-freeze" + && sameMembers(list(freeze.allowed_future_source_changes), [ + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json", + ]) + && freeze.invalidation_workflow === "release-freeze-invalidation.yml" + && acceptance.producer_workflow === "source-proof.yml" + && acceptance.receipt_authority === "github_actions" + && acceptance.receipt_artifact + === "release-freeze-receipt-attempt-${{ github.run_attempt }}" + && acceptance.receipt_file === "release-freeze-receipt.json" + && acceptance.receipt_producer_job === "resolve" + && acceptance.status_scope === "exact_candidate_head" + && acceptance.later_commit_revokes === true + && acceptance.event === "workflow_dispatch" + && acceptance.hostile_job === "freeze-hostile-mutations" + && acceptance.hostile_step === "Execute exact-head hostile mutation matrix" + && acceptance.windows_job === "freeze-windows-native-probe" + && acceptance.windows_step === "Run exact-head Windows native probe" + && sameMembers(list(acceptance.windows_runner), [ + "self-hosted", + "Windows", + "X64", + "codestory-vulkan", + ]) + && acceptance.windows_probe_max_seconds === 90 + && acceptance.publisher_job === "freeze-acceptance" + && acceptance.publisher_step === "Publish executable release freeze" + && acceptance.status_creator === "github-actions[bot]" + && acceptance.job_manifest + === ".github/scripts/release-freeze-acceptance-jobs.json" + && /^[0-9a-f]{64}$/u.test(String(acceptance.job_manifest_sha256 ?? "")) + && acceptance.job_manifest_sha256 === acceptanceManifestDigest + && sameMembers(list(calibrationSourcePhase.known_future_source_changes), [ + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json", + ]) + && JSON.stringify(list(calibrationSourcePhase.planned_actions)) === JSON.stringify([ + "calibration-source-acceptance", + "calibration", + "generated-constant-freeze", + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", + ]) + && calibrationSourcePhase.next_permitted_mutation + === "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json" + && list(frozenCandidatePhase.known_future_source_changes).length === 0 + && JSON.stringify(list(frozenCandidatePhase.planned_actions)) === JSON.stringify([ + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", + ]) + && frozenCandidatePhase.next_permitted_mutation === null, + "[freeze_barrier] release claim graph must pin the executable exact-head freeze contract", + ); + add( + violations, + hasExactKeys(acceptanceManifest, [ + "schema", + "workflow", + "workflow_context_sha256", + "jobs", + ]) + && acceptanceManifest.schema === "codestory.release-freeze-acceptance-jobs/v2" + && acceptanceManifest.workflow === ".github/workflows/source-proof.yml" + && /^[0-9a-f]{64}$/u.test( + String(acceptanceManifest.workflow_context_sha256 ?? ""), + ) + && sameMembers(Object.keys(acceptanceManifestJobs), acceptanceJobNames) + && acceptanceJobNames.every(jobName => + /^[0-9a-f]{64}$/u.test(String(acceptanceManifestJobs[jobName] ?? "")) + ), + "[freeze_barrier] canonical acceptance job manifest must pin exactly the executable acceptance jobs", + ); + add( + violations, + barrierSource.includes('gh(["api", `repos/${repository}/pulls/${number}`])') + && barrierSource.includes( + "`repos/${repository}/git/ref/heads/dev/codestory-next`", + ) + && barrierSource.includes( + "`repos/${repository}/compare/${liveBaseCommit}...${commit}`", + ) + && barrierSource.includes("base_commit: liveBaseCommit") + && barrierSource.includes("const currentReleasePr = releasePr(") + && barrierSource.includes( + "currentReleasePr.base_commit !== receipt?.release_pr?.base_commit", + ) + && barrierSource.includes("release PR base advanced after freeze acceptance") + && barrierSource.includes("git([\"merge-base\", \"--is-ancestor\", mergeCommit, commit]") + && barrierSource.includes("support PR #${number} is not merged"), + "[freeze_barrier] Actions receipt authority must recheck the live release PR base and integrated support PR ancestry", + ); + add( + violations, + barrierSource.includes("for (const status of ACTIVE_RUN_STATES)") + && barrierSource.includes('"api",\n "--paginate",\n "--slurp",') + && barrierSource.includes( + "`repos/${repository}/actions/runs?status=${status}&per_page=100`", + ) + && !barrierSource.includes('"run",\n "list",'), + "[freeze_barrier] obsolete-run discovery must paginate every active Actions state", + ); + + const invalidationFile = freeze.invalidation_workflow; + const invalidation = workflows.get(invalidationFile); + add( + violations, + sameMembers(at(invalidation, "on", "pull_request", "branches"), [ + "dev/codestory-next", + ]) + && sameMembers(at(invalidation, "on", "pull_request", "types"), [ + "synchronize", + ]) + && sameMembers(at(invalidation, "on", "push", "branches"), [ + "dev/codestory-next", + ]) + && object(invalidation.permissions).actions === "write" + && object(invalidation.permissions).contents === "read" + && object(invalidation.permissions).statuses === "write" + && at(invalidation, "concurrency", "cancel-in-progress") === true, + "[freeze_barrier] release freeze invalidation must run automatically when a candidate head is superseded", + ); + const invalidationJob = requireJob( + violations, + invalidationFile, + invalidation, + "invalidate", + ); + add( + violations, + invalidationJob["runs-on"] === "ubuntu-latest" + && invalidationJob["timeout-minutes"] === 5 + && sameMembers( + list(invalidationJob.steps).map(step => step?.name ?? step?.uses), + [ + "actions/checkout@v5", + "Invalidate a superseded release freeze", + ], + ), + "[freeze_barrier] release freeze invalidation must remain one bounded cancellation job", + ); + requireStepRun( + violations, + invalidationFile, + invalidationJob, + "Invalidate a superseded release freeze", + [ + 'test "$BEFORE_SHA" != "$AFTER_SHA"', + "commits/$BEFORE_SHA/statuses?per_page=100", + '.state == "success"', + 'startswith("codestory/release-freeze/")', + 'if [ -z "$freeze_contexts" ]; then', + '"repos/$GITHUB_REPOSITORY/statuses/$BEFORE_SHA"', + "-f state=error", + '-f "context=$context"', + '-f "description=superseded-by=$AFTER_SHA"', + "release-freeze-barrier.mjs invalidate-superseded", + '--commit "$AFTER_SHA"', + '--broad-workflow "Exact-head source proof"', + '--broad-workflow "Platform and integration proof"', + '--broad-workflow "Release"', + '--broad-workflow "Auto Release"', + ], + ); + forbidStepRun( + violations, + invalidationFile, + invalidationJob, + "Invalidate a superseded release freeze", + [ + '.state == "pending"', + ".state == 'pending'", + ], + ); + requireStepEnv( + violations, + invalidationFile, + invalidationJob, + "Invalidate a superseded release freeze", + { + AFTER_SHA: "${{ github.event.after || github.sha }}", + BEFORE_SHA: "${{ github.event.before }}", + EVENT_NAME: "${{ github.event_name }}", + }, + ); + const invalidationRun = executableRunText(stepRun( + invalidationJob, + "Invalidate a superseded release freeze", + )); + add( + violations, + occurrenceCount(invalidationRun, "release-freeze-barrier.mjs invalidate-superseded") + === 2 + && occurrenceCount(invalidationRun, '--broad-workflow "Auto Release"') === 2 + && invalidationRun.indexOf('if [ "$EVENT_NAME" = push ]; then') + < invalidationRun.indexOf("commits/$BEFORE_SHA/statuses?per_page=100") + && invalidationRun.indexOf("release-freeze-barrier.mjs invalidate-superseded") + < invalidationRun.indexOf("commits/$BEFORE_SHA/statuses?per_page=100"), + "[freeze_barrier] every dev push must cancel obsolete proof before PR-status revocation logic", + ); + + for (const file of ["source-proof.yml", "packaged-platform-pr.yml"]) { + const workflow = workflows.get(file); + add( + violations, + trigger(workflow, "pull_request") === undefined, + `[proof_identity] ${file} must not run broad proof from a support PR event`, + ); + add( + violations, + String(at(workflow, "concurrency", "group") ?? "").includes("${{ github.sha }}"), + `[proof_identity] ${file} concurrency must bind the exact Actions SHA`, + ); + const freezeInput = object(at( + workflow, + "on", + "workflow_dispatch", + "inputs", + "freeze_receipt_digest", + )); + add( + violations, + ( + file === "source-proof.yml" + ? freezeInput.required === false && freezeInput.default === "" + : freezeInput.required === true && freezeInput.default === undefined + ) + && freezeInput.type === "string", + file === "source-proof.yml" + ? "[freeze_barrier] source acceptance must mint its own receipt digest" + : "[freeze_barrier] packaged proof must require an exact-head freeze digest", + ); + if (file === "source-proof.yml") { + const dispatchVersionInput = object(at( + workflow, + "on", + "workflow_dispatch", + "inputs", + "version", + )); + const callVersionInput = object(at( + workflow, + "on", + "workflow_call", + "inputs", + "version", + )); + const acceptanceInput = object(at( + workflow, + "on", + "workflow_dispatch", + "inputs", + "acceptance_only", + )); + const acceptancePhaseInput = object(at( + workflow, + "on", + "workflow_dispatch", + "inputs", + "acceptance_phase", + )); + const callFreezeInput = object(at( + workflow, + "on", + "workflow_call", + "inputs", + "freeze_receipt_digest", + )); + add( + violations, + dispatchVersionInput.required === true + && dispatchVersionInput.type === "string" + && callVersionInput.required === true + && callVersionInput.type === "string" + && callFreezeInput.required === true + && callFreezeInput.type === "string" + && acceptanceInput.required === false + && acceptanceInput.type === "boolean" + && acceptanceInput.default === false + && acceptancePhaseInput.required === false + && acceptancePhaseInput.type === "choice" + && acceptancePhaseInput.default === "frozen_candidate" + && JSON.stringify(list(acceptancePhaseInput.options)) + === JSON.stringify(["calibration_source", "frozen_candidate"]) + && at(workflow, "on", "workflow_dispatch", "inputs", "emit_release_cells") + === undefined + && at(workflow, "on", "workflow_call", "inputs", "emit_release_cells") + === undefined, + "[freeze_barrier] source-proof.yml must separate acceptance from broad proof", + ); + add( + violations, + object(workflow.permissions).statuses === "write", + "[freeze_barrier] source-proof.yml acceptance must publish an exact-head commit status", + ); + } else { + add( + violations, + object(workflow.permissions).statuses === "read", + "[freeze_barrier] packaged-platform-pr.yml must authenticate the exact-head freeze status", + ); + } + add( + violations, + object(workflow.permissions).actions === "write", + `[freeze_barrier] ${file} must be able to cancel superseded runs`, + ); + const coordinatorJob = file === "source-proof.yml" ? "resolve" : "route"; + requireStepRun( + violations, + file, + requireJob(violations, file, workflow, coordinatorJob), + "Cancel superseded proof runs", + [ + "release-freeze-barrier.mjs cancel-superseded", + '--commit "$HEAD_SHA"', + '--broad-workflow "Exact-head source proof"', + '--broad-workflow "Platform and integration proof"', + '--broad-workflow "Release"', + '--broad-workflow "Auto Release"', + ], + ); + } + + const sourceWorkflow = workflows.get("source-proof.yml"); + const sourceJobNames = [ + "resolve", + "freeze-hostile-mutations", + "freeze-windows-native-probe", + "freeze-acceptance", + "full-source-gate", + "retrieval-generalization", + "windows-native-contracts", + ]; + add( + violations, + sameMembers(Object.keys(object(sourceWorkflow.jobs)), sourceJobNames), + "[freeze_barrier] source-proof.yml must use the closed source and acceptance job contract", + ); + const actualWorkflowContextDigest = createHash("sha256") + .update(canonicalJson(workflowExecutionContext(sourceWorkflow))) + .digest("hex"); + add( + violations, + actualWorkflowContextDigest === acceptanceManifest.workflow_context_sha256, + "[freeze_barrier] source-proof.yml workflow execution context must match the canonical acceptance manifest", + ); + for (const jobName of acceptanceJobNames) { + const actualDigest = createHash("sha256") + .update(canonicalJson(object(at(sourceWorkflow, "jobs", jobName)))) + .digest("hex"); + add( + violations, + actualDigest === acceptanceManifestJobs[jobName], + `[freeze_barrier] source-proof.yml ${jobName} must match the canonical acceptance job manifest`, + ); + } + const sourceResolve = requireJob( + violations, + "source-proof.yml", + sourceWorkflow, + acceptance.receipt_producer_job, + ); + const acceptedCheckout = namedStep(sourceResolve, "Checkout accepted source head"); + add( + violations, + acceptedCheckout?.uses === "actions/checkout@v5" + && object(acceptedCheckout.with).ref === "${{ steps.resolve.outputs.ref }}" + && object(acceptedCheckout.with)["fetch-depth"] === 0, + "[freeze_barrier] Actions receipt generation must have complete history for support PR ancestry", + ); + const recordReceipt = namedStep(sourceResolve, "Record executable release freeze"); + add( + violations, + recordReceipt?.if === "${{ inputs.acceptance_only }}", + "[freeze_barrier] Actions may generate a release freeze receipt only in acceptance mode", + ); + requireStepRun( + violations, + "source-proof.yml", + sourceResolve, + "Record executable release freeze", + [ + 'test -z "$CALLER_FREEZE_RECEIPT_DIGEST"', + "release-freeze-barrier.mjs record-actions-receipt", + '--repository "$GITHUB_REPOSITORY"', + '--repo "$GITHUB_WORKSPACE"', + '--branch "$GITHUB_REF_NAME"', + '--commit "$HEAD_SHA"', + '--tree "$tree"', + '--release-pr "$PR_NUMBER"', + '--support-prs-json "$SUPPORT_PRS_JSON"', + '--reusable-evidence-json "$REUSABLE_EVIDENCE_JSON"', + '--invalidated-evidence-json "$INVALIDATED_EVIDENCE_JSON"', + '--cancelled-runs-json "$CANCELLED_RUNS_JSON"', + '--run-id "$GITHUB_RUN_ID"', + '--run-attempt "$GITHUB_RUN_ATTEMPT"', + '--phase "$ACCEPTANCE_PHASE"', + '--output "$RUNNER_TEMP/release-freeze-receipt.json"', + '--github-output "$GITHUB_OUTPUT"', + ], + ); + requireStepEnv( + violations, + "source-proof.yml", + sourceResolve, + "Record executable release freeze", + { + CALLER_FREEZE_RECEIPT_DIGEST: "${{ inputs.freeze_receipt_digest }}", + ACCEPTANCE_PHASE: "${{ inputs.acceptance_phase }}", + CANCELLED_RUNS_JSON: "${{ steps.cancel.outputs.cancelled }}", + HEAD_SHA: "${{ steps.resolve.outputs.ref }}", + INVALIDATED_EVIDENCE_JSON: "${{ inputs.invalidated_evidence_json }}", + PR_NUMBER: "${{ inputs.pr_number }}", + REUSABLE_EVIDENCE_JSON: "${{ inputs.reusable_evidence_json }}", + SUPPORT_PRS_JSON: "${{ inputs.support_prs_json }}", + }, + ); + const receiptUpload = namedStep( + sourceResolve, + "Upload executable release freeze receipt", + ); + add( + violations, + receiptUpload?.if === "${{ inputs.acceptance_only }}" + && receiptUpload?.uses === "actions/upload-artifact@v7.0.1" + && object(receiptUpload.with).name + === "${{ steps.receipt.outputs.artifact_name }}" + && object(receiptUpload.with).path + === "${{ runner.temp }}/release-freeze-receipt.json" + && object(receiptUpload.with)["if-no-files-found"] === "error" + && object(receiptUpload.with)["retention-days"] === 30 + && object(sourceResolve.outputs).freeze_digest + === "${{ steps.receipt.outputs.digest }}" + && object(sourceResolve.outputs).freeze_artifact_name + === "${{ steps.receipt.outputs.artifact_name }}", + "[freeze_barrier] source acceptance must retain one immutable attempt-qualified Actions receipt", + ); + const broadFreeze = namedStep(sourceResolve, "Require executable release freeze"); + add( + violations, + broadFreeze?.if === "${{ !inputs.acceptance_only }}", + "[freeze_barrier] broad source proof must authenticate the accepted freeze", + ); + requireStepRun( + violations, + "source-proof.yml", + sourceResolve, + "Require executable release freeze", + [ + "release-freeze-barrier.mjs verify-status", + '--commit "$HEAD_SHA"', + '--tree "$tree"', + "--phase frozen_candidate", + '--receipt-digest "$FREEZE_RECEIPT_DIGEST"', + ], + ); + requireStepEnv( + violations, + "source-proof.yml", + sourceResolve, + "Require executable release freeze", + { + FREEZE_RECEIPT_DIGEST: "${{ inputs.freeze_receipt_digest }}", + HEAD_SHA: "${{ steps.resolve.outputs.ref }}", + }, + ); + add( + violations, + !scalarStrings(sourceWorkflow).some(value => value.includes("verify-pending")), + "[freeze_barrier] source proof must never accept a caller-authored pending status", + ); + const hostileJob = requireJob( + violations, + "source-proof.yml", + sourceWorkflow, + acceptance.hostile_job, + ); + add( + violations, + hostileJob.if === "inputs.acceptance_only" + && sameMembers(needs(hostileJob), ["resolve"]) + && hostileJob["runs-on"] === "ubuntu-latest" + && hostileJob["timeout-minutes"] === 5 + && namedStep(hostileJob, acceptance.hostile_step)?.["continue-on-error"] !== true, + "[freeze_barrier] source acceptance must execute the exact blocking hostile mutation job", + ); + requireStepRun( + violations, + "source-proof.yml", + hostileJob, + acceptance.hostile_step, + [ + "node --test", + ".github/scripts/check-workflow-policy.test.mjs", + ".github/scripts/release-freeze-barrier.test.mjs", + ".github/scripts/cargo-build-artifacts.test.mjs", + ".github/scripts/candidate-archive-store.test.mjs", + ], + ); + + const windowsJob = requireJob( + violations, + "source-proof.yml", + sourceWorkflow, + acceptance.windows_job, + ); + add( + violations, + windowsJob.if === "inputs.acceptance_only" + && sameMembers(needs(windowsJob), ["resolve"]) + && sameMembers(list(windowsJob["runs-on"]), list(acceptance.windows_runner)) + && windowsJob["timeout-minutes"] === 5 + && namedStep(windowsJob, acceptance.windows_step)?.shell === "pwsh" + && namedStep(windowsJob, acceptance.windows_step)?.["continue-on-error"] !== true, + "[freeze_barrier] source acceptance must execute the protected blocking Windows native probe", + ); + requireStepRun( + violations, + "source-proof.yml", + windowsJob, + acceptance.windows_step, + [ + "cargo new --quiet --bin", + "cargo build --release --quiet", + "node --test .github/scripts/cargo-build-artifacts.test.mjs", + "left.dev !== right.dev", + "left.ino !== right.ino", + "left.nlink !== 2n", + "right.nlink !== 2n", + "Elapsed.TotalSeconds -ge 90", + "Remove-Item -LiteralPath $probeRoot -Recurse -Force", + ], + ); + + const publisherJob = requireJob( + violations, + "source-proof.yml", + sourceWorkflow, + acceptance.publisher_job, + ); + add( + violations, + sameMembers(needs(publisherJob), [ + "resolve", + acceptance.hostile_job, + acceptance.windows_job, + ]) + && publisherJob["runs-on"] === "ubuntu-latest" + && publisherJob["timeout-minutes"] === 5 + && [ + "always()", + "inputs.acceptance_only", + `needs.${acceptance.hostile_job}.result == 'success'`, + `needs.${acceptance.windows_job}.result == 'success'`, + ].every(fragment => String(publisherJob.if ?? "").includes(fragment)), + "[freeze_barrier] acceptance publisher must depend on both exact successful mutation jobs", + ); + const receiptDownload = namedStep( + publisherJob, + "Download executable release freeze receipt", + ); + add( + violations, + receiptDownload?.uses === "actions/download-artifact@v8.0.1" + && object(receiptDownload.with).name + === "${{ needs.resolve.outputs.freeze_artifact_name }}" + && object(receiptDownload.with).path + === "${{ runner.temp }}/release-freeze-receipt" + && stepIndex(publisherJob, "Download executable release freeze receipt") + < stepIndex(publisherJob, acceptance.publisher_step), + "[freeze_barrier] acceptance publisher must download the exact Actions receipt before publication", + ); + requireStepRun( + violations, + "source-proof.yml", + publisherJob, + acceptance.publisher_step, + [ + "release-freeze-barrier.mjs verify-file", + '--receipt "$RUNNER_TEMP/release-freeze-receipt/release-freeze-receipt.json"', + '--repository "$GITHUB_REPOSITORY"', + '--commit "$HEAD_SHA"', + '--tree "$tree"', + '--run-id "$GITHUB_RUN_ID"', + '--run-attempt "$GITHUB_RUN_ATTEMPT"', + '--phase "$ACCEPTANCE_PHASE"', + 'test "$verified_digest" = "$FREEZE_RECEIPT_DIGEST"', + "repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA", + "-f state=success", + "-f \"context=codestory/release-freeze/$FREEZE_RECEIPT_DIGEST\"", + "-f \"description=tree=$tree\"", + "actions/runs/$GITHUB_RUN_ID", + ], + ); + requireStepEnv( + violations, + "source-proof.yml", + publisherJob, + acceptance.publisher_step, + { + FREEZE_RECEIPT_DIGEST: "${{ needs.resolve.outputs.freeze_digest }}", + HEAD_SHA: "${{ needs.resolve.outputs.ref }}", + ACCEPTANCE_PHASE: "${{ inputs.acceptance_phase }}", + }, + ); + + for (const file of list(freeze.coordinator_only_workflows)) { + const workflow = workflows.get(file); + add( + violations, + trigger(workflow, "workflow_call") !== undefined + && trigger(workflow, "workflow_dispatch") === undefined, + `[freeze_barrier] ${file} must be callable only through an accepted coordinator`, + ); + } + + const coordinator = workflows.get("packaged-platform-pr.yml"); + const route = requireJob(violations, "packaged-platform-pr.yml", coordinator, "route"); + add( + violations, + namedStep(route, "Require executable release freeze")?.if === undefined, + "[freeze_barrier] every packaged proof mode must authenticate the exact candidate head", + ); + requireStepRun( + violations, + "packaged-platform-pr.yml", + route, + "Require executable release freeze", + [ + "release-freeze-barrier.mjs verify-status", + '--commit "$HEAD_SHA"', + 'if [ "$RESOLVED_MODE" = calibration ]; then', + "freeze_phase=calibration_source", + "freeze_phase=frozen_candidate", + '--phase "$freeze_phase"', + '--receipt-digest "$FREEZE_RECEIPT_DIGEST"', + ], + ); + requireStepEnv( + violations, + "packaged-platform-pr.yml", + route, + "Require executable release freeze", + { + RESOLVED_MODE: "${{ steps.resolve.outputs.mode }}", + }, + ); + const packagedSourceProof = namedStep(route, "Require successful exact-head source proof"); + add( + violations, + packagedSourceProof?.if + === "steps.resolve.outputs.mode != 'integration' && steps.resolve.outputs.mode != 'calibration'", + "[freeze_barrier] calibration must precede the sole frozen-candidate source proof", + ); + requireStepRun( + violations, + "packaged-platform-pr.yml", + route, + "Require successful exact-head source proof", + [ + "actions/runs?head_sha=$HEAD_SHA", + '.event == "workflow_dispatch" and .conclusion == "success"', + '.name == "full-source-gate" and .conclusion == "success"', + ], + ); + + const release = workflows.get("release.yml"); + const auto = workflows.get("auto-release.yml"); + add( + violations, + at(release, "concurrency", "cancel-in-progress") === true + && at(auto, "concurrency", "cancel-in-progress") === true, + "[freeze_barrier] release and auto-release must cancel superseded work", + ); + add( + violations, + object(release.permissions).statuses === undefined + && object(at(auto, "jobs", "release", "permissions")).statuses === undefined, + "[freeze_barrier] publication must reuse accepted frozen-candidate proof without an active status", + ); + const preflight = requireJob(violations, "release.yml", release, "preflight"); + requireStepRun( + violations, + "release.yml", + preflight, + "Resolve reusable prior evidence", + [ + 'release_tree="$(git rev-parse "$GITHUB_SHA^{tree}")"', + 'test "$(git rev-parse "$head_sha^{tree}")" = "$release_tree"', + 'git merge-base --is-ancestor "$head_sha" "$GITHUB_SHA"', + 'artifact_name="release-cell-prepublish-source-attempt-$run_attempt"', + ".expired == false", + 'test "$artifact_count" = 1 || continue', + "The release workflow will not start a broad proof", + "source_proof_reused=true", + ], + ); + forbidStepRun( + violations, + "release.yml", + preflight, + "Resolve reusable prior evidence", + [ + "release-freeze-barrier.mjs verify-status", + "freeze_receipt_digest", + ], + ); + const sourceJob = requireJob(violations, "release.yml", release, "source-proof"); + add( + violations, + sourceJob.if === "needs.preflight.outputs.source_proof_reused != 'true'" + && object(preflight.outputs).source_proof_reused + === "${{ steps.reuse.outputs.source_proof_reused }}" + && object(sourceJob.with).freeze_receipt_digest === "", + "[freeze_barrier] release must make the post-calibration source-proof fallback unreachable", + ); + + const lineageSource = fs.readFileSync( + path.join( + repositoryRoot, + ".github", + "scripts", + "packaged_agent_proof", + "calibration_lineage.py", + ), + "utf8", + ); + add( + violations, + lineageSource.includes("frozen_parents == [calibration_source[\"commit\"]]") + && lineageSource.includes("Any later commit revokes acceptance") + && lineageSource.includes("allow_promotion_commit"), + "[freeze_barrier] calibration lineage must require one direct constant-only child with an explicit promotion exception", + ); + return violations; +} + export function releaseWorkflowContractViolations( workflows, graph = loadReleaseClaimGraph(repositoryRoot), @@ -8215,6 +9092,7 @@ export function releaseWorkflowContractViolations( `[proof_identity] ${file} must resolve the current head and compare its exact SHA before executing labeled work`, ); } + violations.push(...releaseFreezeBarrierWorkflowViolations(workflows, graph)); return violations; } @@ -8802,7 +9680,11 @@ function validateReleaseArtifactRerunSafety(workflows, violations) { const upload = object(step.with); const artifactName = String(upload.name ?? ""); const uploadKey = `${file}/${jobId}/${step.name ?? ""}`; - const attemptQualified = artifactName.includes("${{ github.run_attempt }}"); + const attemptQualified = artifactName.includes("${{ github.run_attempt }}") + || ( + uploadKey === "source-proof.yml/resolve/Upload executable release freeze receipt" + && artifactName === "${{ steps.receipt.outputs.artifact_name }}" + ); const expectedStable = replaceableStableIntermediates.get(uploadKey); const stableIntermediateMatches = expectedStable !== undefined && !observedStableIntermediates.has(uploadKey) diff --git a/.github/scripts/check-workflow-policy.test.mjs b/.github/scripts/check-workflow-policy.test.mjs index ab96496d7..aee6de8ff 100644 --- a/.github/scripts/check-workflow-policy.test.mjs +++ b/.github/scripts/check-workflow-policy.test.mjs @@ -3,6 +3,8 @@ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { chmodSync, + linkSync, + lstatSync, mkdirSync, mkdtempSync, readFileSync, @@ -37,6 +39,7 @@ import { releaseEvidenceApprovalViolations, releaseProofCpuSelectorViolations, releaseEvidenceWorkflowRef, + releaseFreezeBarrierWorkflowViolations, releaseWorkflowContractViolations, retrievalGeneralizationSuitePolicyViolations, retrievalFile, @@ -47,6 +50,10 @@ import { validateWorkflows, windowsManifestProofPolicyViolations, } from "./check-workflow-policy.mjs"; +import { + produceQualificationDriverArtifact, + verifyQualificationDriverArtifact, +} from "./qualification-driver-artifact.mjs"; const fullSha = "0123456789abcdef0123456789abcdef01234567"; const proofTopology = "proof5-v1-64015a841a2f69f33f7c9ce284f671ad27b3923a58db865fd4806d86230df6c5"; @@ -225,16 +232,24 @@ function commitCalibrationFixture(repository, message) { }; } -function runCalibrationReleaseCheck(repository, expectedSha) { +function runCalibrationReleaseCheck( + repository, + expectedSha, + { allowPromotionCommit = false } = {}, +) { + const argumentsList = [ + calibrationReleaseChecker, + "--repo", + repository, + "--expected-sha", + expectedSha, + ]; + if (allowPromotionCommit) { + argumentsList.push("--allow-promotion-commit"); + } return spawnSync( "python", - [ - calibrationReleaseChecker, - "--repo", - repository, - "--expected-sha", - expectedSha, - ], + argumentsList, { cwd: root, encoding: "utf8", @@ -873,10 +888,9 @@ test("constant calibration structure rejects qualification, 3x3 sampling, repeat "", ); }, /must upload attempt-scoped non-selecting evidence/u], - ["Linux calibration requires an upstream package run", "linux-vulkan-proof.yml", workflow => { - workflow.on.workflow_dispatch.inputs.package_run_id.required = true; - delete workflow.on.workflow_dispatch.inputs.package_run_id.default; - }, /must not require an upstream package run/u], + ["Linux calibration restores a direct dispatch", "linux-vulkan-proof.yml", workflow => { + workflow.on.workflow_dispatch = { inputs: {} }; + }, /coordinator-only and not directly dispatchable/u], ["Linux calibration downloads an independently built package", "linux-vulkan-proof.yml", workflow => { workflow.jobs["optional-constant-calibration"].steps.splice(5, 0, { name: "Download exact Linux package", @@ -1610,7 +1624,7 @@ test("qualification driver is built once, retained privately, authenticated, and source.replace("sha256(archivePath) !== identity.archive.sha256", "false")], ["helper follows linked path ancestors", source => source.replace("lstatSync(cursor).isSymbolicLink()", "false")], - ["helper accepts hardlinked drivers", source => + ["helper accepts hardlinked retained drivers", source => source.replace("metadata.nlink !== 1", "false")], ["helper accepts extra identity fields", source => source.replace('fail(`${label} keys changed`)', "return")], @@ -1655,6 +1669,82 @@ test("qualification driver is built once, retained privately, authenticated, and } }); +test("qualification driver retention breaks a Cargo source hardlink and rejects retained hardlinks", () => { + const directory = mkdtempSync( + path.join(os.tmpdir(), "codestory-qualification-driver-"), + ); + try { + const targetDirectory = path.join(directory, "target"); + const releaseDirectory = path.join( + targetDirectory, + "x86_64-pc-windows-msvc", + "release", + ); + const depsDirectory = path.join(releaseDirectory, "deps"); + mkdirSync(depsDirectory, { recursive: true }); + const originalDriver = path.join( + depsDirectory, + "codestory_embedding_qualification-hash.exe", + ); + const cargoDriver = path.join( + releaseDirectory, + "codestory_embedding_qualification.exe", + ); + writeFileSync(originalDriver, "qualification-driver-v1"); + chmodSync(originalDriver, 0o755); + linkSync(originalDriver, cargoDriver); + assert.equal(lstatSync(cargoDriver).nlink, 2); + + const archive = path.join( + directory, + "codestory-cli-v0.16.3-windows-x64.zip", + ); + writeFileSync(archive, "candidate-archive"); + const artifactDirectory = path.join(directory, "artifact"); + const produced = produceQualificationDriverArtifact({ + archive, + assetTarget: "windows-x64", + outDir: artifactDirectory, + sourceSha: "a".repeat(40), + sourceTree: "b".repeat(40), + targetDir: targetDirectory, + trustedRoot: directory, + version: "0.16.3", + }); + assert.equal(lstatSync(produced.driver).nlink, 1); + assert.equal(readFileSync(produced.driver, "utf8"), "qualification-driver-v1"); + + writeFileSync(originalDriver, "qualification-driver-v2"); + assert.equal(readFileSync(produced.driver, "utf8"), "qualification-driver-v1"); + const verified = verifyQualificationDriverArtifact({ + archive, + artifactDir: artifactDirectory, + assetTarget: "windows-x64", + sourceSha: "a".repeat(40), + sourceTree: "b".repeat(40), + trustedRoot: directory, + version: "0.16.3", + }); + assert.equal(verified.identity.driver.sha256, produced.identity.driver.sha256); + + linkSync(produced.driver, path.join(directory, "retained-driver-alias.exe")); + assert.throws( + () => verifyQualificationDriverArtifact({ + archive, + artifactDir: artifactDirectory, + assetTarget: "windows-x64", + sourceSha: "a".repeat(40), + sourceTree: "b".repeat(40), + trustedRoot: directory, + version: "0.16.3", + }), + /qualification driver artifact must be a regular, non-symlink, singly linked file/u, + ); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + test("Windows packages one release graph into exact public and private artifacts", async (t) => { assert.deepEqual(validateWorkflows(loadWorkflows()), []); const file = "packaged-platform-proof.yml"; @@ -2273,7 +2363,7 @@ test("release-head calibration lineage rejects identities and source shapes arou assert.match(result.stderr, /release checkout does not match the expected release source/u); }); - await t.test("a tree-preserving promotion commit stays bound", () => { + await t.test("a later commit revokes candidate acceptance unless it is the explicit promotion", () => { calibrationGit( repository, "commit", @@ -2284,8 +2374,19 @@ test("release-head calibration lineage rejects identities and source shapes arou "promote frozen tree", ); const promoted = calibrationGit(repository, "rev-parse", "HEAD"); - const result = runCalibrationReleaseCheck(repository, promoted); - assert.equal(result.status, 0, result.stderr || result.stdout); + const rejected = runCalibrationReleaseCheck(repository, promoted); + assert.notEqual(rejected.status, 0); + assert.match(rejected.stderr, /later commit revokes acceptance/u); + const promotedResult = runCalibrationReleaseCheck( + repository, + promoted, + { allowPromotionCommit: true }, + ); + assert.equal( + promotedResult.status, + 0, + promotedResult.stderr || promotedResult.stdout, + ); calibrationGit(repository, "reset", "--hard", frozen.commit); }); @@ -2373,7 +2474,7 @@ test("release policy keeps the release-head lineage check mandatory and exact", ["interpreter uses PATH lookup", workflows => { const step = draftStep(workflows.get("release.yml").jobs.preflight, stepName); step.run = step.run.replace("/usr/bin/python3 -E -s", "python"); - }, /must use the pinned interpreter on the exact release checkout/u], + }, /step Verify release-head calibration lineage must run \/usr\/bin\/python3 -E -s/u], ["lineage shell uses PATH lookup", workflows => { const step = draftStep(workflows.get("release.yml").jobs.preflight, stepName); step.shell = "bash -e {0}"; @@ -2399,7 +2500,7 @@ test("release policy keeps the release-head lineage check mandatory and exact", ["wrong release SHA", workflows => { const step = draftStep(workflows.get("release.yml").jobs.preflight, stepName); step.run = step.run.replace("$GITHUB_SHA", "$EXPECTED_HEAD_SHA"); - }, /must use the pinned interpreter on the exact release checkout/u], + }, /step Verify release-head calibration lineage must run --expected-sha/u], ]; assert.deepEqual(validateWorkflows(loadWorkflows()), []); for (const [name, mutate, expected] of cases) { @@ -2559,12 +2660,12 @@ test("exact proof policy rejects trigger and identity downgrades", async (t) => const packagedResolver = workflow => draftStep(workflow.jobs.route, "Resolve trusted exact head"); const mutations = [ - ["source synchronize trigger", sourceFile, workflow => { - workflow.on.pull_request.types.push("synchronize"); - }, /trigger must be label-only/u], - ["platform synchronize trigger", packagedCoordinatorFile, workflow => { - workflow.on.pull_request.types.push("synchronize"); - }, /trigger must be label-only/u], + ["source PR label trigger returns", sourceFile, workflow => { + workflow.on.pull_request = { types: ["labeled"] }; + }, /support PR labels must not trigger broad source proof/u], + ["platform PR label trigger returns", packagedCoordinatorFile, workflow => { + workflow.on.pull_request = { types: ["labeled"] }; + }, /support PR labels must not trigger package or hardware proof/u], ["source PR-number-only concurrency", sourceFile, workflow => { workflow.concurrency.group = "source-proof-${{ inputs.pr_number || github.event.pull_request.number }}"; }, /concurrency must bind the Actions SHA/u], @@ -2600,10 +2701,10 @@ test("exact proof policy rejects trigger and identity downgrades", async (t) => sourceResolver(workflow).run = sourceResolver(workflow).run .replace("set -euo pipefail\n", "set -euo pipefail\n\n"); }, /exact normalized trusted resolver script contract/u], - ["source labeled job disabled", sourceFile, workflow => { + ["source resolve becomes conditional", sourceFile, workflow => { workflow.jobs.resolve.if = "false && (github.event.action == 'labeled' && github.event.label.name == 'review-accepted')"; - }, /only review-accepted labeled PR runs/u], + }, /execute only explicit dispatch and reusable calls/u], ["source manual ref equality", sourceFile, workflow => { sourceResolver(workflow).run = sourceResolver(workflow).run .replace('test "$GITHUB_REF" = "refs\/heads\/$head_ref"', 'test -n "$GITHUB_REF"'); @@ -2640,10 +2741,10 @@ test("exact proof policy rejects trigger and identity downgrades", async (t) => 'if [ -n "$INPUT_SOURCE_RUN_ID" ] \\\n\n ||', ); }, /exact normalized trusted resolver script contract/u], - ["platform labeled job disabled", packagedCoordinatorFile, workflow => { + ["platform route becomes conditional", packagedCoordinatorFile, workflow => { workflow.jobs.route.if = "false && (github.event.action == 'labeled' && github.event.label.name == 'platform-proof')"; - }, /only platform-proof labeled PR runs/u], + }, /execute only explicit dispatches/u], ["integration live dev SHA equality", packagedCoordinatorFile, workflow => { packagedResolver(workflow).run = packagedResolver(workflow).run .replace('test "$GITHUB_SHA" = "$dev_head"', 'test -n "$GITHUB_SHA"'); @@ -2668,10 +2769,9 @@ test("exact proof policy rejects trigger and identity downgrades", async (t) => ["protected Linux candidate proof disabled", packagedCoordinatorFile, workflow => { workflow.jobs["linux-vulkan-proof"].with.candidate_installed_proof = false; }, /Linux proof must close Vulkan and candidate-installed claims/u], - ["manual Linux candidate trusts a non-producer", linuxVulkanFile, workflow => { - workflow.on.workflow_dispatch.inputs.candidate_producer_workflow_path.default - = ".github/workflows/release.yml"; - }, /manual candidate proof must trust the package-producing workflow/u], + ["Linux direct dispatch returns", linuxVulkanFile, workflow => { + workflow.on.workflow_dispatch = { inputs: {} }; + }, /coordinator-only and not directly dispatchable/u], ["closeout skips protected Linux", packagedCoordinatorFile, workflow => { workflow.jobs.closeout.needs = workflow.jobs.closeout.needs .filter(name => name !== "linux-vulkan-proof"); @@ -2824,8 +2924,8 @@ test("source proof reuse accepts only whole successful workflow runs", async (t) "Reuse a completed gate for this exact head", ); step.run = step.run.replace( - '(.event == "pull_request" or .event == "workflow_dispatch") and .conclusion == "success"', - '(.event == "pull_request" or .event == "workflow_dispatch")', + '.event == "workflow_dispatch" and .conclusion == "success"', + '.event == "workflow_dispatch"', ); }, /source-proof\.yml step Reuse a completed gate.*workflow_dispatch.*conclusion/u], ["release preflight reuse", workflows => { @@ -2834,8 +2934,8 @@ test("source proof reuse accepts only whole successful workflow runs", async (t) "Resolve reusable prior evidence", ); step.run = step.run.replace( - ".head_repository.full_name == $repo and .conclusion == \"success\"", - ".head_repository.full_name == $repo", + '.event == "workflow_dispatch" and .conclusion == "success"', + '.event == "workflow_dispatch"', ); }, /release\.yml step Resolve reusable prior evidence.*conclusion/u], ["packaged prior proof lookup", workflows => { @@ -2844,8 +2944,8 @@ test("source proof reuse accepts only whole successful workflow runs", async (t) "Require successful exact-head source proof", ); step.run = step.run.replace( - '(.event == "pull_request" or .event == "workflow_dispatch") and .conclusion == "success"', - '(.event == "pull_request" or .event == "workflow_dispatch")', + '.event == "workflow_dispatch" and .conclusion == "success"', + '.event == "workflow_dispatch"', ); }, /packaged-platform-pr\.yml step Require successful exact-head source proof.*conclusion/u], ]; @@ -2859,6 +2959,657 @@ test("source proof reuse accepts only whole successful workflow runs", async (t) } }); +test("release freeze barrier rejects every broad-proof bypass", async (t) => { + assert.deepEqual(validateWorkflows(loadWorkflows()), []); + const cases = [ + ["source label trigger", workflows => { + workflows.get("source-proof.yml").on.pull_request = { types: ["labeled"] }; + }, /support PR event/u], + ["superseded PR heads stop invalidating proof", workflows => { + workflows.get("release-freeze-invalidation.yml").on.pull_request.types = ["opened"]; + }, /must run automatically when a candidate head is superseded/u], + ["dev head changes stop invalidating proof", workflows => { + delete workflows.get("release-freeze-invalidation.yml").on.push; + }, /must run automatically when a candidate head is superseded/u], + ["invalidation stops checking the prior freeze", workflows => { + const step = draftStep( + workflows.get("release-freeze-invalidation.yml").jobs.invalidate, + "Invalidate a superseded release freeze", + ); + step.run = step.run.replace( + "commits/$BEFORE_SHA/statuses?per_page=100", + "commits/$AFTER_SHA/statuses?per_page=100", + ); + }, /Invalidate a superseded release freeze/u], + ["invalidation stops cancelling auto-release", workflows => { + const step = draftStep( + workflows.get("release-freeze-invalidation.yml").jobs.invalidate, + "Invalidate a superseded release freeze", + ); + step.run = step.run.replace( + '--broad-workflow "Auto Release"', + "", + ); + }, /every dev push must cancel obsolete proof/u], + ["dev push no longer cancels before status lookup", workflows => { + const step = draftStep( + workflows.get("release-freeze-invalidation.yml").jobs.invalidate, + "Invalidate a superseded release freeze", + ); + step.run = step.run.replace( + "release-freeze-barrier.mjs invalidate-superseded", + "release-freeze-barrier.mjs cancelled-too-late", + ); + }, /every dev push must cancel obsolete proof/u], + ["invalidation loses event identity", workflows => { + const step = draftStep( + workflows.get("release-freeze-invalidation.yml").jobs.invalidate, + "Invalidate a superseded release freeze", + ); + delete step.env.EVENT_NAME; + }, /must bind EVENT_NAME/u], + ["invalidation accepts a pending freeze", workflows => { + const step = draftStep( + workflows.get("release-freeze-invalidation.yml").jobs.invalidate, + "Invalidate a superseded release freeze", + ); + step.run = step.run.replace( + '.state == "success"', + '(.state == "pending" or .state == "success")', + ); + }, /Invalidate a superseded release freeze/u], + ["invalidation cannot revoke the old status", workflows => { + workflows.get("release-freeze-invalidation.yml").permissions.statuses = "read"; + }, /must run automatically when a candidate head is superseded/u], + ["invalidation stops publishing the revocation", workflows => { + const step = draftStep( + workflows.get("release-freeze-invalidation.yml").jobs.invalidate, + "Invalidate a superseded release freeze", + ); + step.run = step.run.replace("-f state=error", "-f state=success"); + }, /Invalidate a superseded release freeze/u], + ["platform label trigger", workflows => { + workflows.get("packaged-platform-pr.yml").on.pull_request = { types: ["labeled"] }; + }, /support PR event/u], + ...[ + "macos-metal-proof.yml", + "windows-vulkan-proof.yml", + "linux-vulkan-proof.yml", + ].map(file => [ + `${file} direct dispatch`, + workflows => { + workflows.get(file).on.workflow_dispatch = { inputs: {} }; + }, + /callable only through an accepted coordinator/u, + ]), + ["source acceptance requires a caller receipt", workflows => { + workflows.get("source-proof.yml").on.workflow_dispatch + .inputs.freeze_receipt_digest.required = true; + }, /acceptance must mint its own receipt digest/u], + ["source acceptance becomes the default", workflows => { + workflows.get("source-proof.yml").on.workflow_dispatch + .inputs.acceptance_only.default = true; + }, /separate acceptance from broad proof/u], + ["source acceptance loses its closed phase selector", workflows => { + workflows.get("source-proof.yml").on.workflow_dispatch + .inputs.acceptance_phase.options.push("pre_calibration_source_proof"); + }, /separate acceptance from broad proof/u], + ["acceptance adds an Ubuntu workspace test job", workflows => { + workflows.get("source-proof.yml").jobs["acceptance-ubuntu-workspace"] = { + if: "inputs.acceptance_only", + "runs-on": "ubuntu-latest", + steps: [{ + run: "cargo test --workspace --locked", + }], + }; + }, /closed source and acceptance job contract/u], + ["acceptance adds a protected Windows release workspace test job", workflows => { + workflows.get("source-proof.yml").jobs["acceptance-windows-workspace"] = { + if: "inputs.acceptance_only", + "runs-on": ["self-hosted", "Windows", "X64", "codestory-vulkan"], + steps: [{ + shell: "pwsh", + run: "cargo test --release --workspace --locked", + }], + }; + }, /closed source and acceptance job contract/u], + ["acceptance hides a workspace test in the hostile job", workflows => { + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"].steps.push({ + name: "Unexpected broad source proof", + run: "cargo test --workspace --locked", + }); + }, /canonical acceptance job manifest/u], + ["acceptance hides an Ubuntu workspace test behind a variable", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"], + "Execute exact-head hostile mutation matrix", + ); + step.run += '\nbroad_scope=--workspace\ncargo test "$broad_scope" --locked\n'; + }, /canonical acceptance job manifest/u], + ["acceptance hides a Windows workspace test behind a variable", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-windows-native-probe"], + "Run exact-head Windows native probe", + ); + step.run += '\n$scope = "--workspace"\ncargo test --release $scope --locked\n'; + }, /canonical acceptance job manifest/u], + ["acceptance hides a workspace test behind an alias", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"], + "Execute exact-head hostile mutation matrix", + ); + step.run += "\nalias broad='cargo test --workspace --locked'\nbroad\n"; + }, /canonical acceptance job manifest/u], + ["acceptance hides a workspace test behind a shell function", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"], + "Execute exact-head hostile mutation matrix", + ); + step.run += "\nrun_broad() { cargo test --workspace --locked; }\nrun_broad\n"; + }, /canonical acceptance job manifest/u], + ["acceptance delegates to an unreviewed script", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"], + "Execute exact-head hostile mutation matrix", + ); + step.run += "\nbash scripts/run-broad-source.sh\n"; + }, /canonical acceptance job manifest/u], + ["acceptance chains a workspace test after an approved command", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"], + "Execute exact-head hostile mutation matrix", + ); + step.run += "\ntrue && cargo test --workspace --locked\n"; + }, /canonical acceptance job manifest/u], + ["acceptance substitutes an alternate shell", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"], + "Execute exact-head hostile mutation matrix", + ); + step.shell = "python"; + }, /canonical acceptance job manifest/u], + ["source acceptance cannot publish status", workflows => { + delete workflows.get("source-proof.yml").permissions.statuses; + }, /acceptance must publish an exact-head commit status/u], + ["Actions receipt generation is removed", workflows => { + const job = workflows.get("source-proof.yml").jobs.resolve; + job.steps = job.steps.filter(({ name }) => + name !== "Record executable release freeze"); + }, /Record executable release freeze/u], + ["Actions receipt generation loses live release PR authentication", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs.resolve, + "Record executable release freeze", + ); + step.run = step.run.replace('--release-pr "$PR_NUMBER"', ""); + }, /Record executable release freeze.*--release-pr/u], + ["Actions receipt generation loses merged support PR authentication", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs.resolve, + "Record executable release freeze", + ); + step.run = step.run.replace('--support-prs-json "$SUPPORT_PRS_JSON"', ""); + }, /Record executable release freeze.*--support-prs-json/u], + ["Actions receipt generation loses its candidate phase", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs.resolve, + "Record executable release freeze", + ); + step.run = step.run.replace('--phase "$ACCEPTANCE_PHASE"', ""); + }, /Record executable release freeze.*--phase/u], + ["Actions receipt generation loses support PR history", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs.resolve, + "Checkout accepted source head", + ); + delete step.with["fetch-depth"]; + }, /complete history for support PR ancestry/u], + ["Actions receipt artifact is removed", workflows => { + const job = workflows.get("source-proof.yml").jobs.resolve; + job.steps = job.steps.filter(({ name }) => + name !== "Upload executable release freeze receipt"); + }, /immutable attempt-qualified Actions receipt/u], + ["Actions receipt artifact is substituted", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs.resolve, + "Upload executable release freeze receipt", + ); + step.with.name = "release-freeze-receipt"; + }, /immutable attempt-qualified Actions receipt/u], + ["source restores conditional cell emission", workflows => { + workflows.get("source-proof.yml").on.workflow_dispatch + .inputs.emit_release_cells = { + required: false, + default: false, + type: "boolean", + }; + draftStep( + workflows.get("source-proof.yml").jobs["full-source-gate"], + "Upload authenticated source release cell", + ).if = "success() && inputs.emit_release_cells"; + }, /source release cell must be an unconditional success-only retained artifact/u], + ["hostile mutation job is removed", workflows => { + delete workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"]; + }, /freeze-hostile-mutations/u], + ["hostile mutation matrix is weakened", workflows => { + draftStep( + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"], + "Execute exact-head hostile mutation matrix", + ).run = "node --test .github/scripts/release-freeze-barrier.test.mjs"; + }, /Execute exact-head hostile mutation matrix/u], + ["hostile mutations become advisory", workflows => { + draftStep( + workflows.get("source-proof.yml").jobs["freeze-hostile-mutations"], + "Execute exact-head hostile mutation matrix", + )["continue-on-error"] = true; + }, /exact blocking hostile mutation job/u], + ["Windows probe leaves the protected runner", workflows => { + workflows.get("source-proof.yml").jobs["freeze-windows-native-probe"]["runs-on"] + = ["self-hosted", "Windows", "X64"]; + }, /protected blocking Windows native probe/u], + ["Windows probe restores a full build", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-windows-native-probe"], + "Run exact-head Windows native probe", + ); + step.run = step.run.replace( + "cargo build --release --quiet", + "cargo build --workspace --release", + ); + }, /Run exact-head Windows native probe/u], + ["Windows probe allows 90 seconds", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-windows-native-probe"], + "Run exact-head Windows native probe", + ); + step.run = step.run.replace("Elapsed.TotalSeconds -ge 90", "Elapsed.TotalSeconds -gt 90"); + }, /Run exact-head Windows native probe/u], + ["acceptance publisher stops waiting for Windows", workflows => { + workflows.get("source-proof.yml").jobs["freeze-acceptance"].needs + = ["resolve", "freeze-hostile-mutations"]; + }, /publisher must depend on both exact successful mutation jobs/u], + ["acceptance publisher stops downloading the Actions receipt", workflows => { + const job = workflows.get("source-proof.yml").jobs["freeze-acceptance"]; + job.steps = job.steps.filter(({ name }) => + name !== "Download executable release freeze receipt"); + }, /download the exact Actions receipt before publication/u], + ["acceptance publisher trusts the caller digest", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-acceptance"], + "Publish executable release freeze", + ); + step.env.FREEZE_RECEIPT_DIGEST = "${{ inputs.freeze_receipt_digest }}"; + }, /FREEZE_RECEIPT_DIGEST/u], + ["acceptance publisher skips receipt verification", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-acceptance"], + "Publish executable release freeze", + ); + step.run = step.run.replace( + "release-freeze-barrier.mjs verify-file", + "printf '%s' \"$FREEZE_RECEIPT_DIGEST\"", + ); + }, /Publish executable release freeze.*verify-file/u], + ["source acceptance restores pending status trust", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs.resolve, + "Require executable release freeze", + ); + step.run = step.run.replace("verify-status", "verify-pending"); + }, /caller-authored pending status/u], + ["broad source proof accepts a calibration-source receipt", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs.resolve, + "Require executable release freeze", + ); + step.run = step.run.replace( + "--phase frozen_candidate", + "--phase calibration_source", + ); + }, /Require executable release freeze.*frozen_candidate/u], + ["acceptance publisher loses Actions provenance", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs["freeze-acceptance"], + "Publish executable release freeze", + ); + step.run = step.run.replace( + "actions/runs/$GITHUB_RUN_ID", + "pull/$GITHUB_RUN_ID", + ); + }, /Publish executable release freeze/u], + ["packaged proof makes the exact-head receipt optional", workflows => { + workflows.get("packaged-platform-pr.yml").on.workflow_dispatch + .inputs.freeze_receipt_digest.required = false; + workflows.get("packaged-platform-pr.yml").on.workflow_dispatch + .inputs.freeze_receipt_digest.default = ""; + }, /packaged proof must require an exact-head freeze digest/u], + ["platform cannot read freeze status", workflows => { + delete workflows.get("packaged-platform-pr.yml").permissions.statuses; + }, /must authenticate the exact-head freeze status/u], + ["qualification bypasses its exact-head freeze status", workflows => { + draftStep( + workflows.get("packaged-platform-pr.yml").jobs.route, + "Require executable release freeze", + ).if = "steps.resolve.outputs.mode != 'qualification'"; + }, /every packaged proof mode must authenticate the exact candidate head/u], + ["calibration regains a pre-freeze source proof", workflows => { + draftStep( + workflows.get("packaged-platform-pr.yml").jobs.route, + "Require successful exact-head source proof", + ).if = "steps.resolve.outputs.mode != 'integration'"; + }, /calibration must precede the sole frozen-candidate source proof/u], + ["qualification loses the frozen-head source proof", workflows => { + draftStep( + workflows.get("packaged-platform-pr.yml").jobs.route, + "Require successful exact-head source proof", + ).if + = "steps.resolve.outputs.mode != 'integration' && steps.resolve.outputs.mode != 'calibration' && steps.resolve.outputs.mode != 'qualification'"; + }, /calibration must precede the sole frozen-candidate source proof/u], + ["release searches the calibration source instead of the frozen tree", workflows => { + const step = draftStep( + workflows.get("release.yml").jobs.preflight, + "Resolve reusable prior evidence", + ); + step.run = step.run.replace( + 'release_tree="$(git rev-parse "$GITHUB_SHA^{tree}")"', + 'release_tree="$(git rev-parse "$SOURCE_SHA^{tree}")"', + ); + }, /Resolve reusable prior evidence.*release_tree/u], + ["release restores post-calibration fallback", workflows => { + workflows.get("release.yml").jobs["source-proof"].if = "always()"; + }, /post-calibration source-proof fallback unreachable/u], + ["source reuse accepts an expired cell", workflows => { + const step = draftStep( + workflows.get("source-proof.yml").jobs.resolve, + "Reuse a completed gate for this exact head", + ); + step.run = step.run.replace(".expired == false", "true"); + }, /Reuse a completed gate.*expired/u], + ["release accepts an expired source cell", workflows => { + const step = draftStep( + workflows.get("release.yml").jobs.preflight, + "Resolve reusable prior evidence", + ); + step.run = step.run.replace(".expired == false", "true"); + }, /Resolve reusable prior evidence.*expired/u], + ["qualification trusts a bare success status", workflows => { + const step = draftStep( + workflows.get("packaged-platform-pr.yml").jobs.route, + "Require executable release freeze", + ); + step.run = step.run.replace( + "release-freeze-barrier.mjs verify-status", + "gh api repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/status", + ); + }, /Require executable release freeze.*verify-status/u], + ["packaged calibration and qualification share one receipt phase", workflows => { + const step = draftStep( + workflows.get("packaged-platform-pr.yml").jobs.route, + "Require executable release freeze", + ); + step.run = step.run.replace( + 'if [ "$RESOLVED_MODE" = calibration ]; then', + "if false; then", + ); + }, /Require executable release freeze.*RESOLVED_MODE/u], + ["release restores active freeze status authentication", workflows => { + const step = draftStep( + workflows.get("release.yml").jobs.preflight, + "Resolve reusable prior evidence", + ); + step.run += "\nnode .github/scripts/release-freeze-barrier.mjs verify-status\n"; + }, /Resolve reusable prior evidence must not run release-freeze-barrier\.mjs verify-status/u], + ["release placeholder propagates a post-calibration receipt", workflows => { + workflows.get("release.yml").jobs["source-proof"].with.freeze_receipt_digest + = "${{ inputs.freeze_receipt_digest }}"; + }, /unreachable source fallback must fail closed without a post-calibration freeze/u], + ["release stops cancelling superseded work", workflows => { + workflows.get("release.yml").concurrency["cancel-in-progress"] = false; + }, /release and auto-release must cancel superseded work/u], + ["automatic release restores freeze status authority", workflows => { + workflows.get("auto-release.yml").jobs.release.permissions.statuses = "read"; + }, /publication must reuse accepted frozen-candidate proof without an active status/u], + ["manual release restores freeze status authority", workflows => { + workflows.get("release.yml").permissions.statuses = "read"; + }, /publication must reuse accepted frozen-candidate proof without an active status/u], + ["auto-release stops cancelling superseded work", workflows => { + workflows.get("auto-release.yml").concurrency["cancel-in-progress"] = false; + }, /release and auto-release must cancel superseded work/u], + ["source stale-run sweep is removed", workflows => { + const job = workflows.get("source-proof.yml").jobs.resolve; + job.steps = job.steps.filter(({ name }) => name !== "Cancel superseded proof runs"); + }, /Cancel superseded proof runs/u], + ["platform stale-run sweep is removed", workflows => { + const job = workflows.get("packaged-platform-pr.yml").jobs.route; + job.steps = job.steps.filter(({ name }) => name !== "Cancel superseded proof runs"); + }, /Cancel superseded proof runs/u], + ["acceptance-only mode restores the full Windows source lane", workflows => { + workflows.get("source-proof.yml").jobs["windows-native-contracts"].if + = "needs.resolve.outputs.reuse != 'true'"; + }, /Windows native source contracts must run in parallel on the resolved exact head/u], + ]; + for (const [name, mutate, expected] of cases) { + await t.test(name, () => { + const workflows = loadWorkflows(); + mutate(workflows); + assert.match(validateWorkflows(workflows).join("\n"), expected); + }); + } +}); + +test("release freeze policy pins live PR base and support ancestry revalidation", async (t) => { + const source = readFileSync( + path.join(root, ".github", "scripts", "release-freeze-barrier.mjs"), + "utf8", + ); + const cases = [ + ["release PR lookup stops using live REST state", value => + value.replace( + 'gh(["api", `repos/${repository}/pulls/${number}`])', + "JSON.parse('{}')", + )], + ["release base lookup stops using the live integration ref", value => + value.replace( + "`repos/${repository}/git/ref/heads/dev/codestory-next`", + "`repos/${repository}/git/commits/${pr.base.sha}`", + )], + ["release PR head stops proving it contains the current dev base", value => + value.replace( + "`repos/${repository}/compare/${liveBaseCommit}...${commit}`", + "`repos/${repository}/commits/${commit}`", + )], + ["verification stops detecting a base advance", value => + value.replace( + "currentReleasePr.base_commit !== receipt?.release_pr?.base_commit", + "false", + )], + ["support PR ancestry becomes advisory", value => + value.replace( + 'git(["merge-base", "--is-ancestor", mergeCommit, commit]', + 'git(["rev-parse", commit]', + )], + ]; + for (const [name, mutate] of cases) { + await t.test(name, () => { + const violations = releaseFreezeBarrierWorkflowViolations( + loadWorkflows(), + loadReleaseClaimGraph(root), + mutate(source), + ); + assert.match( + violations.join("\n"), + /recheck the live release PR base and integrated support PR ancestry/u, + ); + }); + } + + await t.test("active workflow discovery becomes bounded", () => { + const bounded = source.replace( + '"api",\n "--paginate",\n "--slurp",', + '"run",\n "list",\n "--limit",', + ); + const violations = releaseFreezeBarrierWorkflowViolations( + loadWorkflows(), + loadReleaseClaimGraph(root), + bounded, + ); + assert.match( + violations.join("\n"), + /obsolete-run discovery must paginate every active Actions state/u, + ); + }); +}); + +test("release freeze policy authenticates the complete acceptance job manifest", async (t) => { + const barrierSource = readFileSync( + path.join(root, ".github", "scripts", "release-freeze-barrier.mjs"), + "utf8", + ); + const manifestPath = path.join( + root, + ".github", + "scripts", + "release-freeze-acceptance-jobs.json", + ); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + const cases = [ + ["manifest substitutes the workflow execution context", value => { + value.workflow_context_sha256 = "0".repeat(64); + }, /workflow execution context must match the canonical acceptance manifest/u], + ["manifest substitutes an approved job body", value => { + value.jobs["freeze-hostile-mutations"] = "0".repeat(64); + }, /freeze-hostile-mutations must match the canonical acceptance job manifest/u], + ["manifest admits an extra executable job", value => { + value.jobs["acceptance-extra"] = "0".repeat(64); + }, /must pin exactly the executable acceptance jobs/u], + ]; + + for (const [name, mutate, expected] of cases) { + await t.test(name, () => { + const changedManifest = structuredClone(manifest); + mutate(changedManifest); + const changedSource = `${JSON.stringify(changedManifest, null, 2)}\n`; + const graph = structuredClone(loadReleaseClaimGraph(root)); + graph.workflow_policy.release_freeze_barrier.acceptance.job_manifest_sha256 + = createHash("sha256").update(changedSource).digest("hex"); + const violations = releaseFreezeBarrierWorkflowViolations( + loadWorkflows(), + graph, + barrierSource, + changedSource, + ); + assert.match(violations.join("\n"), expected); + }); + } + + await t.test("claim graph substitutes the manifest digest", () => { + const graph = structuredClone(loadReleaseClaimGraph(root)); + graph.workflow_policy.release_freeze_barrier.acceptance.job_manifest_sha256 + = "0".repeat(64); + const violations = releaseFreezeBarrierWorkflowViolations( + loadWorkflows(), + graph, + barrierSource, + readFileSync(manifestPath, "utf8"), + ); + assert.match( + violations.join("\n"), + /release claim graph must pin the executable exact-head freeze contract/u, + ); + }); + + const workflowContextCases = [ + ["repository BASH_ENV preload", workflow => { + workflow.env = { + ...workflow.env, + BASH_ENV: "${{ github.workspace }}/scripts/run-broad-source.sh", + }; + }], + ["repository NODE_OPTIONS preload", workflow => { + workflow.env = { + ...workflow.env, + NODE_OPTIONS: "--require ${{ github.workspace }}/scripts/run-broad-source.js", + }; + }], + ["repository shell wrapper", workflow => { + workflow.defaults = { + run: { + shell: "bash scripts/run-broad-source.sh {0}", + }, + }; + }], + ["workflow trigger context", workflow => { + workflow.on.workflow_dispatch.inputs.acceptance_only.default = true; + }], + ["workflow token permissions", workflow => { + workflow.permissions.contents = "write"; + }], + ["workflow cancellation context", workflow => { + workflow.concurrency.group = "unscoped-acceptance"; + }], + ["workflow display identity", workflow => { + workflow.name = "Unreviewed acceptance wrapper"; + }], + ["new workflow-level field", workflow => { + workflow["run-name"] = "unreviewed-${{ github.run_id }}"; + }], + ]; + + for (const [name, mutate] of workflowContextCases) { + await t.test(`workflow context rejects ${name}`, () => { + const workflows = loadWorkflows(); + mutate(workflows.get("source-proof.yml")); + const violations = releaseFreezeBarrierWorkflowViolations( + workflows, + loadReleaseClaimGraph(root), + barrierSource, + readFileSync(manifestPath, "utf8"), + ); + assert.match( + violations.join("\n"), + /source-proof\.yml workflow execution context must match the canonical acceptance manifest/u, + ); + }); + } +}); + +test("calibration precedes the sole frozen-candidate source proof", async (t) => { + assert.deepEqual(validateWorkflows(loadWorkflows()), []); + const coordinatorFile = "packaged-platform-pr.yml"; + const mutations = [ + ["calibration regains a pre-freeze source proof", workflow => { + draftStep( + workflow.jobs.route, + "Require successful exact-head source proof", + ).if = "steps.resolve.outputs.mode != 'integration'"; + }], + ["qualification loses the frozen-head source proof", workflow => { + draftStep( + workflow.jobs.route, + "Require successful exact-head source proof", + ).if + = "steps.resolve.outputs.mode != 'integration' && steps.resolve.outputs.mode != 'calibration' && steps.resolve.outputs.mode != 'qualification'"; + }], + ["every mode loses the exact-head source proof", workflow => { + draftStep( + workflow.jobs.route, + "Require successful exact-head source proof", + ).if = "false"; + }], + ]; + + for (const [name, mutate] of mutations) { + await t.test(name, () => { + const workflows = loadWorkflows(); + mutate(workflows.get(coordinatorFile)); + assert.match( + validateWorkflows(workflows).join("\n"), + /calibration must precede the sole frozen-candidate source proof/u, + ); + }); + } +}); + test("Windows package proof retains the readable native sccache executable", () => { const directory = mkdtempSync(path.join(os.tmpdir(), "codestory-windows-sccache-")); try { @@ -3497,17 +4248,14 @@ test("standard release paths reject calibration plumbing", async (t) => { required: true, type: "string", }; - }, /release\.yml standard release path must not reference calibration/u], + }, /release\.yml must not accept calibration bundle inputs/u], ["release forwards calibration to package proof", workflows => { workflows.get("release.yml").jobs["packaged-proof"].with.calibration_bundle_run_id = "${{ inputs.calibration_bundle_run_id }}"; - }, /release\.yml standard release path must not reference calibration/u], - ["release hides calibration plumbing in a same-named decoy step", workflows => { - workflows.get("release.yml").jobs["workflow-policy"].steps.push({ - name: "Verify release-head calibration lineage", - run: "echo calibration_bundle_artifact", - }); - }, /release\.yml standard release path must not reference calibration/u], + }, /release\.yml packaged proof must not receive calibration_bundle_run_id/u], + ["release restores a second source proof fallback", workflows => { + workflows.get("release.yml").jobs["source-proof"].if = "always()"; + }, /source proof may be skipped only when preflight resolved reusable evidence/u], ["post-publish proof receives calibration", workflows => { const step = draftStep( workflows.get("post-publish-release-smoke.yml").jobs.smoke, @@ -4852,7 +5600,12 @@ test("release policy rejects manifest producer, trusted-map, and publication byp uses: "./.github/workflows/release.yml", }; }], - ["source emission", workflows => { delete workflows.get("release.yml").jobs["source-proof"].with.emit_release_cells; }], + ["source emission", workflows => { + draftStep( + workflows.get("source-proof.yml").jobs["full-source-gate"], + "Upload authenticated source release cell", + ).if = "success() && inputs.emit_release_cells"; + }], ["full rerun preflight guard", workflows => { workflows.get("release.yml").jobs.preflight.steps = workflows .get("release.yml").jobs.preflight.steps diff --git a/.github/scripts/fixtures/workflow-policy-invalid.json b/.github/scripts/fixtures/workflow-policy-invalid.json index 5226f2543..35685c8ea 100644 --- a/.github/scripts/fixtures/workflow-policy-invalid.json +++ b/.github/scripts/fixtures/workflow-policy-invalid.json @@ -83,18 +83,18 @@ ] }, { - "id": "synchronize-proof-trigger", + "id": "support-proof-trigger", "class_prefix": "[proof_identity]", "workflow": "source-proof.yml", "field": [ "on", - "pull_request", - "types" + "pull_request" ], - "value": [ - "labeled", - "synchronize" - ] + "value": { + "types": [ + "labeled" + ] + } }, { "id": "pr-number-only-proof-concurrency", diff --git a/.github/scripts/packaged_agent_proof/calibration_lineage.py b/.github/scripts/packaged_agent_proof/calibration_lineage.py index f063045df..a40424ac9 100644 --- a/.github/scripts/packaged_agent_proof/calibration_lineage.py +++ b/.github/scripts/packaged_agent_proof/calibration_lineage.py @@ -81,6 +81,8 @@ def _tracked_source_dirty(repository_root: Path) -> bool: def verify_release_head_calibration_lineage( repository_root: Path, expected_release_commit: str, + *, + allow_promotion_commit: bool = False, ) -> dict: """Bind a release checkout to the calibration source in its freeze record. @@ -140,6 +142,7 @@ def verify_release_head_calibration_lineage( calibration_source, release_source, repository_root, + allow_promotion_commit=allow_promotion_commit, ) return { **lineage, @@ -152,6 +155,8 @@ def verify_calibration_source_lineage( calibration_source: dict, frozen_source: dict, repository_root: Path, + *, + allow_promotion_commit: bool = False, ) -> dict: require( frozen_source.get("tracked_dirty") is False, @@ -238,8 +243,47 @@ def verify_calibration_source_lineage( ) + f". The {REQUIRED_RELEASE_ORDERING}.", ) + frozen_parents = _git( + repository_root, + "rev-list", + "--parents", + "-n", + "1", + frozen_source["commit"], + ).split()[1:] + direct_freeze = frozen_parents == [calibration_source["commit"]] + promotion_parent = None + if allow_promotion_commit and not direct_freeze: + candidates = [] + for parent in frozen_parents: + parent_parents = _git( + repository_root, + "rev-list", + "--parents", + "-n", + "1", + parent, + ).split()[1:] + parent_tree = _git(repository_root, "rev-parse", f"{parent}^{{tree}}") + if ( + parent_parents == [calibration_source["commit"]] + and parent_tree == frozen_source["tree"] + ): + candidates.append(parent) + if len(candidates) == 1: + promotion_parent = candidates[0] + require( + direct_freeze or promotion_parent is not None, + "the frozen candidate must be the direct single-parent child of the " + "accepted calibration source. Any later commit revokes acceptance; " + "publication may add only one explicit tree-preserving promotion commit", + ) return { "selection_commit": calibration_source["commit"], "frozen_commit": frozen_source["commit"], + "freeze_commit": promotion_parent or frozen_source["commit"], + "promotion_commit": ( + frozen_source["commit"] if promotion_parent is not None else None + ), "allowed_changed_paths": changed_paths, } diff --git a/.github/scripts/packaged_agent_proof/measurement_protocol_validation.py b/.github/scripts/packaged_agent_proof/measurement_protocol_validation.py index 812b2c861..424f1f9c1 100644 --- a/.github/scripts/packaged_agent_proof/measurement_protocol_validation.py +++ b/.github/scripts/packaged_agent_proof/measurement_protocol_validation.py @@ -35,7 +35,7 @@ "calibration_workload_state_overrides", ) EXPECTED_QUALIFICATION_MEASUREMENT_SHAPE_SHA256 = ( - "1c065562adc34d0d9978187857807e491c4e6d4aa233fdd94f5636931a7b730e" + "bc78e8c0277062f1274b0ed97e9bafbef2574b2d1934cb6ab89e7f514900fef8" ) @@ -117,6 +117,11 @@ def _verify_scenario_and_metric_contracts(protocol: dict) -> tuple[set[str], dic and all(isinstance(event, str) and event for event in boundaries), f"measurement metric {metric} must have exact start and end events", ) + require( + phase_boundaries["true_idle_exit"] + == ["final_product_request_completed", "engine_and_server_absent"], + "true-idle qualification must start at final product completion", + ) metric_contracts = protocol.get("metric_contracts") require( isinstance(metric_contracts, dict) @@ -419,6 +424,11 @@ def _verify_measurement_sampling( workload.get("input_generator"), f"measurement workload {metric}.input_generator", ) + require( + workloads["true_idle_exit"].get("workload_id") + == "true_idle_after_product_completion_60000_awake_ms_v2", + "true-idle qualification workload changed its product-completion boundary", + ) sampling = protocol.get("metric_sampling") require( isinstance(sampling, dict) and set(sampling) == required_metrics, diff --git a/.github/scripts/packaged_agent_proof/self_test_calibration_lineage.py b/.github/scripts/packaged_agent_proof/self_test_calibration_lineage.py index 1ab2b2999..00b8850a5 100644 --- a/.github/scripts/packaged_agent_proof/self_test_calibration_lineage.py +++ b/.github/scripts/packaged_agent_proof/self_test_calibration_lineage.py @@ -125,6 +125,8 @@ def _accepts_the_single_freeze_commit(root: Path, calibration: dict) -> dict: == { "selection_commit": calibration["commit"], "frozen_commit": frozen["commit"], + "freeze_commit": frozen["commit"], + "promotion_commit": None, "allowed_changed_paths": [CONSTANT_SET_FREEZE_PATH], }, "the one allowed constant-set freeze commit was not accepted intact", @@ -132,6 +134,33 @@ def _accepts_the_single_freeze_commit(root: Path, calibration: dict) -> dict: return frozen +def _rejects_commit_after_freeze( + root: Path, + calibration: dict, + frozen: dict, +) -> None: + later = _commit(root, "later empty commit", allow_empty=True) + _reject( + "a later tree-preserving commit", + ["direct single-parent child", "later commit revokes acceptance"], + calibration, + later, + root, + ) + accepted_promotion = verify_calibration_source_lineage( + calibration, + later, + root, + allow_promotion_commit=True, + ) + require( + accepted_promotion["freeze_commit"] == frozen["commit"] + and accepted_promotion["promotion_commit"] == later["commit"], + "the explicit tree-preserving promotion exception lost its exact commits", + ) + _git(root, "reset", "-q", "--hard", frozen["commit"]) + + def _rejects_identity_and_checkout_drift( root: Path, calibration: dict, @@ -396,6 +425,7 @@ def run_calibration_lineage_self_tests() -> None: root.mkdir(parents=True) calibration = _build_calibration_history(root) frozen = _accepts_the_single_freeze_commit(root, calibration) + _rejects_commit_after_freeze(root, calibration, frozen) _rejects_identity_and_checkout_drift(root, calibration, frozen) _rejects_calibrate_then_bump(root, calibration) _rejects_missing_freeze_and_unrelated_history(root, frozen) diff --git a/.github/scripts/packaged_agent_proof/self_test_full_stack_calibration.py b/.github/scripts/packaged_agent_proof/self_test_full_stack_calibration.py index 433dae637..c881c0a1e 100644 --- a/.github/scripts/packaged_agent_proof/self_test_full_stack_calibration.py +++ b/.github/scripts/packaged_agent_proof/self_test_full_stack_calibration.py @@ -33,6 +33,38 @@ def _qualification_matrix_tests(fixture: FullStackFixture) -> dict: self_measurement_protocol, require_frozen=False, ) + for label, field, value in ( + ( + "server idle epoch", + "phase", + [ + "last_queued_active_or_leased_work_ended", + "engine_and_server_absent", + ], + ), + ( + "pre-completion workload", + "workload", + "true_idle_60000_awake_ms_v1", + ), + ): + regressed_true_idle = json.loads( + json.dumps(measurement_contract["measurement_protocol"]) + ) + if field == "phase": + regressed_true_idle["phase_boundaries"]["true_idle_exit"] = value + else: + regressed_true_idle["workloads"]["true_idle_exit"]["workload_id"] = value + regressed_true_idle_path = ( + fixture.root / f"true-idle-{field}-regression.json" + ) + write_json(regressed_true_idle_path, regressed_true_idle) + try: + load_measurement_protocol(regressed_true_idle_path) + except ProofFailure: + pass + else: + raise ProofFailure(f"true-idle qualification accepted {label}") for quality_metric in ( "answer_quality", "packet_quality", diff --git a/.github/scripts/qualification-driver-artifact.mjs b/.github/scripts/qualification-driver-artifact.mjs index 5d8cfaa90..d9f973550 100644 --- a/.github/scripts/qualification-driver-artifact.mjs +++ b/.github/scripts/qualification-driver-artifact.mjs @@ -119,6 +119,19 @@ function regularFile(file, label) { return metadata; } +function regularBuildOutput(file, label) { + const metadata = lstatSync(file); + if ( + metadata.isSymbolicLink() + || !metadata.isFile() + || !Number.isSafeInteger(metadata.nlink) + || metadata.nlink < 1 + ) { + fail(`${label} must be a regular, non-symlink build output`); + } + return metadata; +} + function regularDirectory(directory, label) { const metadata = lstatSync(directory); if (metadata.isSymbolicLink() || !metadata.isDirectory()) { @@ -270,7 +283,10 @@ export function produceQualificationDriverArtifact({ label: "qualification driver source", root: targetDir, }); - const sourceMetadata = regularFile(source, "qualification driver"); + const sourceMetadata = regularBuildOutput( + source, + "qualification driver source", + ); if (process.platform !== "win32" && (sourceMetadata.mode & 0o111) === 0) { fail("qualification driver must be executable"); } diff --git a/.github/scripts/release-freeze-acceptance-jobs.json b/.github/scripts/release-freeze-acceptance-jobs.json new file mode 100644 index 000000000..e99d402a7 --- /dev/null +++ b/.github/scripts/release-freeze-acceptance-jobs.json @@ -0,0 +1,11 @@ +{ + "schema": "codestory.release-freeze-acceptance-jobs/v2", + "workflow": ".github/workflows/source-proof.yml", + "workflow_context_sha256": "c4fc041ddabf8ac44f4966e13e0c351b4f0d70bf3a94878490aff7030f912f66", + "jobs": { + "resolve": "da6c955c944644cd714728bf67d43aabd4ad049d5fde943ce7b1739f3d7cd8e5", + "freeze-hostile-mutations": "ebc27d28a1c087f848be090d2a2a458acee0177f06048c4d357e0724cf38be1a", + "freeze-windows-native-probe": "252f90a48322275128f47c58f48895a0be25909e323ae7e049ddaff015bf2299", + "freeze-acceptance": "544688894a77c9f95ef5799e3070ec3bea4e199b5ed650f9aa62d35a54e83ca4" + } +} diff --git a/.github/scripts/release-freeze-barrier.mjs b/.github/scripts/release-freeze-barrier.mjs new file mode 100644 index 000000000..81a9660e5 --- /dev/null +++ b/.github/scripts/release-freeze-barrier.mjs @@ -0,0 +1,835 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { + lstatSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { execFileSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import process from "node:process"; + +const ACTIVE_RUN_STATES = new Set([ + "queued", + "waiting", + "requested", + "pending", + "in_progress", +]); +const CONSTANT_SET_PATH = + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json"; +const PHASE_CONTRACTS = Object.freeze({ + calibration_source: Object.freeze({ + knownFutureSourceChanges: Object.freeze([CONSTANT_SET_PATH]), + plannedProofActions: Object.freeze([ + "calibration-source-acceptance", + "calibration", + "generated-constant-freeze", + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", + ]), + nextPermittedMutation: CONSTANT_SET_PATH, + }), + frozen_candidate: Object.freeze({ + knownFutureSourceChanges: Object.freeze([]), + plannedProofActions: Object.freeze([ + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", + ]), + nextPermittedMutation: null, + }), +}); +const RECEIPT_ARTIFACT_PREFIX = "release-freeze-receipt-attempt-"; +const RECEIPT_FILE = "release-freeze-receipt.json"; +const STATUS_PREFIX = "codestory/release-freeze"; +const CANCEL_POLL_ATTEMPTS = Number.parseInt( + process.env.CODESTORY_FREEZE_CANCEL_POLL_ATTEMPTS ?? "10", + 10, +); +const CANCEL_POLL_MS = Number.parseInt( + process.env.CODESTORY_FREEZE_CANCEL_POLL_MS ?? "1000", + 10, +); + +function fail(message) { + throw new Error(message); +} + +function phaseContract(phase) { + const contract = PHASE_CONTRACTS[phase]; + if (!contract) { + fail("freeze phase must be calibration_source or frozen_candidate"); + } + return contract; +} + +function run(command, args, options = {}) { + return execFileSync(command, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + ...options, + }).trim(); +} + +function git(args, repo) { + return run("git", ["-C", repo, ...args]); +} + +function gh(args) { + return run("gh", args); +} + +function values(args, name) { + const result = []; + for (let index = 0; index < args.length; index += 1) { + if (args[index] === name) { + const value = args[index + 1]; + if (!value || value.startsWith("--")) { + fail(`${name} requires a value`); + } + result.push(value); + index += 1; + } + } + return result; +} + +function value(args, name, fallback = undefined) { + const found = values(args, name); + if (found.length > 1) { + fail(`${name} may be specified only once`); + } + return found[0] ?? fallback; +} + +function required(args, name) { + const result = value(args, name); + if (!result) { + fail(`${name} is required`); + } + return result; +} + +function parseJsonFile(path, label) { + try { + return JSON.parse(readFileSync(path, "utf8")); + } catch (error) { + fail(`${label} is not valid JSON: ${error.message}`); + } +} + +function stable(value) { + if (Array.isArray(value)) { + return value.map(stable); + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => [key, stable(item)]), + ); + } + return value; +} + +export function receiptDigest(receipt) { + const withoutDigest = { ...receipt }; + delete withoutDigest.digest; + return createHash("sha256") + .update(`${JSON.stringify(stable(withoutDigest))}\n`) + .digest("hex"); +} + +function elapsedSeconds(step) { + const started = Date.parse(String(step?.started_at ?? "")); + const completed = Date.parse(String(step?.completed_at ?? "")); + if (!Number.isFinite(started) || !Number.isFinite(completed) || completed < started) { + fail(`acceptance step ${step?.name ?? ""} has invalid Actions timing`); + } + return (completed - started) / 1000; +} + +export function validateAcceptanceProvenance({ + status, + run, + jobs, + artifact, + receipt, + repository, + commit, + tree, + digest, + phase, +}) { + const escapedRepository = repository.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); + const target = new RegExp( + `^https://github\\.com/${escapedRepository}/actions/runs/([1-9][0-9]*)$`, + "u", + ).exec(String(status?.target_url ?? "")); + if ( + status?.state !== "success" + || status?.context !== `${STATUS_PREFIX}/${digest}` + || status?.description !== `tree=${tree}` + || status?.creator?.login !== "github-actions[bot]" + || status?.creator?.type !== "Bot" + || !target + ) { + fail("release freeze status is not authenticated Actions acceptance"); + } + if ( + String(run?.id) !== target[1] + || run?.head_sha !== commit + || run?.path !== ".github/workflows/source-proof.yml" + || run?.event !== "workflow_dispatch" + || run?.status !== "completed" + || run?.conclusion !== "success" + || run?.head_repository?.full_name !== repository + ) { + fail("release freeze acceptance run provenance changed"); + } + const artifactName = `${RECEIPT_ARTIFACT_PREFIX}${run.run_attempt}`; + if ( + artifact?.name !== artifactName + || artifact?.expired !== false + || String(artifact?.workflow_run?.id) !== String(run.id) + || receipt?.digest !== digest + ) { + fail("release freeze receipt artifact provenance changed"); + } + validateReceipt(receipt, { + repository, + commit, + tree, + runId: String(run.id), + runAttempt: String(run.run_attempt), + phase, + }); + if (!Array.isArray(jobs)) { + fail("release freeze acceptance jobs are missing"); + } + const requiredJobs = new Map([ + ["freeze-hostile-mutations", "Execute exact-head hostile mutation matrix"], + ["freeze-windows-native-probe", "Run exact-head Windows native probe"], + ["freeze-acceptance", "Publish executable release freeze"], + ]); + for (const [jobName, stepName] of requiredJobs) { + const job = jobs.find((candidate) => candidate?.name === jobName); + if ( + job?.status !== "completed" + || job?.conclusion !== "success" + || job?.head_sha !== commit + || String(job?.run_id) !== String(run.id) + || String(job?.run_attempt) !== String(run.run_attempt) + ) { + fail(`release freeze acceptance job ${jobName} is not a successful exact-run job`); + } + const step = job.steps?.find((candidate) => candidate?.name === stepName); + if (step?.status !== "completed" || step?.conclusion !== "success") { + fail(`release freeze acceptance step ${stepName} did not execute successfully`); + } + if (jobName === "freeze-windows-native-probe") { + const labels = new Set(job.labels ?? []); + for (const label of ["self-hosted", "Windows", "X64", "codestory-vulkan"]) { + if (!labels.has(label)) { + fail(`Windows native probe did not run on protected label ${label}`); + } + } + if (elapsedSeconds(step) >= 90) { + fail("Windows native probe must complete in under 90 seconds"); + } + } + } + return Number(target[1]); +} + +export function validateReceipt( + receipt, + { repository, commit, tree, runId, runAttempt, phase }, +) { + const expectedContract = phaseContract(phase); + if ( + receipt?.schema !== 3 + || receipt?.authority !== "github_actions" + || receipt?.phase !== phase + ) { + fail("freeze receipt must use the GitHub Actions authority schema"); + } + if ( + receipt.repository !== repository + || receipt.commit !== commit + || receipt.tree !== tree + ) { + fail("freeze receipt does not match the exact commit and tree"); + } + if (receipt.worktree_clean !== true || receipt.remote_head !== commit) { + fail("freeze receipt must prove a clean worktree pushed at the exact commit"); + } + if ( + !Number.isInteger(receipt?.release_pr?.number) + || receipt.release_pr.number <= 0 + || receipt?.release_pr?.head_commit !== commit + || receipt?.release_pr?.head !== receipt.branch + || receipt?.release_pr?.base !== "dev/codestory-next" + || !/^[0-9a-f]{40}$/u.test(String(receipt?.release_pr?.base_commit ?? "")) + ) { + fail("freeze receipt must bind the open release PR at this exact head"); + } + if ( + !Array.isArray(receipt.integrated_support_prs) + || new Set(receipt.integrated_support_prs.map(entry => entry?.number)).size + !== receipt.integrated_support_prs.length + ) { + fail("freeze receipt must contain unique integrated support PRs"); + } + if ( + !Array.isArray(receipt.known_future_source_changes) + || JSON.stringify(receipt.known_future_source_changes) + !== JSON.stringify(expectedContract.knownFutureSourceChanges) + ) { + fail(`freeze receipt future changes do not match ${phase}`); + } + if ( + JSON.stringify(receipt.planned_proof_actions) + !== JSON.stringify(expectedContract.plannedProofActions) + || JSON.stringify(receipt.proof_triggering_labels) !== "[]" + || JSON.stringify(receipt.proof_triggering_actions) !== JSON.stringify( + expectedContract.plannedProofActions, + ) + ) { + fail(`freeze receipt must record the exact ${phase} actions and no labels`); + } + for (const field of [ + "reusable_evidence", + "invalidated_evidence", + "running_workflows", + "cancelled_superseded_runs", + ]) { + if (!Array.isArray(receipt[field])) { + fail(`freeze receipt must contain ${field}`); + } + } + if (receipt.next_permitted_mutation !== expectedContract.nextPermittedMutation) { + fail(`freeze receipt next mutation does not match ${phase}`); + } + if ( + String(receipt?.acceptance_run?.id) !== String(runId) + || String(receipt?.acceptance_run?.attempt) !== String(runAttempt) + || receipt?.acceptance_run?.workflow !== ".github/workflows/source-proof.yml" + || receipt?.acceptance_run?.event !== "workflow_dispatch" + ) { + fail("freeze receipt must bind its exact Actions run and attempt"); + } + if (receipt.digest !== receiptDigest(receipt)) { + fail("freeze receipt digest does not match its contents"); + } +} + +function currentRuns(repository) { + const runs = []; + for (const status of ACTIVE_RUN_STATES) { + const raw = gh([ + "api", + "--paginate", + "--slurp", + `repos/${repository}/actions/runs?status=${status}&per_page=100`, + ]); + const pages = JSON.parse(raw || "[]"); + if (!Array.isArray(pages)) { + fail(`active workflow query for ${status} did not return paginated pages`); + } + for (const page of pages) { + for (const entry of page?.workflow_runs ?? []) { + runs.push({ + databaseId: entry.id, + workflowName: entry.name, + headSha: entry.head_sha, + headBranch: entry.head_branch, + status: entry.status, + event: entry.event, + url: entry.html_url, + }); + } + } + } + const unique = new Map(runs.map(entry => [String(entry.databaseId), entry])); + return [...unique.values()].filter((entry) => ACTIVE_RUN_STATES.has(entry.status)); +} + +function cancelSupersededRuns({ repository, commit, workflows, runs }) { + const allowlist = new Set(workflows); + const cancelled = []; + for (const entry of runs) { + if (!allowlist.has(entry.workflowName) || entry.headSha === commit) { + continue; + } + gh(["run", "cancel", String(entry.databaseId), "--repo", repository]); + cancelled.push({ + database_id: entry.databaseId, + head_sha: entry.headSha, + workflow: entry.workflowName, + }); + } + return cancelled; +} + +function waitForSupersededRunsToStop({ repository, commit, workflows }) { + if ( + !Number.isInteger(CANCEL_POLL_ATTEMPTS) + || CANCEL_POLL_ATTEMPTS < 1 + || !Number.isInteger(CANCEL_POLL_MS) + || CANCEL_POLL_MS < 0 + ) { + fail("cancellation polling configuration is invalid"); + } + for (let attempt = 0; attempt < CANCEL_POLL_ATTEMPTS; attempt += 1) { + const remaining = currentRuns(repository).filter((entry) => + workflows.includes(entry.workflowName) && entry.headSha !== commit + ); + if (remaining.length === 0) { + return; + } + if (attempt + 1 < CANCEL_POLL_ATTEMPTS) { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, CANCEL_POLL_MS); + } + } + fail("superseded broad proof remains queued or running after cancellation"); +} + +function cancelSuperseded(args) { + const repository = required(args, "--repository"); + const commit = required(args, "--commit"); + const workflows = values(args, "--broad-workflow"); + if (workflows.length === 0) { + fail("--broad-workflow is required"); + } + const before = currentRuns(repository); + const duplicate = before.find((entry) => + workflows.includes(entry.workflowName) + && entry.headSha === commit + && String(entry.databaseId) !== String(process.env.GITHUB_RUN_ID ?? "") + ); + if (duplicate) { + fail( + `unchanged head ${commit} already has active ${duplicate.workflowName} run ${duplicate.databaseId}`, + ); + } + const cancelled = cancelSupersededRuns({ + repository, + commit, + workflows, + runs: before, + }); + waitForSupersededRunsToStop({ repository, commit, workflows }); + process.stdout.write(`${JSON.stringify({ cancelled })}\n`); +} + +function invalidateSuperseded(args) { + const repository = required(args, "--repository"); + const commit = required(args, "--commit"); + const workflows = values(args, "--broad-workflow"); + if (workflows.length === 0) { + fail("--broad-workflow is required"); + } + const cancelled = cancelSupersededRuns({ + repository, + commit, + workflows, + runs: currentRuns(repository), + }); + waitForSupersededRunsToStop({ repository, commit, workflows }); + process.stdout.write(`${JSON.stringify({ cancelled })}\n`); +} + +function supportPr(repository, number, commit, repo) { + const pr = JSON.parse(gh([ + "pr", + "view", + String(number), + "--repo", + repository, + "--json", + "number,state,mergedAt,mergeCommit,baseRefName,headRefName", + ])); + const mergeCommit = pr?.mergeCommit?.oid; + if (pr.state !== "MERGED" || !pr.mergedAt || !mergeCommit) { + fail(`support PR #${number} is not merged`); + } + try { + git(["merge-base", "--is-ancestor", mergeCommit, commit], repo); + } catch { + fail(`support PR #${number} merge ${mergeCommit} is not integrated into ${commit}`); + } + return { + number: pr.number, + merge_commit: mergeCommit, + base: pr.baseRefName, + head: pr.headRefName, + }; +} + +function releasePr(repository, number, { branch, commit }) { + const pr = JSON.parse(gh(["api", `repos/${repository}/pulls/${number}`])); + const liveBaseRef = JSON.parse(gh([ + "api", + `repos/${repository}/git/ref/heads/dev/codestory-next`, + ])); + const liveBaseCommit = liveBaseRef?.object?.sha; + if ( + pr.state !== "open" + || pr?.base?.ref !== "dev/codestory-next" + || pr?.head?.ref !== branch + || pr?.head?.sha !== commit + || pr?.head?.repo?.full_name !== repository + || !/^[0-9a-f]{40}$/u.test(String(liveBaseCommit ?? "")) + ) { + fail( + `release PR #${number} must be an open same-repository ${branch} -> ` + + `dev/codestory-next PR at exact head ${commit}`, + ); + } + const comparison = JSON.parse(gh([ + "api", + `repos/${repository}/compare/${liveBaseCommit}...${commit}`, + ])); + if (!["ahead", "identical"].includes(comparison?.status)) { + fail( + `release PR #${number} head ${commit} does not contain current dev base ${liveBaseCommit}`, + ); + } + return { + number: pr.number, + base: pr.base.ref, + base_commit: liveBaseCommit, + head: pr.head.ref, + head_commit: pr.head.sha, + }; +} + +function jsonArray(args, name, label) { + let parsed; + try { + parsed = JSON.parse(value(args, name, "[]")); + } catch (error) { + fail(`${label} must be valid JSON: ${error.message}`); + } + if (!Array.isArray(parsed)) { + fail(`${label} must be a JSON array`); + } + return parsed; +} + +function stringArray(args, name, label) { + const parsed = jsonArray(args, name, label); + if (!parsed.every(entry => typeof entry === "string" && entry.length > 0)) { + fail(`${label} must contain only non-empty strings`); + } + return parsed; +} + +function recordActionsReceipt(args) { + const repo = value(args, "--repo", process.cwd()); + const repository = required(args, "--repository"); + const branch = required(args, "--branch"); + const commit = required(args, "--commit"); + const tree = required(args, "--tree"); + const output = required(args, "--output"); + const releasePrNumber = required(args, "--release-pr"); + const runId = required(args, "--run-id"); + const runAttempt = required(args, "--run-attempt"); + const phase = required(args, "--phase"); + const contract = phaseContract(phase); + const supportPrNumbers = jsonArray(args, "--support-prs-json", "support PRs"); + if ( + !supportPrNumbers.every(number => Number.isInteger(number) && number > 0) + || new Set(supportPrNumbers).size !== supportPrNumbers.length + ) { + fail("support PRs must contain unique positive integers"); + } + const reusableEvidence = stringArray( + args, + "--reusable-evidence-json", + "reusable evidence", + ); + const invalidatedEvidence = stringArray( + args, + "--invalidated-evidence-json", + "invalidated evidence", + ); + const cancelledRuns = jsonArray( + args, + "--cancelled-runs-json", + "cancelled superseded runs", + ); + const broadWorkflows = values(args, "--broad-workflow"); + if (broadWorkflows.length === 0) { + fail("release freeze requires broad workflow names"); + } + if ( + process.env.GITHUB_ACTIONS !== "true" + || process.env.GITHUB_EVENT_NAME !== "workflow_dispatch" + ) { + fail("the canonical release freeze receipt may be produced only by workflow_dispatch"); + } + + if (git(["status", "--porcelain=v1", "--untracked-files=all"], repo) !== "") { + fail("release freeze requires a clean worktree, including untracked files"); + } + if ( + git(["rev-parse", "HEAD"], repo) !== commit + || git(["rev-parse", "HEAD^{tree}"], repo) !== tree + ) { + fail("checked-out Actions source does not match the declared commit and tree"); + } + + const acceptedReleasePr = releasePr(repository, releasePrNumber, { + branch, + commit, + }); + const integratedSupportPrs = supportPrNumbers.map( + (number) => supportPr(repository, number, commit, repo), + ); + + const remainingRuns = currentRuns(repository).filter( + entry => String(entry.databaseId) !== String(runId), + ); + const remainingBroadRun = remainingRuns.find((entry) => + broadWorkflows.includes(entry.workflowName) + ); + if (remainingBroadRun) { + fail( + `broad proof ${remainingBroadRun.databaseId} remains active before freeze declaration`, + ); + } + + const receipt = { + schema: 3, + authority: "github_actions", + phase, + repository, + branch, + commit, + tree, + worktree_clean: true, + remote_head: commit, + release_pr: acceptedReleasePr, + integrated_support_prs: integratedSupportPrs, + known_future_source_changes: [...contract.knownFutureSourceChanges], + planned_proof_actions: [...contract.plannedProofActions], + proof_triggering_labels: [], + proof_triggering_actions: [...contract.plannedProofActions], + reusable_evidence: reusableEvidence, + invalidated_evidence: invalidatedEvidence, + running_workflows: remainingRuns, + cancelled_superseded_runs: cancelledRuns, + next_permitted_mutation: contract.nextPermittedMutation, + acceptance_run: { + id: Number(runId), + attempt: Number(runAttempt), + workflow: ".github/workflows/source-proof.yml", + event: "workflow_dispatch", + }, + }; + receipt.digest = receiptDigest(receipt); + validateReceipt(receipt, { + repository, + commit, + tree, + runId, + runAttempt, + phase, + }); + writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`); + const githubOutput = value(args, "--github-output"); + if (githubOutput) { + writeFileSync( + githubOutput, + `digest=${receipt.digest}\nartifact_name=${RECEIPT_ARTIFACT_PREFIX}${runAttempt}\n`, + { flag: "a" }, + ); + } + process.stdout.write(`${receipt.digest}\n`); +} + +function verifyFile(args) { + const receipt = parseJsonFile(required(args, "--receipt"), "freeze receipt"); + const repository = required(args, "--repository"); + const commit = required(args, "--commit"); + const tree = required(args, "--tree"); + validateReceipt(receipt, { + repository, + commit, + tree, + runId: required(args, "--run-id"), + runAttempt: required(args, "--run-attempt"), + phase: required(args, "--phase"), + }); + process.stdout.write(`${receipt.digest}\n`); +} + +export function acceptedFreezeStatus(statuses, { tree, digest }) { + if (!Array.isArray(statuses)) { + fail("release freeze statuses are missing"); + } + const context = `${STATUS_PREFIX}/${digest}`; + const newest = statuses + .filter(status => status?.context === context) + .reduce((latest, status) => { + if (!latest) { + return status; + } + const latestId = BigInt(String(latest.id ?? "0")); + const statusId = BigInt(String(status.id ?? "0")); + return statusId > latestId ? status : latest; + }, undefined); + if (newest?.state !== "success" || newest?.description !== `tree=${tree}`) { + return undefined; + } + return newest; +} + +function matchingStatus({ repository, commit, tree, digest }) { + const statuses = JSON.parse(gh([ + "api", + `repos/${repository}/commits/${commit}/statuses?per_page=100`, + ])); + return acceptedFreezeStatus(statuses, { tree, digest }); +} + +function downloadAuthenticatedReceipt({ repository, run }) { + const artifactName = `${RECEIPT_ARTIFACT_PREFIX}${run.run_attempt}`; + const payload = JSON.parse(gh([ + "api", + `repos/${repository}/actions/runs/${run.id}/artifacts?per_page=100`, + ])); + const matches = (payload.artifacts ?? []).filter( + artifact => artifact?.name === artifactName && artifact?.expired === false, + ); + if (matches.length !== 1) { + fail(`acceptance run must retain exactly one unexpired ${artifactName}`); + } + const directory = mkdtempSync(path.join(tmpdir(), "codestory-freeze-receipt-")); + try { + gh([ + "run", + "download", + String(run.id), + "--repo", + repository, + "--name", + artifactName, + "--dir", + directory, + ]); + const entries = readdirSync(directory); + if ( + entries.length !== 1 + || entries[0] !== RECEIPT_FILE + || !lstatSync(path.join(directory, RECEIPT_FILE)).isFile() + || lstatSync(path.join(directory, RECEIPT_FILE)).nlink !== 1 + ) { + fail("release freeze artifact must contain one singly linked canonical receipt"); + } + return { + artifact: matches[0], + receipt: parseJsonFile( + path.join(directory, RECEIPT_FILE), + "authenticated freeze receipt", + ), + }; + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +function verifyStatus(args) { + const repository = required(args, "--repository"); + const commit = required(args, "--commit"); + const tree = required(args, "--tree"); + const digest = required(args, "--receipt-digest"); + const phase = required(args, "--phase"); + const status = matchingStatus({ + repository, + commit, + tree, + digest, + phase, + }); + if (!status) { + fail("no successful exact-head release freeze status matches this receipt digest and tree"); + } + const target = /\/actions\/runs\/([1-9][0-9]*)$/u.exec(String(status.target_url ?? "")); + if (!target) { + fail("release freeze success status has no authenticated Actions run"); + } + const run = JSON.parse(gh([ + "api", + `repos/${repository}/actions/runs/${target[1]}`, + ])); + const { artifact, receipt } = downloadAuthenticatedReceipt({ + repository, + run, + }); + const currentReleasePr = releasePr(repository, receipt?.release_pr?.number, { + branch: receipt?.branch, + commit, + }); + if (currentReleasePr.base_commit !== receipt?.release_pr?.base_commit) { + fail("release PR base advanced after freeze acceptance"); + } + const jobsPayload = JSON.parse(gh([ + "api", + `repos/${repository}/actions/runs/${target[1]}/jobs?per_page=100`, + ])); + validateAcceptanceProvenance({ + status, + run, + jobs: jobsPayload.jobs, + artifact, + receipt, + repository, + commit, + tree, + digest, + phase, + }); + process.stdout.write(`${digest}\n`); +} + +function main() { + const [command, ...args] = process.argv.slice(2); + if (command === "record-actions-receipt") { + recordActionsReceipt(args); + } else if (command === "verify-file") { + verifyFile(args); + } else if (command === "verify-status") { + verifyStatus(args); + } else if (command === "cancel-superseded") { + cancelSuperseded(args); + } else if (command === "invalidate-superseded") { + invalidateSuperseded(args); + } else { + fail( + "usage: release-freeze-barrier.mjs " + + " ...", + ); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + try { + main(); + } catch (error) { + process.stderr.write(`release freeze rejected: ${error.message}\n`); + process.exitCode = 1; + } +} diff --git a/.github/scripts/release-freeze-barrier.test.mjs b/.github/scripts/release-freeze-barrier.test.mjs new file mode 100644 index 000000000..137a4edf2 --- /dev/null +++ b/.github/scripts/release-freeze-barrier.test.mjs @@ -0,0 +1,726 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { + acceptedFreezeStatus, + receiptDigest, + validateAcceptanceProvenance, + validateReceipt, +} from "./release-freeze-barrier.mjs"; + +const REPOSITORY = "TheGreenCedar/CodeStory"; +const COMMIT = "1".repeat(40); +const TREE = "2".repeat(40); +const RUN_ID = 77; +const RUN_ATTEMPT = 2; +const NEXT_PERMITTED_MUTATION = + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json"; +const CALIBRATION_SOURCE_ACTIONS = [ + "calibration-source-acceptance", + "calibration", + "generated-constant-freeze", + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", +]; +const FROZEN_CANDIDATE_ACTIONS = [ + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", +]; + +function receipt(overrides = {}) { + const phase = overrides.phase ?? "calibration_source"; + const frozen = phase === "frozen_candidate"; + const plannedActions = frozen + ? FROZEN_CANDIDATE_ACTIONS + : CALIBRATION_SOURCE_ACTIONS; + const candidate = { + schema: 3, + authority: "github_actions", + phase, + repository: REPOSITORY, + branch: "codex/release", + commit: COMMIT, + tree: TREE, + worktree_clean: true, + remote_head: COMMIT, + release_pr: { + number: 1597, + base: "dev/codestory-next", + base_commit: "0".repeat(40), + head: "codex/release", + head_commit: COMMIT, + }, + integrated_support_prs: [], + known_future_source_changes: frozen ? [] : [NEXT_PERMITTED_MUTATION], + planned_proof_actions: [...plannedActions], + proof_triggering_labels: [], + proof_triggering_actions: [...plannedActions], + reusable_evidence: [], + invalidated_evidence: [], + running_workflows: [], + cancelled_superseded_runs: [], + next_permitted_mutation: frozen ? null : NEXT_PERMITTED_MUTATION, + acceptance_run: { + id: RUN_ID, + attempt: RUN_ATTEMPT, + workflow: ".github/workflows/source-proof.yml", + event: "workflow_dispatch", + }, + ...overrides, + }; + candidate.digest = receiptDigest(candidate); + return candidate; +} + +const RECEIPT_CONTEXT = { + repository: REPOSITORY, + commit: COMMIT, + tree: TREE, + runId: String(RUN_ID), + runAttempt: String(RUN_ATTEMPT), + phase: "calibration_source", +}; + +test("an exact clean pushed calibration-source Actions receipt passes", () => { + validateReceipt(receipt(), RECEIPT_CONTEXT); +}); + +test("a frozen-candidate receipt carries no future mutation and passes", () => { + const frozen = receipt({ phase: "frozen_candidate" }); + validateReceipt(frozen, { + ...RECEIPT_CONTEXT, + phase: "frozen_candidate", + }); + assert.deepEqual(frozen.known_future_source_changes, []); + assert.equal(frozen.next_permitted_mutation, null); +}); + +test("calibration-source acceptance orders calibration before the sole source proof", () => { + const actions = receipt().planned_proof_actions; + assert.ok(actions.indexOf("calibration") < actions.indexOf("generated-constant-freeze")); + assert.ok(actions.indexOf("generated-constant-freeze") < actions.indexOf("source-proof")); + assert.equal(actions.filter(action => action === "source-proof").length, 1); +}); + +test("receipts cannot cross the calibration-source and frozen-candidate phases", () => { + assert.throws( + () => validateReceipt(receipt(), { + ...RECEIPT_CONTEXT, + phase: "frozen_candidate", + }), + /authority schema/u, + ); + assert.throws( + () => validateReceipt(receipt({ phase: "frozen_candidate" }), RECEIPT_CONTEXT), + /authority schema/u, + ); +}); + +test("a newer invalidation status revokes an older accepted freeze", () => { + const acceptedReceipt = receipt(); + const context = `codestory/release-freeze/${acceptedReceipt.digest}`; + const accepted = { + id: "9007199254740993", + state: "success", + context, + description: `tree=${TREE}`, + }; + assert.equal( + acceptedFreezeStatus([accepted], { + tree: TREE, + digest: acceptedReceipt.digest, + }), + accepted, + ); + assert.equal( + acceptedFreezeStatus([ + accepted, + { + id: "9007199254740994", + state: "error", + context, + description: `superseded-by=${"3".repeat(40)}`, + }, + ], { + tree: TREE, + digest: acceptedReceipt.digest, + }), + undefined, + ); +}); + +for (const [name, mutate, pattern] of [ + ["later commit", (value) => { value.commit = "3".repeat(40); }, /exact commit and tree/u], + ["later tree", (value) => { value.tree = "4".repeat(40); }, /exact commit and tree/u], + ["dirty worktree", (value) => { value.worktree_clean = false; }, /clean worktree/u], + ["unpushed head", (value) => { value.remote_head = "5".repeat(40); }, /clean worktree/u], + ["moved release PR", (value) => { + value.release_pr.head_commit = "5".repeat(40); + }, /bind the open release PR/u], + ["unbound release base", (value) => { + value.release_pr.base_commit = ""; + }, /bind the open release PR/u], + ["undeclared source change", (value) => { + value.known_future_source_changes.push(".github/workflows/release.yml"); + }, /future changes do not match calibration_source/u], + ["caller-selected proof actions", (value) => { + value.planned_proof_actions = ["source-proof"]; + }, /exact calibration_source actions/u], + ["proof-triggering label", (value) => { + value.proof_triggering_labels = ["source-proof"]; + }, /exact calibration_source actions/u], + ["cross-attempt receipt", (value) => { + value.acceptance_run.attempt = RUN_ATTEMPT + 1; + }, /exact Actions run and attempt/u], + ["missing handoff field", (value) => { delete value.running_workflows; }, /running_workflows/u], + ["missing next mutation", (value) => { + value.next_permitted_mutation = ""; + }, /next mutation does not match calibration_source/u], + ["tampered receipt", (value) => { + value.reusable_evidence.push("unauthenticated evidence"); + }, /digest/u], +]) { + test(`freeze barrier rejects ${name}`, () => { + const candidate = receipt(); + mutate(candidate); + if (name !== "tampered receipt") { + candidate.digest = receiptDigest(candidate); + } + assert.throws( + () => validateReceipt(candidate, RECEIPT_CONTEXT), + pattern, + ); + }); +} + +function acceptanceProvenance() { + const acceptedReceipt = receipt(); + const digest = acceptedReceipt.digest; + const startedAt = "2026-07-30T12:00:00Z"; + const completedAt = "2026-07-30T12:00:06Z"; + const job = (name, stepName, labels = ["ubuntu-latest"]) => ({ + name, + status: "completed", + conclusion: "success", + head_sha: COMMIT, + run_id: RUN_ID, + run_attempt: RUN_ATTEMPT, + labels, + steps: [{ + name: stepName, + status: "completed", + conclusion: "success", + started_at: startedAt, + completed_at: completedAt, + }], + }); + return { + status: { + state: "success", + context: `codestory/release-freeze/${digest}`, + description: `tree=${TREE}`, + target_url: `https://github.com/${REPOSITORY}/actions/runs/${RUN_ID}`, + creator: { login: "github-actions[bot]", type: "Bot" }, + }, + run: { + id: RUN_ID, + run_attempt: RUN_ATTEMPT, + head_sha: COMMIT, + path: ".github/workflows/source-proof.yml", + event: "workflow_dispatch", + status: "completed", + conclusion: "success", + head_repository: { full_name: REPOSITORY }, + }, + jobs: [ + job("freeze-hostile-mutations", "Execute exact-head hostile mutation matrix"), + job( + "freeze-windows-native-probe", + "Run exact-head Windows native probe", + ["self-hosted", "Windows", "X64", "codestory-vulkan"], + ), + job("freeze-acceptance", "Publish executable release freeze"), + ], + artifact: { + name: `release-freeze-receipt-attempt-${RUN_ATTEMPT}`, + expired: false, + workflow_run: { id: RUN_ID }, + }, + receipt: acceptedReceipt, + repository: REPOSITORY, + commit: COMMIT, + tree: TREE, + digest, + phase: "calibration_source", + }; +} + +test("acceptance trusts exact Actions run, job, step, host, and duration provenance", () => { + assert.equal(validateAcceptanceProvenance(acceptanceProvenance()), 77); +}); + +for (const [name, mutate, pattern] of [ + ["caller-authored success", (value) => { + value.status.creator = { login: "TheGreenCedar", type: "User" }; + }, /not authenticated Actions acceptance/u], + ["cross-head run", (value) => { + value.run.head_sha = "3".repeat(40); + }, /run provenance changed/u], + ["wrong workflow", (value) => { + value.run.path = ".github/workflows/release.yml"; + }, /run provenance changed/u], + ["skipped hostile mutations", (value) => { + value.jobs[0].conclusion = "skipped"; + }, /not a successful exact-run job/u], + ["unprotected Windows runner", (value) => { + value.jobs[1].labels = ["self-hosted", "Windows", "X64"]; + }, /protected label codestory-vulkan/u], + ["90-second Windows probe", (value) => { + value.jobs[1].steps[0].completed_at = "2026-07-30T12:01:30Z"; + }, /under 90 seconds/u], + ["fabricated native step", (value) => { + value.jobs[1].steps[0].conclusion = "failure"; + }, /did not execute successfully/u], + ["wrong receipt artifact", (value) => { + value.artifact.name = "release-freeze-receipt-attempt-999"; + }, /receipt artifact provenance changed/u], + ["expired receipt artifact", (value) => { + value.artifact.expired = true; + }, /receipt artifact provenance changed/u], + ["cross-run receipt artifact", (value) => { + value.artifact.workflow_run.id = RUN_ID + 1; + }, /receipt artifact provenance changed/u], + ["cross-attempt receipt artifact", (value) => { + value.artifact.name = `release-freeze-receipt-attempt-${RUN_ATTEMPT + 1}`; + }, /receipt artifact provenance changed/u], + ["tampered receipt artifact", (value) => { + value.receipt.running_workflows.push({ id: 123 }); + }, /digest/u], +]) { + test(`acceptance rejects ${name}`, () => { + const value = acceptanceProvenance(); + mutate(value); + assert.throws(() => validateAcceptanceProvenance(value), pattern); + }); +} + +test("verify-file is executable and rejects a later commit", () => { + const root = mkdtempSync(path.join(tmpdir(), "codestory-freeze-")); + const receiptPath = path.join(root, "receipt.json"); + writeFileSync(receiptPath, `${JSON.stringify(receipt(), null, 2)}\n`); + const script = new URL("./release-freeze-barrier.mjs", import.meta.url); + const accepted = spawnSync( + process.execPath, + [ + script.pathname, + "verify-file", + "--receipt", + receiptPath, + "--repository", + REPOSITORY, + "--commit", + COMMIT, + "--tree", + TREE, + "--run-id", + String(RUN_ID), + "--run-attempt", + String(RUN_ATTEMPT), + "--phase", + "calibration_source", + ], + { encoding: "utf8" }, + ); + assert.equal(accepted.status, 0, accepted.stderr); + assert.equal(accepted.stdout.trim(), receipt().digest); + + const rejected = spawnSync( + process.execPath, + [ + script.pathname, + "verify-file", + "--receipt", + receiptPath, + "--repository", + REPOSITORY, + "--commit", + "8".repeat(40), + "--tree", + TREE, + "--run-id", + String(RUN_ID), + "--run-attempt", + String(RUN_ATTEMPT), + "--phase", + "calibration_source", + ], + { encoding: "utf8" }, + ); + assert.notEqual(rejected.status, 0); + assert.match(rejected.stderr, /exact commit and tree/u); +}); + +test("record-actions-receipt refuses to mint authority outside GitHub Actions", () => { + const root = mkdtempSync(path.join(tmpdir(), "codestory-freeze-outside-actions-")); + const script = new URL("./release-freeze-barrier.mjs", import.meta.url); + const result = spawnSync( + process.execPath, + [ + script.pathname, + "record-actions-receipt", + "--repo", + root, + "--repository", + REPOSITORY, + "--branch", + "codex/release", + "--commit", + COMMIT, + "--tree", + TREE, + "--release-pr", + "1", + "--output", + path.join(root, "receipt.json"), + "--run-id", + String(RUN_ID), + "--run-attempt", + String(RUN_ATTEMPT), + "--phase", + "calibration_source", + "--support-prs-json", + "[]", + "--broad-workflow", + "Exact-head source proof", + ], + { + encoding: "utf8", + env: { + ...process.env, + GITHUB_ACTIONS: "", + GITHUB_EVENT_NAME: "", + }, + }, + ); + assert.notEqual(result.status, 0); + assert.match( + result.stderr, + /canonical release freeze receipt may be produced only by workflow_dispatch/u, + ); +}); + +test("record-actions-receipt rejects a PR whose snapshot omits the live dev head", () => { + const sandbox = mkdtempSync(path.join(tmpdir(), "codestory-freeze-stale-base-")); + const root = path.join(sandbox, "repo"); + mkdirSync(root); + execFileSync("git", ["init", "-q", "-b", "codex/release", root]); + execFileSync("git", ["-C", root, "config", "user.email", "test@example.com"]); + execFileSync("git", ["-C", root, "config", "user.name", "Test"]); + writeFileSync(path.join(root, "tracked.txt"), "candidate\n"); + execFileSync("git", ["-C", root, "add", "tracked.txt"]); + execFileSync("git", ["-C", root, "commit", "-qm", "candidate"]); + const commit = execFileSync("git", ["-C", root, "rev-parse", "HEAD"], { + encoding: "utf8", + }).trim(); + const tree = execFileSync("git", ["-C", root, "rev-parse", "HEAD^{tree}"], { + encoding: "utf8", + }).trim(); + const staleBase = "a".repeat(40); + const liveBase = "b".repeat(40); + const fakeGh = path.join(sandbox, "gh"); + writeFileSync( + fakeGh, + `#!/bin/sh +if [ "$1" = "api" ] && [ "$2" = "repos/${REPOSITORY}/pulls/1597" ]; then + printf '%s\\n' '{"number":1597,"state":"open","base":{"ref":"dev/codestory-next","sha":"${staleBase}"},"head":{"ref":"codex/release","sha":"${commit}","repo":{"full_name":"${REPOSITORY}"}}}' + exit 0 +fi +if [ "$1" = "api" ] && [ "$2" = "repos/${REPOSITORY}/git/ref/heads/dev/codestory-next" ]; then + printf '%s\\n' '{"object":{"sha":"${liveBase}"}}' + exit 0 +fi +if [ "$1" = "api" ] && [ "$2" = "repos/${REPOSITORY}/compare/${liveBase}...${commit}" ]; then + printf '%s\\n' '{"status":"diverged"}' + exit 0 +fi +if [ "$1" = "api" ] && [ "$2" = "repos/${REPOSITORY}/compare/${staleBase}...${commit}" ]; then + printf '%s\\n' '{"status":"ahead"}' + exit 0 +fi +if [ "$1 $2" = "run list" ]; then + printf '%s\\n' '[]' + exit 0 +fi +exit 9 +`, + ); + chmodSync(fakeGh, 0o755); + const script = new URL("./release-freeze-barrier.mjs", import.meta.url); + const result = spawnSync( + process.execPath, + [ + script.pathname, + "record-actions-receipt", + "--repo", + root, + "--repository", + REPOSITORY, + "--branch", + "codex/release", + "--commit", + commit, + "--tree", + tree, + "--release-pr", + "1597", + "--output", + path.join(root, "receipt.json"), + "--run-id", + String(RUN_ID), + "--run-attempt", + String(RUN_ATTEMPT), + "--phase", + "calibration_source", + "--support-prs-json", + "[]", + "--reusable-evidence-json", + "[]", + "--invalidated-evidence-json", + "[]", + "--cancelled-runs-json", + "[]", + "--broad-workflow", + "Exact-head source proof", + ], + { + encoding: "utf8", + env: { + ...process.env, + GITHUB_ACTIONS: "true", + GITHUB_EVENT_NAME: "workflow_dispatch", + PATH: `${sandbox}${path.delimiter}${process.env.PATH}`, + }, + }, + ); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /does not contain current dev base/u); +}); + +test("cancel-superseded rejects a cancellation request that leaves the run active", () => { + const root = mkdtempSync(path.join(tmpdir(), "codestory-freeze-gh-")); + const fakeGh = path.join(root, "gh"); + writeFileSync( + fakeGh, + `#!/bin/sh +if [ "$1 $2 $3" = "api --paginate --slurp" ]; then + case "$4" in + *status=in_progress*) + printf '%s\\n' '[ + {"workflow_runs":[{"id":123,"name":"Exact-head source proof","head_sha":"${"9".repeat(40)}","head_branch":"old","status":"in_progress","event":"workflow_dispatch","html_url":"https://example.invalid/123"}]} + ]' + ;; + *) printf '%s\\n' '[{"workflow_runs":[]}]' ;; + esac + exit 0 +fi +if [ "$1 $2" = "run cancel" ]; then + exit 0 +fi +exit 1 +`, + ); + chmodSync(fakeGh, 0o755); + const script = new URL("./release-freeze-barrier.mjs", import.meta.url); + const result = spawnSync( + process.execPath, + [ + script.pathname, + "cancel-superseded", + "--repository", + "TheGreenCedar/CodeStory", + "--commit", + COMMIT, + "--broad-workflow", + "Exact-head source proof", + ], + { + encoding: "utf8", + env: { + ...process.env, + CODESTORY_FREEZE_CANCEL_POLL_ATTEMPTS: "2", + CODESTORY_FREEZE_CANCEL_POLL_MS: "0", + PATH: `${root}${path.delimiter}${process.env.PATH}`, + }, + }, + ); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /remains queued or running after cancellation/u); +}); + +test("cancel-superseded finds an obsolete proof on a later active-run page", () => { + const root = mkdtempSync(path.join(tmpdir(), "codestory-freeze-paginated-gh-")); + const fakeGh = path.join(root, "gh"); + const cancelledMarker = path.join(root, "cancelled"); + writeFileSync( + fakeGh, + `#!/bin/sh +if [ "$1 $2 $3" = "api --paginate --slurp" ]; then + case "$4" in + *status=in_progress*) + if [ -f "${cancelledMarker}" ]; then + printf '%s\\n' '[{"workflow_runs":[]}]' + else + printf '%s\\n' '[ + {"workflow_runs":[{"id":1,"name":"Draft source checks","head_sha":"${COMMIT}","head_branch":"candidate","status":"in_progress","event":"pull_request","html_url":"https://example.invalid/1"}]}, + {"workflow_runs":[{"id":999,"name":"Exact-head source proof","head_sha":"${"9".repeat(40)}","head_branch":"obsolete","status":"in_progress","event":"workflow_dispatch","html_url":"https://example.invalid/999"}]} + ]' + fi + ;; + *) printf '%s\\n' '[{"workflow_runs":[]}]' ;; + esac + exit 0 +fi +if [ "$1 $2 $3" = "run cancel 999" ]; then + : > "${cancelledMarker}" + exit 0 +fi +exit 9 +`, + ); + chmodSync(fakeGh, 0o755); + const script = new URL("./release-freeze-barrier.mjs", import.meta.url); + const result = spawnSync( + process.execPath, + [ + script.pathname, + "cancel-superseded", + "--repository", + REPOSITORY, + "--commit", + COMMIT, + "--broad-workflow", + "Exact-head source proof", + ], + { + encoding: "utf8", + env: { + ...process.env, + CODESTORY_FREEZE_CANCEL_POLL_ATTEMPTS: "2", + CODESTORY_FREEZE_CANCEL_POLL_MS: "0", + PATH: `${root}${path.delimiter}${process.env.PATH}`, + }, + }, + ); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { + cancelled: [{ + database_id: 999, + head_sha: "9".repeat(40), + workflow: "Exact-head source proof", + }], + }); +}); + +test("cancel-superseded rejects another active broad run on the unchanged head", () => { + const root = mkdtempSync(path.join(tmpdir(), "codestory-freeze-duplicate-gh-")); + const fakeGh = path.join(root, "gh"); + writeFileSync( + fakeGh, + `#!/bin/sh +if [ "$1 $2 $3" = "api --paginate --slurp" ]; then + case "$4" in + *status=in_progress*) + printf '%s\\n' '[ + {"workflow_runs":[{"id":456,"name":"Exact-head source proof","head_sha":"${COMMIT}","head_branch":"candidate","status":"in_progress","event":"workflow_dispatch","html_url":"https://example.invalid/456"}]} + ]' + ;; + *) printf '%s\\n' '[{"workflow_runs":[]}]' ;; + esac + exit 0 +fi +exit 1 +`, + ); + chmodSync(fakeGh, 0o755); + const script = new URL("./release-freeze-barrier.mjs", import.meta.url); + const result = spawnSync( + process.execPath, + [ + script.pathname, + "cancel-superseded", + "--repository", + "TheGreenCedar/CodeStory", + "--commit", + COMMIT, + "--broad-workflow", + "Exact-head source proof", + ], + { + encoding: "utf8", + env: { + ...process.env, + PATH: `${root}${path.delimiter}${process.env.PATH}`, + }, + }, + ); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /unchanged head.*already has active/u); +}); + +test("automatic invalidation preserves an active proof for the new exact head", () => { + const root = mkdtempSync(path.join(tmpdir(), "codestory-freeze-current-gh-")); + const fakeGh = path.join(root, "gh"); + writeFileSync( + fakeGh, + `#!/bin/sh +if [ "$1 $2 $3" = "api --paginate --slurp" ]; then + case "$4" in + *status=in_progress*) + printf '%s\\n' '[ + {"workflow_runs":[{"id":789,"name":"Exact-head source proof","head_sha":"${COMMIT}","head_branch":"candidate","status":"in_progress","event":"workflow_dispatch","html_url":"https://example.invalid/789"}]} + ]' + ;; + *) printf '%s\\n' '[{"workflow_runs":[]}]' ;; + esac + exit 0 +fi +if [ "$1 $2" = "run cancel" ]; then + exit 9 +fi +exit 1 +`, + ); + chmodSync(fakeGh, 0o755); + const script = new URL("./release-freeze-barrier.mjs", import.meta.url); + const result = spawnSync( + process.execPath, + [ + script.pathname, + "invalidate-superseded", + "--repository", + "TheGreenCedar/CodeStory", + "--commit", + COMMIT, + "--broad-workflow", + "Exact-head source proof", + ], + { + encoding: "utf8", + env: { + ...process.env, + CODESTORY_FREEZE_CANCEL_POLL_ATTEMPTS: "2", + CODESTORY_FREEZE_CANCEL_POLL_MS: "0", + PATH: `${root}${path.delimiter}${process.env.PATH}`, + }, + }, + ); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { cancelled: [] }); +}); diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index be1b5528c..d07cdc9d5 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -27,7 +27,7 @@ permissions: concurrency: group: auto-release-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: true jobs: detect-version: @@ -61,7 +61,7 @@ jobs: needs: detect-version if: needs.detect-version.outputs.should_release == 'true' && needs.detect-version.outputs.release_lane == 'native' permissions: - actions: read + actions: write # This is the lane that actually publishes releases, so it is the lane whose token has to be # able to read the lost-runner annotation. A called workflow cannot widen the caller's grant. checks: read diff --git a/.github/workflows/linux-vulkan-proof.yml b/.github/workflows/linux-vulkan-proof.yml index 156dcdedb..7ea477072 100644 --- a/.github/workflows/linux-vulkan-proof.yml +++ b/.github/workflows/linux-vulkan-proof.yml @@ -45,47 +45,8 @@ on: required: false default: false type: boolean - workflow_dispatch: - inputs: - version: - required: true - type: string - ref: - required: true - type: string - proof_key: - required: false - type: string - package_run_id: - description: Upstream packaged-platform run containing codestory-cli-linux-x64; omitted for standalone constant calibration. - required: false - default: "" - type: string - calibration_bundle_artifact: - required: false - default: "" - type: string - calibration_bundle_run_id: - required: false - default: "" - type: string - candidate_installed_proof: - description: Install and prove the exact package through the candidate-managed launcher boundary. - required: false - default: true - type: boolean - candidate_producer_workflow_path: - description: Top-level workflow path authenticated as the candidate artifact producer. - required: false - default: ".github/workflows/packaged-platform-pr.yml" - type: string - server_behavior_only: - description: Prove bounded package retrieval readiness without answer-quality or performance claims. - required: false - default: true - type: boolean constant_calibration_mode: - description: Collect optional Linux Vulkan constant-calibration evidence without feeding the frozen bundle. + description: Collect optional Linux Vulkan calibration evidence without feeding assembly. required: false default: false type: boolean @@ -662,7 +623,7 @@ jobs: retention-days: 30 optional-constant-calibration: - if: ${{ github.event_name == 'workflow_dispatch' && inputs.constant_calibration_mode }} + if: ${{ inputs.constant_calibration_mode }} needs: route name: Optional Linux Vulkan constant calibration runs-on: [self-hosted, Linux, X64, codestory-linux-vulkan] @@ -728,10 +689,11 @@ jobs: shell: bash env: CODESTORY_EMBED_ALLOW_CPU: "0" + CONSTANT_CALIBRATION_MODE: ${{ inputs.constant_calibration_mode }} INPUT_VERSION: ${{ inputs.version }} run: | set -euo pipefail - test "$GITHUB_EVENT_NAME" = workflow_dispatch + test "$CONSTANT_CALIBRATION_MODE" = true test "$(jq -r .status crates/codestory-llama-sys/per-user-embedding-server-constant-set.json)" = unfrozen version="${INPUT_VERSION#v}" source_sha="$(git rev-parse HEAD)" diff --git a/.github/workflows/macos-metal-proof.yml b/.github/workflows/macos-metal-proof.yml index d7b85a6e4..6a865193b 100644 --- a/.github/workflows/macos-metal-proof.yml +++ b/.github/workflows/macos-metal-proof.yml @@ -56,51 +56,6 @@ on: required: false default: false type: boolean - workflow_dispatch: - inputs: - version: - description: CodeStory version to prove. - required: true - type: string - ref: - description: Git ref to check out. Defaults to the current SHA. - required: false - type: string - proof_key: - description: Stable proof identity for cancellation. - required: false - type: string - calibration_bundle_artifact: - description: Frozen calibration bundle artifact name. - required: false - default: "" - type: string - calibration_bundle_run_id: - description: Workflow run that produced the frozen calibration bundle artifact. - required: false - default: "" - type: string - calibration_mode: - description: Collect three independent pre-freeze Metal calibration runs. - required: false - default: false - type: boolean - candidate_installed_proof: - description: Install and prove the exact package through the candidate-managed launcher boundary. - required: false - default: false - type: boolean - candidate_producer_workflow_path: - description: Top-level workflow path authenticated as the candidate artifact producer. - required: false - default: ".github/workflows/macos-metal-proof.yml" - type: string - server_behavior_only: - description: Prove bounded package retrieval readiness without answer-quality or performance claims. - required: false - default: false - type: boolean - permissions: actions: read contents: read diff --git a/.github/workflows/packaged-platform-pr.yml b/.github/workflows/packaged-platform-pr.yml index 64d322a15..3247d71fd 100644 --- a/.github/workflows/packaged-platform-pr.yml +++ b/.github/workflows/packaged-platform-pr.yml @@ -1,8 +1,6 @@ name: Platform and integration proof on: - pull_request: - types: [labeled] workflow_dispatch: inputs: mode: @@ -37,19 +35,23 @@ on: description: Workflow run that produced the frozen calibration bundle artifact. required: false type: string + freeze_receipt_digest: + description: Active executable-freeze receipt digest for this exact proof head. + required: true + type: string permissions: - actions: read + actions: write contents: read pull-requests: read + statuses: read concurrency: - group: proof-${{ github.sha }}-${{ inputs.mode || 'platform' }}-${{ inputs.pr_number || github.event.pull_request.number || 'dev' }}-${{ github.event.action == 'labeled' && github.event.label.name || 'dispatch' }} + group: proof-${{ github.sha }}-${{ inputs.mode || 'platform' }}-${{ inputs.pr_number || 'dev' }} cancel-in-progress: true jobs: route: - if: github.event_name != 'pull_request' || (github.event.action == 'labeled' && github.event.label.name == 'platform-proof') runs-on: ubuntu-latest timeout-minutes: 10 outputs: @@ -165,36 +167,28 @@ jobs: echo "proof_key=$mode-pr-$pr_number-$current_head" } >> "$GITHUB_OUTPUT" - - name: Require successful exact-head source proof - if: steps.resolve.outputs.mode != 'integration' + - name: Checkout accepted candidate + uses: actions/checkout@v5 + with: + ref: ${{ steps.resolve.outputs.head_sha }} + fetch-depth: 0 + + - name: Cancel superseded proof runs shell: bash env: GH_TOKEN: ${{ github.token }} HEAD_SHA: ${{ steps.resolve.outputs.head_sha }} run: | - set -euo pipefail - accepted=false - while IFS= read -r run_id; do - if gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id/jobs?per_page=100" \ - --jq '.jobs[] | select(.name == "full-source-gate" and .conclusion == "success") | .id' \ - | grep -q . - then - accepted=true - echo "Accepted exact-head source proof run $run_id for $HEAD_SHA." - break - fi - done < <( - gh api --paginate \ - "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$HEAD_SHA&status=completed&per_page=100" \ - | jq -r --arg repo "$GITHUB_REPOSITORY" \ - '.workflow_runs[] | select(.path == ".github/workflows/source-proof.yml" and .head_repository.full_name == $repo and (.event == "pull_request" or .event == "workflow_dispatch") and .conclusion == "success") | .id' - ) - test "$accepted" = true || { - echo "::error::No successful full-source-gate job exists for exact head $HEAD_SHA." - exit 1 - } + node .github/scripts/release-freeze-barrier.mjs cancel-superseded \ + --repository "$GITHUB_REPOSITORY" \ + --commit "$HEAD_SHA" \ + --broad-workflow "Exact-head source proof" \ + --broad-workflow "Platform and integration proof" \ + --broad-workflow "Release" \ + --broad-workflow "Auto Release" - name: Authenticate calibration bundle producer + id: calibration if: inputs.calibration_bundle_artifact != '' || inputs.calibration_bundle_run_id != '' shell: bash env: @@ -218,10 +212,59 @@ jobs: )" test "$artifact_count" = 1 - - uses: actions/checkout@v5 - with: - ref: ${{ steps.resolve.outputs.head_sha }} - fetch-depth: 0 + - name: Require executable release freeze + shell: bash + env: + GH_TOKEN: ${{ github.token }} + FREEZE_RECEIPT_DIGEST: ${{ inputs.freeze_receipt_digest }} + HEAD_SHA: ${{ steps.resolve.outputs.head_sha }} + RESOLVED_MODE: ${{ steps.resolve.outputs.mode }} + run: | + set -euo pipefail + printf '%s' "$FREEZE_RECEIPT_DIGEST" | grep -Eq '^[0-9a-f]{64}$' + if [ "$RESOLVED_MODE" = calibration ]; then + freeze_phase=calibration_source + else + freeze_phase=frozen_candidate + fi + tree="$( + gh api "repos/$GITHUB_REPOSITORY/git/commits/$HEAD_SHA" --jq '.tree.sha' + )" + node .github/scripts/release-freeze-barrier.mjs verify-status \ + --repository "$GITHUB_REPOSITORY" \ + --commit "$HEAD_SHA" \ + --tree "$tree" \ + --phase "$freeze_phase" \ + --receipt-digest "$FREEZE_RECEIPT_DIGEST" + + - name: Require successful exact-head source proof + if: steps.resolve.outputs.mode != 'integration' && steps.resolve.outputs.mode != 'calibration' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ steps.resolve.outputs.head_sha }} + run: | + set -euo pipefail + accepted=false + while IFS= read -r run_id; do + if gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id/jobs?per_page=100" \ + --jq '.jobs[] | select(.name == "full-source-gate" and .conclusion == "success") | .id' \ + | grep -q . + then + accepted=true + echo "Accepted exact-head source proof run $run_id for $HEAD_SHA." + break + fi + done < <( + gh api --paginate \ + "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$HEAD_SHA&status=completed&per_page=100" \ + | jq -r --arg repo "$GITHUB_REPOSITORY" \ + '.workflow_runs[] | select(.path == ".github/workflows/source-proof.yml" and .head_repository.full_name == $repo and .event == "workflow_dispatch" and .conclusion == "success") | .id' + ) + test "$accepted" = true || { + echo "::error::No successful full-source-gate job exists for exact head $HEAD_SHA." + exit 1 + } - name: Select change-aware proof scope id: scope @@ -381,6 +424,8 @@ jobs: with: ref: ${{ needs.route.outputs.head_sha }} proof_key: ${{ needs.route.outputs.proof_key }} + version: ${{ needs.route.outputs.version }} + freeze_receipt_digest: ${{ inputs.freeze_receipt_digest }} packaged-proof: if: >- diff --git a/.github/workflows/release-freeze-invalidation.yml b/.github/workflows/release-freeze-invalidation.yml new file mode 100644 index 000000000..21e4dda82 --- /dev/null +++ b/.github/workflows/release-freeze-invalidation.yml @@ -0,0 +1,78 @@ +name: Release freeze invalidation + +on: + pull_request: + branches: + - dev/codestory-next + types: [synchronize] + push: + branches: + - dev/codestory-next + +permissions: + actions: write + contents: read + statuses: write + +concurrency: + group: release-freeze-invalidation-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + invalidate: + name: Cancel proof for a superseded frozen head + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + + - name: Invalidate a superseded release freeze + shell: bash + env: + AFTER_SHA: ${{ github.event.after || github.sha }} + BEFORE_SHA: ${{ github.event.before }} + EVENT_NAME: ${{ github.event_name }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + printf '%s' "$BEFORE_SHA" | grep -Eq '^[0-9a-f]{40}$' + printf '%s' "$AFTER_SHA" | grep -Eq '^[0-9a-f]{40}$' + test "$BEFORE_SHA" != "$AFTER_SHA" + if [ "$EVENT_NAME" = push ]; then + node .github/scripts/release-freeze-barrier.mjs invalidate-superseded \ + --repository "$GITHUB_REPOSITORY" \ + --commit "$AFTER_SHA" \ + --broad-workflow "Exact-head source proof" \ + --broad-workflow "Platform and integration proof" \ + --broad-workflow "Release" \ + --broad-workflow "Auto Release" + exit 0 + fi + freeze_contexts="$( + gh api "repos/$GITHUB_REPOSITORY/commits/$BEFORE_SHA/statuses?per_page=100" \ + | jq -r \ + '[.[] | select( + .state == "success" + and (.context | startswith("codestory/release-freeze/")) + ) | .context] | unique[]' + )" + if [ -z "$freeze_contexts" ]; then + echo "Previous head $BEFORE_SHA was not a declared release candidate." + exit 0 + fi + while IFS= read -r context; do + gh api \ + --method POST \ + "repos/$GITHUB_REPOSITORY/statuses/$BEFORE_SHA" \ + -f state=error \ + -f "context=$context" \ + -f "description=superseded-by=$AFTER_SHA" \ + -f "target_url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + done <<<"$freeze_contexts" + node .github/scripts/release-freeze-barrier.mjs invalidate-superseded \ + --repository "$GITHUB_REPOSITORY" \ + --commit "$AFTER_SHA" \ + --broad-workflow "Exact-head source proof" \ + --broad-workflow "Platform and integration proof" \ + --broad-workflow "Release" \ + --broad-workflow "Auto Release" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6a98e50c..7145cd62b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,9 +27,8 @@ on: description: "Exact dev/codestory-next head to authenticate without publishing" required: true type: string - permissions: - actions: read + actions: write # accelerator-non-claim reads the job annotation that identifies a lost runner, which the Actions # annotations endpoint gates on `checks: read`. Without it the collector fails closed and this # workflow stops rather than mistaking an unreadable signature for an ordinary failure. @@ -39,7 +38,7 @@ permissions: concurrency: group: release-${{ inputs.version }} - cancel-in-progress: false + cancel-in-progress: true jobs: workflow-policy: @@ -95,16 +94,47 @@ jobs: with: fetch-depth: 0 + - name: Cancel superseded proof runs + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + node .github/scripts/release-freeze-barrier.mjs cancel-superseded \ + --repository "$GITHUB_REPOSITORY" \ + --commit "$GITHUB_SHA" \ + --broad-workflow "Exact-head source proof" \ + --broad-workflow "Platform and integration proof" \ + --broad-workflow "Release" \ + --broad-workflow "Auto Release" + - name: Verify release-head calibration lineage + id: lineage env: BASH_ENV: /dev/null + PUBLISH_RELEASE: ${{ inputs.publish_release }} shell: /bin/bash --noprofile --norc -e -o pipefail {0} working-directory: ${{ github.workspace }} - run: >- - /usr/bin/python3 -E -s - "$GITHUB_WORKSPACE/.github/scripts/check-calibration-release-lineage.py" - --repo "$GITHUB_WORKSPACE" - --expected-sha "$GITHUB_SHA" + run: | + promotion_args=() + if [ "$PUBLISH_RELEASE" = true ]; then + promotion_args+=(--allow-promotion-commit) + fi + result="$( + /usr/bin/python3 -E -s \ + "$GITHUB_WORKSPACE/.github/scripts/check-calibration-release-lineage.py" \ + --repo "$GITHUB_WORKSPACE" \ + --expected-sha "$GITHUB_SHA" \ + "${promotion_args[@]}" + )" + jq -e '.status == "passed"' <<<"$result" >/dev/null + selection_commit="$(jq -r '.selection_commit' <<<"$result")" + selection_tree="$(jq -r '.selection_tree' <<<"$result")" + printf '%s' "$selection_commit" | grep -Eq '^[0-9a-f]{40}$' + printf '%s' "$selection_tree" | grep -Eq '^[0-9a-f]{40}$' + { + echo "selection_commit=$selection_commit" + echo "selection_tree=$selection_tree" + } >> "$GITHUB_OUTPUT" - name: Validate release authority env: @@ -185,8 +215,9 @@ jobs: set -euo pipefail entries=() - # The source gate proves a tree. When dev was already gated and promoted without - # changing the tree, re-running it for an hour cannot reach a different answer. + # The one source proof belongs to the frozen candidate. A tree-preserving promotion + # may reuse it, but a calibration-source proof cannot stand in for the generated + # constant-set child. release_tree="$(git rev-parse "$GITHUB_SHA^{tree}")" while IFS= read -r run_id; do head_sha="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" --jq .head_sha)" @@ -194,25 +225,38 @@ jobs: test "$(git rev-parse "$head_sha^{tree}")" = "$release_tree" || continue git merge-base --is-ancestor "$head_sha" "$GITHUB_SHA" || continue gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id/jobs?per_page=100" \ - --jq '.jobs[] | select(.name | endswith("full-source-gate")) | select(.conclusion == "success") | .id' \ + --jq '.jobs[] | select(.name == "full-source-gate" and .conclusion == "success") | .id' \ | grep -q . || continue + run_attempt="$( + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" --jq '.run_attempt' + )" + artifact_name="release-cell-prepublish-source-attempt-$run_attempt" + artifact_count="$( + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id/artifacts?per_page=100" \ + | jq --arg name "$artifact_name" \ + '[.artifacts[] | select(.name == $name and .expired == false)] | length' + )" + test "$artifact_count" = 1 || continue entries+=("source_behavior=$run_id:$head_sha") - echo "Reusing source proof from run $run_id (tree $release_tree)." + echo "Reusing frozen-candidate source proof and $artifact_name from run $run_id (tree $release_tree)." break done < <( gh api --paginate \ "repos/$GITHUB_REPOSITORY/actions/runs?status=completed&per_page=100" \ | jq -r --arg repo "$GITHUB_REPOSITORY" \ - '.workflow_runs[] | select(.path == ".github/workflows/source-proof.yml" and .head_repository.full_name == $repo and .conclusion == "success") | .id' + '.workflow_runs[] | select(.path == ".github/workflows/source-proof.yml" and .head_repository.full_name == $repo and .event == "workflow_dispatch" and .conclusion == "success") | .id' ) reuse="$(IFS=,; echo "${entries[*]:-}")" - echo "reuse=${reuse:--}" >> "$GITHUB_OUTPUT" - if [ -n "$reuse" ]; then - echo "source_proof_reused=true" >> "$GITHUB_OUTPUT" - else - echo "source_proof_reused=false" >> "$GITHUB_OUTPUT" - fi + test -n "$reuse" || { + echo "::error::The frozen candidate has no reusable full-source-gate. The release workflow will not start a broad proof." + exit 1 + } + + { + echo "reuse=$reuse" + echo "source_proof_reused=true" + } >> "$GITHUB_OUTPUT" - name: Prove the public marketplace install path if: inputs.publish_release @@ -267,15 +311,17 @@ jobs: source-proof: needs: preflight - # A completed gate for this exact tree is already authenticated evidence; the closeout - # consumes it through the reuse binding instead of re-running an hour of compilation. + # Preflight fails unless the one frozen-candidate source proof is reusable. This job is a + # structural DAG placeholder for closeout compatibility and must remain unreachable. if: needs.preflight.outputs.source_proof_reused != 'true' uses: ./.github/workflows/source-proof.yml with: ref: ${{ github.sha }} proof_key: release-${{ needs.preflight.outputs.version }} version: ${{ needs.preflight.outputs.version }} - emit_release_cells: true + # This job is deliberately unreachable. An empty digest makes any policy + # regression that reaches it fail before starting a second broad proof. + freeze_receipt_digest: "" packaged-proof: needs: preflight diff --git a/.github/workflows/source-proof.yml b/.github/workflows/source-proof.yml index 50f9f93df..e764b798f 100644 --- a/.github/workflows/source-proof.yml +++ b/.github/workflows/source-proof.yml @@ -1,8 +1,6 @@ name: Exact-head source proof on: - pull_request: - types: [labeled] workflow_call: inputs: ref: @@ -12,13 +10,12 @@ on: required: true type: string version: - required: false - default: "" + required: true + type: string + freeze_receipt_digest: + description: Digest of the exact-head release freeze status. + required: true type: string - emit_release_cells: - required: false - default: false - type: boolean workflow_dispatch: inputs: pr_number: @@ -29,14 +26,52 @@ on: description: Exact reviewed head SHA. The selected --ref, github.sha, and live PR head must match. required: true type: string + freeze_receipt_digest: + description: Existing successful freeze receipt digest. Leave empty when acceptance_only is true. + required: false + default: "" + type: string + version: + description: Release version whose source cell this accepted proof emits. + required: true + type: string + acceptance_only: + description: Execute only the hostile mutation and protected Windows native-probe freeze barrier. + required: false + default: false + type: boolean + acceptance_phase: + description: Candidate phase represented by an acceptance-only receipt. + required: false + default: frozen_candidate + type: choice + options: + - calibration_source + - frozen_candidate + support_prs_json: + description: JSON array of support PR numbers already merged into the release head. + required: false + default: "[]" + type: string + reusable_evidence_json: + description: JSON array naming evidence reusable by this exact head. + required: false + default: "[]" + type: string + invalidated_evidence_json: + description: JSON array naming evidence invalidated before this exact head. + required: false + default: "[]" + type: string permissions: - actions: read + actions: write contents: read pull-requests: read + statuses: write concurrency: - group: source-proof-${{ github.sha }}-${{ inputs.proof_key || inputs.pr_number || github.event.pull_request.number || github.ref }}-${{ github.event.action == 'labeled' && github.event.label.name || 'dispatch' }} + group: source-proof-${{ github.sha }}-${{ inputs.proof_key || inputs.pr_number || github.ref }} cancel-in-progress: true env: @@ -46,12 +81,13 @@ env: jobs: resolve: - if: github.event_name != 'pull_request' || (github.event.action == 'labeled' && github.event.label.name == 'review-accepted') runs-on: ubuntu-latest timeout-minutes: 10 outputs: ref: ${{ steps.resolve.outputs.ref }} reuse: ${{ steps.reuse.outputs.reuse }} + freeze_digest: ${{ steps.receipt.outputs.digest }} + freeze_artifact_name: ${{ steps.receipt.outputs.artifact_name }} steps: - name: Resolve trusted exact head id: resolve @@ -116,11 +152,85 @@ jobs: echo "ref=$CALLER_REF" >> "$GITHUB_OUTPUT" fi + - name: Checkout accepted source head + uses: actions/checkout@v5 + with: + ref: ${{ steps.resolve.outputs.ref }} + fetch-depth: 0 + + - name: Cancel superseded proof runs + id: cancel + shell: bash + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ steps.resolve.outputs.ref }} + run: | + set -euo pipefail + result="$( + node .github/scripts/release-freeze-barrier.mjs cancel-superseded \ + --repository "$GITHUB_REPOSITORY" \ + --commit "$HEAD_SHA" \ + --broad-workflow "Exact-head source proof" \ + --broad-workflow "Platform and integration proof" \ + --broad-workflow "Release" \ + --broad-workflow "Auto Release" + )" + echo "cancelled=$(jq -c '.cancelled' <<<"$result")" >> "$GITHUB_OUTPUT" + + - name: Record executable release freeze + id: receipt + if: ${{ inputs.acceptance_only }} + shell: bash + env: + CALLER_FREEZE_RECEIPT_DIGEST: ${{ inputs.freeze_receipt_digest }} + ACCEPTANCE_PHASE: ${{ inputs.acceptance_phase }} + CANCELLED_RUNS_JSON: ${{ steps.cancel.outputs.cancelled }} + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ steps.resolve.outputs.ref }} + INVALIDATED_EVIDENCE_JSON: ${{ inputs.invalidated_evidence_json }} + PR_NUMBER: ${{ inputs.pr_number }} + REUSABLE_EVIDENCE_JSON: ${{ inputs.reusable_evidence_json }} + SUPPORT_PRS_JSON: ${{ inputs.support_prs_json }} + run: | + set -euo pipefail + test -z "$CALLER_FREEZE_RECEIPT_DIGEST" || { + echo "::error::acceptance_only mints its receipt digest; callers must leave freeze_receipt_digest empty." + exit 1 + } + tree="$(git rev-parse 'HEAD^{tree}')" + node .github/scripts/release-freeze-barrier.mjs record-actions-receipt \ + --repository "$GITHUB_REPOSITORY" \ + --repo "$GITHUB_WORKSPACE" \ + --branch "$GITHUB_REF_NAME" \ + --commit "$HEAD_SHA" \ + --tree "$tree" \ + --release-pr "$PR_NUMBER" \ + --support-prs-json "$SUPPORT_PRS_JSON" \ + --reusable-evidence-json "$REUSABLE_EVIDENCE_JSON" \ + --invalidated-evidence-json "$INVALIDATED_EVIDENCE_JSON" \ + --cancelled-runs-json "$CANCELLED_RUNS_JSON" \ + --run-id "$GITHUB_RUN_ID" \ + --run-attempt "$GITHUB_RUN_ATTEMPT" \ + --phase "$ACCEPTANCE_PHASE" \ + --broad-workflow "Exact-head source proof" \ + --broad-workflow "Platform and integration proof" \ + --broad-workflow "Release" \ + --broad-workflow "Auto Release" \ + --output "$RUNNER_TEMP/release-freeze-receipt.json" \ + --github-output "$GITHUB_OUTPUT" + + - name: Upload executable release freeze receipt + if: ${{ inputs.acceptance_only }} + uses: actions/upload-artifact@v7.0.1 + with: + name: ${{ steps.receipt.outputs.artifact_name }} + path: ${{ runner.temp }}/release-freeze-receipt.json + if-no-files-found: error + retention-days: 30 + - name: Reuse a completed gate for this exact head id: reuse - # Only the label and dispatch paths. workflow_call always supplies `ref`, and the release - # chain requires full-source-gate to actually run. - if: inputs.ref == '' + if: ${{ !inputs.acceptance_only }} shell: bash env: GH_TOKEN: ${{ github.token }} @@ -134,18 +244,212 @@ jobs: --jq '.jobs[] | select(.name == "full-source-gate" and .conclusion == "success") | .id' \ | grep -q . then + run_attempt="$( + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" --jq '.run_attempt' + )" + artifact_name="release-cell-prepublish-source-attempt-$run_attempt" + artifact_count="$( + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id/artifacts?per_page=100" \ + | jq --arg name "$artifact_name" \ + '[.artifacts[] | select(.name == $name and .expired == false)] | length' + )" + test "$artifact_count" = 1 || continue reuse=true - echo "Reusing full-source-gate from run $run_id for exact head $HEAD_SHA." + echo "Reusing full-source-gate and $artifact_name from run $run_id for exact head $HEAD_SHA." break fi done < <( gh api --paginate \ "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$HEAD_SHA&status=completed&per_page=100" \ | jq -r --arg repo "$GITHUB_REPOSITORY" \ - '.workflow_runs[] | select(.path == ".github/workflows/source-proof.yml" and .head_repository.full_name == $repo and (.event == "pull_request" or .event == "workflow_dispatch") and .conclusion == "success") | .id' + '.workflow_runs[] | select(.path == ".github/workflows/source-proof.yml" and .head_repository.full_name == $repo and .event == "workflow_dispatch" and .conclusion == "success") | .id' ) echo "reuse=$reuse" >> "$GITHUB_OUTPUT" + - name: Require executable release freeze + if: ${{ !inputs.acceptance_only }} + shell: bash + env: + GH_TOKEN: ${{ github.token }} + FREEZE_RECEIPT_DIGEST: ${{ inputs.freeze_receipt_digest }} + HEAD_SHA: ${{ steps.resolve.outputs.ref }} + run: | + set -euo pipefail + printf '%s' "$FREEZE_RECEIPT_DIGEST" | grep -Eq '^[0-9a-f]{64}$' + tree="$( + gh api "repos/$GITHUB_REPOSITORY/git/commits/$HEAD_SHA" --jq '.tree.sha' + )" + node .github/scripts/release-freeze-barrier.mjs verify-status \ + --repository "$GITHUB_REPOSITORY" \ + --commit "$HEAD_SHA" \ + --tree "$tree" \ + --phase frozen_candidate \ + --receipt-digest "$FREEZE_RECEIPT_DIGEST" + + freeze-hostile-mutations: + name: freeze-hostile-mutations + if: inputs.acceptance_only + needs: resolve + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.resolve.outputs.ref }} + + - uses: actions/setup-node@v5 + with: + node-version: "24" + package-manager-cache: false + + - name: Install workflow policy dependencies + run: npm ci --ignore-scripts + + - name: Execute exact-head hostile mutation matrix + run: >- + node --test + .github/scripts/check-workflow-policy.test.mjs + .github/scripts/release-freeze-barrier.test.mjs + .github/scripts/cargo-build-artifacts.test.mjs + .github/scripts/candidate-archive-store.test.mjs + + freeze-windows-native-probe: + name: freeze-windows-native-probe + if: inputs.acceptance_only + needs: resolve + runs-on: [self-hosted, Windows, X64, codestory-vulkan] + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.resolve.outputs.ref }} + + - name: Run exact-head Windows native probe + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $probeRoot = Join-Path $env:RUNNER_TEMP ( + "codestory-cargo-hardlink-probe-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT" + ) + if (Test-Path -LiteralPath $probeRoot) { + throw "native probe root already exists: $probeRoot" + } + try { + cargo new --quiet --bin --name cargo-hardlink-probe $probeRoot + if ($LASTEXITCODE -ne 0) { + throw "cargo new failed" + } + $clock = [Diagnostics.Stopwatch]::StartNew() + $vswhere = Join-Path ${env:ProgramFiles(x86)} ( + "Microsoft Visual Studio/Installer/vswhere.exe" + ) + $visualStudio = & $vswhere -latest -products "*" ` + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -property installationPath + $vsDevCmd = Join-Path $visualStudio "Common7/Tools/VsDevCmd.bat" + $build = ( + "`"$vsDevCmd`" -arch=x64 -host_arch=x64 >nul " + + "&& cd /d `"$probeRoot`" && cargo build --release --quiet" + ) + & cmd.exe /d /s /c $build + if ($LASTEXITCODE -ne 0) { + throw "tiny Cargo release probe failed" + } + node --test .github/scripts/cargo-build-artifacts.test.mjs + if ($LASTEXITCODE -ne 0) { + throw "exact-head Windows artifact selector mutations failed" + } + $rootExe = Join-Path $probeRoot "target/release/cargo-hardlink-probe.exe" + $depsExe = Join-Path $probeRoot "target/release/deps/cargo_hardlink_probe.exe" + $identityScript = @' + const fs = require("node:fs"); + const [root, deps] = process.argv.slice(1); + const left = fs.statSync(root, { bigint: true }); + const right = fs.statSync(deps, { bigint: true }); + if ( + left.dev !== right.dev + || left.ino !== right.ino + || left.nlink !== 2n + || right.nlink !== 2n + ) { + throw new Error("Cargo release root/deps outputs are not one native two-link file"); + } + console.log(JSON.stringify({ + device: String(left.dev), + inode: String(left.ino), + nlink: String(left.nlink), + })); + '@ + node -e $identityScript $rootExe $depsExe + if ($LASTEXITCODE -ne 0) { + throw "Cargo native hardlink identity probe failed" + } + $clock.Stop() + if ($clock.Elapsed.TotalSeconds -ge 90) { + throw "native probe took $($clock.Elapsed.TotalSeconds) seconds" + } + "native_probe_seconds=$([Math]::Round($clock.Elapsed.TotalSeconds, 3))" + } finally { + Remove-Item -LiteralPath $probeRoot -Recurse -Force -ErrorAction SilentlyContinue + } + + freeze-acceptance: + name: freeze-acceptance + if: >- + always() && + inputs.acceptance_only && + needs.resolve.result == 'success' && + needs.freeze-hostile-mutations.result == 'success' && + needs.freeze-windows-native-probe.result == 'success' + needs: + - resolve + - freeze-hostile-mutations + - freeze-windows-native-probe + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.resolve.outputs.ref }} + + - name: Download executable release freeze receipt + uses: actions/download-artifact@v8.0.1 + with: + name: ${{ needs.resolve.outputs.freeze_artifact_name }} + path: ${{ runner.temp }}/release-freeze-receipt + + - name: Publish executable release freeze + shell: bash + env: + GH_TOKEN: ${{ github.token }} + FREEZE_RECEIPT_DIGEST: ${{ needs.resolve.outputs.freeze_digest }} + HEAD_SHA: ${{ needs.resolve.outputs.ref }} + ACCEPTANCE_PHASE: ${{ inputs.acceptance_phase }} + run: | + set -euo pipefail + tree="$(git rev-parse 'HEAD^{tree}')" + verified_digest="$( + node .github/scripts/release-freeze-barrier.mjs verify-file \ + --receipt "$RUNNER_TEMP/release-freeze-receipt/release-freeze-receipt.json" \ + --repository "$GITHUB_REPOSITORY" \ + --commit "$HEAD_SHA" \ + --tree "$tree" \ + --run-id "$GITHUB_RUN_ID" \ + --run-attempt "$GITHUB_RUN_ATTEMPT" \ + --phase "$ACCEPTANCE_PHASE" + )" + test "$verified_digest" = "$FREEZE_RECEIPT_DIGEST" || { + echo "::error::Downloaded acceptance receipt digest differs from the Actions-generated resolve output." + exit 1 + } + gh api \ + --method POST \ + "repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \ + -f state=success \ + -f "context=codestory/release-freeze/$FREEZE_RECEIPT_DIGEST" \ + -f "description=tree=$tree" \ + -f "target_url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + full-source-gate: name: full-source-gate needs: resolve @@ -153,7 +457,7 @@ jobs: # and cannot reach a different answer, which is what made re-labelling a PR expensive. Release # runs (workflow_call, which always supplies `ref`) never take this path: their chain requires # the job to execute. - if: needs.resolve.outputs.reuse != 'true' + if: ${{ !inputs.acceptance_only && needs.resolve.outputs.reuse != 'true' }} runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -399,7 +703,6 @@ jobs: cargo test --workspace --doc --locked - name: Emit authenticated source release cell - if: inputs.emit_release_cells shell: bash env: INPUT_VERSION: ${{ inputs.version }} @@ -419,7 +722,7 @@ jobs: --out target/release-cells/source_behavior.json - name: Upload authenticated source release cell - if: success() && inputs.emit_release_cells + if: success() uses: actions/upload-artifact@v7.0.1 with: name: release-cell-prepublish-source-attempt-${{ github.run_attempt }} @@ -430,7 +733,7 @@ jobs: retrieval-generalization: name: retrieval-generalization needs: resolve - if: needs.resolve.outputs.reuse != 'true' + if: ${{ !inputs.acceptance_only && needs.resolve.outputs.reuse != 'true' }} runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -452,7 +755,7 @@ jobs: windows-native-contracts: name: windows-native-contracts needs: resolve - if: needs.resolve.outputs.reuse != 'true' + if: ${{ !inputs.acceptance_only && needs.resolve.outputs.reuse != 'true' }} runs-on: windows-latest timeout-minutes: 15 env: diff --git a/.github/workflows/windows-vulkan-proof.yml b/.github/workflows/windows-vulkan-proof.yml index 07198f264..945d124fb 100644 --- a/.github/workflows/windows-vulkan-proof.yml +++ b/.github/workflows/windows-vulkan-proof.yml @@ -44,41 +44,6 @@ on: required: false default: false type: boolean - workflow_dispatch: - inputs: - version: - required: true - type: string - ref: - required: false - type: string - proof_key: - required: false - type: string - calibration_bundle_artifact: - required: false - default: "" - type: string - calibration_bundle_run_id: - required: false - default: "" - type: string - candidate_installed_proof: - description: Install and prove the exact package through the candidate-managed launcher boundary. - required: false - default: false - type: boolean - candidate_producer_workflow_path: - description: Top-level workflow path authenticated as the candidate artifact producer. - required: false - default: ".github/workflows/windows-vulkan-proof.yml" - type: string - server_behavior_only: - description: Prove bounded package retrieval readiness without answer-quality or performance claims. - required: false - default: false - type: boolean - permissions: actions: read contents: read diff --git a/AGENTS.md b/AGENTS.md index 395f8e149..308cf6318 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,8 +120,8 @@ adapter to compensate for incorrect upstream state. lanes. - Do not use `cargo test --workspace --all-targets` as the routine broad gate; it expands Criterion targets. Draft work uses focused checks. The full - workspace test and all-target/all-feature clippy gate run once on an - independently accepted exact head. + workspace test and all-target/all-feature clippy gate run once on the source + head accepted by the executable release freeze barrier. - CLI integration tests must launch through `tests/test_support::cli_command` or its supplied-binary variant, use isolated cache/install/plugin state roots. @@ -160,6 +160,9 @@ adapter to compensate for incorrect upstream state. saga label) must close a PR-sized issue with `Closes`, `Fixes`, or `Resolves`. Use `Refs` for broader parents. A partial slice closes only its child issue; keep the parent open until its acceptance criteria are met. +- Before creating an issue, branch, worktree, or PR, search open and closed + issues, merged PRs, and integration history for the requested outcome, then + prove that outcome is absent from the current integration head. - For PRs targeting `dev/codestory-next`, add both the issue and PR to the Project; computed linked-PR fields may not populate before default-branch promotion. @@ -170,6 +173,9 @@ adapter to compensate for incorrect upstream state. - PRs should explain context, what changed, how to review, verification, risk, and follow-up. Include exact SHAs and distinguish completed proof from non-claims. +- Release handoffs must name the final intended source head, known future + source changes, proof-triggering labels or actions, reusable and invalidated + evidence, currently running workflows, and the next permitted mutation. - Public GitHub status comments must use `node scripts/github-status-comment.mjs --issue --body-file ` or stdin; the helper rejects literal `\\n` text. @@ -186,6 +192,44 @@ adapter to compensate for incorrect upstream state. ## Release Rules +### Candidate freeze and proof budget + +- Before any gate expected to exceed five minutes, record the exact commit and + tree, confirm the worktree is clean and pushed, and confirm that every + planned source or workflow change is already merged. Independent acceptance + must execute the required hostile mutations on that exact head; diff review + and existing green tests do not qualify. Any later commit revokes + acceptance. +- Support PRs use focused checks only. Do not add a proof-triggering label or + dispatch a broad source, package, calibration, or hardware gate until all + support PRs are integrated into the release lane. Broad proof belongs to the + final integration head, not every independently mergeable PR. +- Release order is: merge all blockers, run focused checks, run actual-host + microprobes, execute hostile mutation acceptance, push and declare the source + head frozen for calibration, calibrate, apply the sole generated constant-set + change, accept and freeze that generated head, run one broad source proof on + it, then qualify. If another source or workflow change becomes necessary, + immediately invalidate the candidate and cancel every queued or running + proof for it. +- Run the full workspace source proof exactly once per release candidate, on + the generated constant-only frozen head after calibration. The calibration + source receives focused hostile-mutation and native-probe acceptance, not a + broad source proof. Use deterministic selection validation, direct + constant-only lineage verification, and frozen-candidate qualification; + never run both a pre-calibration and post-calibration workspace proof. +- Cancel a run whose head is no longer the intended release candidate. Never + let an expensive obsolete run finish for information. Before dispatching, + inspect both in-flight runs and whether any known source change will + invalidate the result. +- After a platform-specific packaging or filesystem failure, do not run a full + rebuild until a sub-90-second native probe reproduces the relevant path, + link, staging, cache, or identity behavior on that operating system. Test the + selector against the probe or captured artifact first. +- Use one implementer and one adversarial verifier. Give the verifier the exact + mutation matrix and only the context needed to execute it. Its output is + limited to counterexamples or acceptance evidence. After two failed + revisions of the same shape, stop patching examples and redesign the seam. + - Freeze the selected release claim before qualification. For the standard v0.16 release described in `CHANGELOG.md`, build one candidate; install its exact archives on Apple Silicon macOS, Windows x64, and Linux x64; complete diff --git a/benchmarks/release-evidence/fixtures/candidate.json b/benchmarks/release-evidence/fixtures/candidate.json index c8c6a5b49..28f6da4e3 100644 --- a/benchmarks/release-evidence/fixtures/candidate.json +++ b/benchmarks/release-evidence/fixtures/candidate.json @@ -62,7 +62,7 @@ }, "release_claims": { "graph_schema": "codestory.release-claims/v1", - "graph_sha256": "7385b32924d42e94c14b8d1f41fcec50bded200fc0e65fec05f43036e2896087", + "graph_sha256": "9df8f932d66a60baa104bf61252ab318b0c1325740a0edf193463618e0946348", "observed_at": "2026-07-21T02:13:20.738Z", "expires_at": "2026-07-22T02:13:20.738Z", "requested_claims": [ @@ -85,7 +85,7 @@ "type": "performance", "tier": "live_behavior", "status": "measured", - "graph_sha256": "7385b32924d42e94c14b8d1f41fcec50bded200fc0e65fec05f43036e2896087", + "graph_sha256": "9df8f932d66a60baa104bf61252ab318b0c1325740a0edf193463618e0946348", "observed_at": "2026-07-21T02:13:20.738Z", "expires_at": "2026-07-22T02:13:20.738Z", "identity": { @@ -106,7 +106,7 @@ "type": "answer_quality", "tier": "answer_quality", "status": "pass", - "graph_sha256": "7385b32924d42e94c14b8d1f41fcec50bded200fc0e65fec05f43036e2896087", + "graph_sha256": "9df8f932d66a60baa104bf61252ab318b0c1325740a0edf193463618e0946348", "observed_at": "2026-07-21T02:13:20.738Z", "expires_at": "2026-07-22T02:13:20.738Z", "identity": { diff --git a/benchmarks/release-evidence/fixtures/report.json b/benchmarks/release-evidence/fixtures/report.json index 7a0d10f04..ccd2f3014 100644 --- a/benchmarks/release-evidence/fixtures/report.json +++ b/benchmarks/release-evidence/fixtures/report.json @@ -7,7 +7,7 @@ "baseline_id": "ci-contract-v1@1111111111111111111111111111111111111111", "baseline_sha256": "0bbbe6dd8b4000151edf7b1270959d08e94e08db876e7f2372b25613e0f237c1", "candidate_path": "benchmarks/release-evidence/fixtures/candidate.json", - "candidate_sha256": "e30832aa8c4d39b07835a3484bcb8d0573904b68fe62b16752c86ff2d740ae70", + "candidate_sha256": "5302ac5856891447183a0c0e7c7df09cfc5ad425efb1b834b2b439364870f438", "artifact_paths": [ { "path": "candidate-stats.json", @@ -26,7 +26,7 @@ "type": "performance", "tier": "live_behavior", "status": "pass", - "graph_sha256": "7385b32924d42e94c14b8d1f41fcec50bded200fc0e65fec05f43036e2896087", + "graph_sha256": "9df8f932d66a60baa104bf61252ab318b0c1325740a0edf193463618e0946348", "observed_at": "2026-07-21T02:13:20.738Z", "expires_at": "2026-07-22T02:13:20.738Z", "identity": { @@ -47,7 +47,7 @@ "type": "answer_quality", "tier": "answer_quality", "status": "pass", - "graph_sha256": "7385b32924d42e94c14b8d1f41fcec50bded200fc0e65fec05f43036e2896087", + "graph_sha256": "9df8f932d66a60baa104bf61252ab318b0c1325740a0edf193463618e0946348", "observed_at": "2026-07-21T02:13:20.738Z", "expires_at": "2026-07-22T02:13:20.738Z", "identity": { @@ -69,7 +69,7 @@ "schema": "codestory.release-claim-evaluation/v1", "status": "pass", "graph_schema": "codestory.release-claims/v1", - "graph_sha256": "7385b32924d42e94c14b8d1f41fcec50bded200fc0e65fec05f43036e2896087", + "graph_sha256": "9df8f932d66a60baa104bf61252ab318b0c1325740a0edf193463618e0946348", "evidence_selection": "all_matching_rows_must_pass", "expected_commit": "2222222222222222222222222222222222222222", "evaluated_at": "2026-07-21T02:13:20.738Z", diff --git a/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/measurements.rs b/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/measurements.rs index aa12723f3..551b09332 100644 --- a/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/measurements.rs +++ b/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/measurements.rs @@ -10,7 +10,7 @@ use super::analysis::{ }; use super::process::{ busy_retry_marker_timeout, busy_retry_worker_timeout, measurement_worker_timeout, - query_parameters, require_worker_success, + query_parameters, }; use super::{ RunningWorker, ScenarioRunner, WorkerOutput, opaque_constant_calibration_sample_id, push_metric, @@ -71,7 +71,7 @@ pub(super) fn declared_phase_boundaries(metric: &str) -> Result<[&'static str; 2 ], "busy_retry_usefulness" => ["typed_retry_emitted", "named_retry_condition_became_true"], "true_idle_exit" => [ - "last_queued_active_or_leased_work_ended", + "final_product_request_completed", "engine_and_server_absent", ], "backend_observed_accelerator_residency" => [ @@ -94,7 +94,7 @@ pub(super) fn declared_workload_id(metric: &str) -> Result<&'static str> { "warm_bulk_ipc" => "warm_bulk_64x256b_v1", "bulk_documents_per_second" | "bulk_tokens_per_second" => "bulk_throughput_256x256b_v1", "busy_retry_usefulness" => "saturated_query_65th_retry_v1", - "true_idle_exit" => "true_idle_60000_awake_ms_v1", + "true_idle_exit" => "true_idle_after_product_completion_60000_awake_ms_v2", "backend_observed_accelerator_residency" => "resident_policy_identity_v1", _ => bail!("embedding_qualification_metric_workload_unknown:{metric}"), }) @@ -556,14 +556,6 @@ impl<'a> ScenarioRunner<'a> { )?; } - let idle_worker = self.spawn_worker("query", query_parameters(1), None)?; - let idle_output = self.finish_worker(idle_worker, measurement_worker_timeout("query"))?; - require_worker_success(&idle_output, "true_idle_owner")?; - let idle_owner = - self.record_worker_snapshot("measurement_true_idle_owner", &idle_output)?; - if !snapshot_has_resident_generation(&idle_owner) { - bail!("embedding_qualification_true_idle_owner_not_resident"); - } let measured = self.run_measure_worker( "measure_true_idle", "true_idle_exit", @@ -846,7 +838,7 @@ fn validate_constant_engine_evidence( || identity.load_error.is_some() || identity.policy != "accelerated" || identity.backend.eq_ignore_ascii_case("cpu") - || !identity.backend.eq_ignore_ascii_case(expected_backend) + || !constant_backend_matches_expected(&identity.backend, expected_backend) || identity.model_digest != expected_model_sha256 || identity.materialized_model_sha256 != expected_model_sha256 || !identity.embedded_model @@ -857,6 +849,29 @@ fn validate_constant_engine_evidence( Ok(()) } +fn constant_backend_matches_expected(observed: &str, expected: &str) -> bool { + let Some(expected_family) = constant_backend_family(expected) else { + return false; + }; + constant_backend_family(observed) == Some(expected_family) +} + +fn constant_backend_family(value: &str) -> Option<&'static str> { + let normalized = value.trim().to_ascii_lowercase(); + match normalized.as_str() { + "metal" | "mtl" => Some("metal"), + "vulkan" => Some("vulkan"), + value + if value.strip_prefix("vulkan").is_some_and(|suffix| { + !suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit()) + }) => + { + Some("vulkan") + } + _ => None, + } +} + fn expected_materialization_reuse(run_index: u32) -> Result { match run_index { 1 => Ok(false), @@ -1093,11 +1108,42 @@ mod constant_calibration_tests { fn engine_precondition_binds_accelerated_backend_model_and_reuse_only() { let server = server_identity("server-a"); let metal = engine_identity("server-a", "Metal", false); - validate_constant_engine_evidence(&metal, &server, "metal", &"a".repeat(64), false) - .expect("Metal identity"); - let vulkan = engine_identity("server-a", "Vulkan", false); - validate_constant_engine_evidence(&vulkan, &server, "vulkan", &"a".repeat(64), false) - .expect("Vulkan identity"); + for (observed, expected) in [ + ("Metal", "metal"), + ("MTL", "metal"), + ("Vulkan", "vulkan"), + ("Vulkan0", "vulkan"), + ] { + let identity = engine_identity("server-a", observed, false); + validate_constant_engine_evidence(&identity, &server, expected, &"a".repeat(64), false) + .unwrap_or_else(|error| { + panic!("{observed} must satisfy expected GPU family {expected}: {error}") + }); + } + for (observed, expected) in [ + ("CPU", "metal"), + ("cpu_explicit", "metal"), + ("", "metal"), + ("unknown", "metal"), + ("metal-cpu", "metal"), + ("mtl0", "metal"), + ("MTL", "vulkan"), + ("Vulkan", "metal"), + ("vulkan-cpu", "vulkan"), + ] { + let identity = engine_identity("server-a", observed, false); + assert!( + validate_constant_engine_evidence( + &identity, + &server, + expected, + &"a".repeat(64), + false, + ) + .is_err(), + "{observed} must not satisfy expected GPU family {expected}" + ); + } let mut invalid = metal.clone(); invalid.policy = "cpu_explicit".into(); diff --git a/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/process.rs b/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/process.rs index 779aeedd8..477bb6eea 100644 --- a/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/process.rs +++ b/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/process.rs @@ -1,6 +1,4 @@ -use super::super::{ - CONTROL_TIMEOUT, ControlEvent, IDLE_EXIT_GRACE, POLL, QUEUE_SETUP_TIMEOUT, SNAPSHOT_TIMEOUT, -}; +use super::super::{CONTROL_TIMEOUT, ControlEvent, POLL, QUEUE_SETUP_TIMEOUT, SNAPSHOT_TIMEOUT}; use super::analysis::elapsed; use super::{ EMBEDDING_QUALIFICATION_WORKER_SCHEMA_VERSION, ProcessInvocation, RunningWorker, WorkerOutput, @@ -17,6 +15,8 @@ use std::path::{Path, PathBuf}; use std::process::{Child, ExitStatus}; use std::time::Duration; +pub(super) const MEASUREMENT_OWNER_ABSENCE_GRACE: Duration = Duration::from_secs(30); + pub(super) fn existing_control_events(directory: &Path) -> Result> { existing_control_events_for_nonce(directory, &qualification_nonce()?) } @@ -291,12 +291,17 @@ pub(super) fn stall_worker_timeout() -> Duration { pub(super) fn measurement_worker_timeout(operation: &str) -> Duration { let budgets = EmbeddingClientBudgets::current(); if operation == "measure_true_idle" { - // The idle worker first proves the resident owner quiescent (bounded - // by the snapshot allowance), then waits out the server's own idle - // deadline plus the exit grace before the absence observation. - return Duration::from_millis(PER_USER_EMBEDDING_SERVER_IDLE_TIMEOUT_MS) - .saturating_add(IDLE_EXIT_GRACE) - .saturating_add(SNAPSHOT_TIMEOUT) + // The idle worker runs the product request that starts the measured + // idle epoch itself, then waits out the server's idle deadline plus + // the exit grace before the absence observation. + return budgets + .connect + .saturating_add(budgets.spawn) + .saturating_add(budgets.query_request) + .saturating_add(Duration::from_millis( + PER_USER_EMBEDDING_SERVER_IDLE_TIMEOUT_MS, + )) + .saturating_add(MEASUREMENT_OWNER_ABSENCE_GRACE) .saturating_add(SNAPSHOT_TIMEOUT) .saturating_add(CONTROL_TIMEOUT); } diff --git a/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/tests.rs b/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/tests.rs index 2d344c217..f08625dd6 100644 --- a/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/tests.rs +++ b/crates/codestory-bench/src/bin/codestory_embedding_qualification/scenarios/artifact/runner/tests.rs @@ -7,9 +7,9 @@ use super::measurements::{ declared_phase_boundaries, declared_workload_id, measurement_span_interval, }; use super::process::{ - LOAD_ESTABLISHMENT_WAITS, busy_retry_worker_timeout, dead_client_setup_timeout, - load_establishment_budget, load_establishment_timeout, measurement_worker_timeout, - published_control_events, + LOAD_ESTABLISHMENT_WAITS, MEASUREMENT_OWNER_ABSENCE_GRACE, busy_retry_worker_timeout, + dead_client_setup_timeout, load_establishment_budget, load_establishment_timeout, + measurement_worker_timeout, published_control_events, }; use super::{ScenarioEvidence, WorkerOutput, opaque_measurement_sample_id}; use crate::qualification::request::{QualificationContracts, REQUIRED_METRICS, REQUIRED_SCENARIOS}; @@ -76,15 +76,26 @@ fn measurement_worker_budgets_dominate_the_deadlines_workers_honor() { measurement_worker_timeout("measure_resident_identity"), "the scenario residency probe must carry the same bulk-deadline budget as the residency measurement" ); - // The true-idle measurement worker waits out the server's own idle - // deadline before the absence observation; its watchdog must dominate - // that self-enforced wait plus its quiescence and absence-grace waits. + // The true-idle measurement worker now performs the product request that + // starts the idle epoch itself; its watchdog must cover that whole client + // chain before the server idle deadline and absence-grace wait. + assert_eq!( + MEASUREMENT_OWNER_ABSENCE_GRACE, + Duration::from_secs(30), + "the coordinator must mirror the measurement worker's owner-absence grace" + ); assert!( measurement_worker_timeout("measure_true_idle") - >= Duration::from_millis(PER_USER_EMBEDDING_SERVER_IDLE_TIMEOUT_MS) - .saturating_add(Duration::from_secs(30)) + >= budgets + .connect + .saturating_add(budgets.spawn) + .saturating_add(budgets.query_request) + .saturating_add(Duration::from_millis( + PER_USER_EMBEDDING_SERVER_IDLE_TIMEOUT_MS, + )) + .saturating_add(MEASUREMENT_OWNER_ABSENCE_GRACE) .saturating_add(SNAPSHOT_TIMEOUT), - "true-idle measurement budget must dominate the server idle deadline plus the worker's own waits" + "true-idle measurement budget must dominate its product request, server idle deadline, and worker waits" ); // The busy-retry worker seeds the held queues (queue-setup phase), then // after release drains queries bounded by its own 120s per-request diff --git a/crates/codestory-cli/src/embedding_qualification/worker.rs b/crates/codestory-cli/src/embedding_qualification/worker.rs index 290d0942e..0f871153e 100644 --- a/crates/codestory-cli/src/embedding_qualification/worker.rs +++ b/crates/codestory-cli/src/embedding_qualification/worker.rs @@ -237,7 +237,11 @@ fn run_measure_operation( &request.parameters, ), "measure_resident_identity" => run_measure_resident_identity(runtime, clock.as_ref()), - "measure_true_idle" => run_measure_true_idle(runtime, clock.as_ref()), + "measure_true_idle" => run_measure_true_idle( + &PerUserEmbeddingClient::for_runtime(runtime)?, + clock.as_ref(), + request.parameters.input_bytes, + ), "measure_busy_retry" => { let marker = request .retry_marker diff --git a/crates/codestory-cli/src/embedding_qualification/worker/operations/measure.rs b/crates/codestory-cli/src/embedding_qualification/worker/operations/measure.rs index 160b13c34..2f7b6927b 100644 --- a/crates/codestory-cli/src/embedding_qualification/worker/operations/measure.rs +++ b/crates/codestory-cli/src/embedding_qualification/worker/operations/measure.rs @@ -19,7 +19,7 @@ use super::super::protocol::{ run_raw_protocol_exchange_with_input, validated_hello, write_protocol_frame, }; use super::ANTI_IDLE_PROTOCOL_DEADLINE_MS; -use super::owner_exit::{observe_owner_exit, wait_for_owner_exit}; +use super::owner_exit::{OwnerExitObservation, observe_owner_exit, wait_for_owner_exit}; use super::queue::{QueueOperation, run_queue_operation}; use anyhow::{Context, Result, bail}; use codestory_retrieval::{ @@ -68,7 +68,7 @@ fn workload_documents(workload_id: &str, repeat: u32, count: usize, bytes: usize .collect() } -struct MeasurementSpanStart { +pub(in crate::embedding_qualification::worker) struct MeasurementSpanStart { awake_started_ns: u64, inclusive_started_ns: u64, boot_id_started: String, @@ -88,6 +88,26 @@ fn begin_span(clock: &dyn AwakeMonotonicClock) -> Result { }) } +/// Stamp product completion for `true_idle_exit`. +/// +/// The awake reading is deliberately first: once the product operation has +/// stamped completion, a delay while returning to the measurement coordinator +/// must remain inside the measured idle interval, never move its start +/// forward. The suspend-inclusive and boot witnesses follow immediately and +/// the downstream tolerance fails closed if sampling them is delayed. +fn begin_true_idle_span_at_product_completion( + clock: &dyn AwakeMonotonicClock, +) -> Result { + let awake_started_ns = clock.now_ns(); + let inclusive_started_ns = crate::embedding_server_transport::inclusive_now_ns()?; + let boot_id_started = crate::embedding_server_transport::boot_id()?; + Ok(MeasurementSpanStart { + awake_started_ns, + inclusive_started_ns, + boot_id_started, + }) +} + /// Stamp the declared end instant: awake reading first, then the /// suspend-inclusive reading and boot id. fn finish_span( @@ -416,30 +436,103 @@ pub(in crate::embedding_qualification::worker) fn run_measure_resident_identity( }) } -/// `true_idle_exit`: span start at the observation proving the resident owner -/// carries zero queued, active, or leased work -/// (`last_queued_active_or_leased_work_ended`), span end at the observation -/// that returned no owner (`engine_and_server_absent`). +fn true_idle_scheduler_is_drained( + active_request_count: u64, + query_depth: u64, + bulk_depth: u64, + lease_count: u64, +) -> bool { + active_request_count == 0 && query_depth == 0 && bulk_depth == 0 && lease_count == 0 +} + +fn snapshot_is_true_idle_boundary(snapshot: &EmbeddingServerSnapshot) -> bool { + snapshot.lifecycle == "resident" + && resident_engine_generation(snapshot) + && true_idle_scheduler_is_drained( + snapshot.scheduler.active_request_count, + snapshot.scheduler.query_depth, + snapshot.scheduler.bulk_depth, + snapshot.scheduler.lease_count, + ) +} + +fn same_true_idle_owner( + expected_server_instance_id: &str, + observed_server_instance_id: &str, +) -> bool { + expected_server_instance_id == observed_server_instance_id +} + +fn validate_true_idle_boundary( + expected_server_instance_id: &str, + snapshot: &EmbeddingServerSnapshot, +) -> Result<()> { + if !same_true_idle_owner( + expected_server_instance_id, + &snapshot.process.server_instance_id, + ) { + bail!("embedding_qualification_true_idle_owner_changed"); + } + if !snapshot_is_true_idle_boundary(snapshot) { + bail!("embedding_qualification_true_idle_not_quiescent"); + } + Ok(()) +} + +/// The product and observation operations used by the true-idle measurement. +/// +/// The worker implements this interface with the real per-user embedding +/// client. Tests use a scripted implementation to execute this same +/// `run_measure_true_idle` algorithm rather than a helper that production can +/// bypass. +pub(in crate::embedding_qualification::worker) trait TrueIdleMeasurementClient { + fn observe_snapshot(&self) -> Result>; + fn complete_product_query_and_stamp( + &self, + input: &str, + clock: &dyn AwakeMonotonicClock, + ) -> Result; + fn observe_owner_exit(&self) -> Result; +} + +impl TrueIdleMeasurementClient for PerUserEmbeddingClient { + fn observe_snapshot(&self) -> Result> { + self.observe() + } + + fn complete_product_query_and_stamp( + &self, + input: &str, + clock: &dyn AwakeMonotonicClock, + ) -> Result { + let _ = self.embed_query(input)?; + begin_true_idle_span_at_product_completion(clock) + } + + fn observe_owner_exit(&self) -> Result { + observe_owner_exit(self) + } +} + +/// `true_idle_exit`: this worker completes the last product request itself, +/// stamps `final_product_request_completed` before returning to its caller, +/// proves the resident scheduler is drained, then ends at the observation that +/// returned no owner (`engine_and_server_absent`). pub(in crate::embedding_qualification::worker) fn run_measure_true_idle( - runtime: &SidecarRuntimeConfig, + client: &dyn TrueIdleMeasurementClient, clock: &dyn AwakeMonotonicClock, + input_bytes: u32, ) -> Result { - let client = PerUserEmbeddingClient::for_runtime(runtime)?; - let idle_owner = wait_for_observed_snapshot( - &client, - clock, - SNAPSHOT_TIMEOUT, - "embedding_qualification_true_idle_not_quiescent", - |snapshot| { - snapshot.lifecycle == "resident" - && resident_engine_generation(snapshot) - && snapshot.scheduler.active_request_count == 0 - && snapshot.scheduler.query_depth == 0 - && snapshot.scheduler.bulk_depth == 0 - && snapshot.scheduler.lease_count == 0 - }, - )?; - let start = begin_span(clock)?; + let expected_owner = client + .observe_snapshot()? + .filter(resident_engine_generation) + .ok_or_else(|| anyhow::anyhow!("embedding_qualification_true_idle_owner_missing"))?; + let input = "q".repeat(input_bytes.max(1) as usize); + let start = client.complete_product_query_and_stamp(&input, clock)?; + let idle_owner = client + .observe_snapshot()? + .ok_or_else(|| anyhow::anyhow!("embedding_qualification_true_idle_owner_missing"))?; + validate_true_idle_boundary(&expected_owner.process.server_instance_id, &idle_owner)?; let timeout = Duration::from_millis(PER_USER_EMBEDDING_SERVER_IDLE_TIMEOUT_MS) .saturating_add(OWNER_ABSENCE_GRACE); let wait_started = clock.now_ns(); @@ -448,7 +541,7 @@ pub(in crate::embedding_qualification::worker) fn run_measure_true_idle( wait_started, timeout, &idle_owner.process.server_instance_id, - || observe_owner_exit(&client), + || client.observe_owner_exit(), )?; let span = finish_span(clock, start)?; Ok(measurement(span, idle_owner)) @@ -718,7 +811,191 @@ fn validate_vector_response( #[cfg(test)] mod tests { - use super::workload_input; + use super::super::owner_exit::OwnerExitObservation; + use super::{ + MeasurementSpanStart, TrueIdleMeasurementClient, + begin_true_idle_span_at_product_completion, run_measure_true_idle, + true_idle_scheduler_is_drained, validate_true_idle_boundary, workload_input, + }; + use anyhow::Result; + use codestory_retrieval::{ + AwakeMonotonicClock, EmbeddingServerAuthoritySnapshot, EmbeddingServerClockSnapshot, + EmbeddingServerEngineSnapshot, EmbeddingServerProcessSnapshot, + EmbeddingServerProtocolSnapshot, EmbeddingServerSchedulerSnapshot, EmbeddingServerSnapshot, + PER_USER_EMBEDDING_SERVER_SNAPSHOT_SCHEMA_VERSION, + }; + use std::collections::VecDeque; + use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + const PRODUCT_COMPLETED_NS: u64 = 100; + const PRODUCT_RETURNED_NS: u64 = 10_000_000_100; + const DRAINED_OWNER_OBSERVED_NS: u64 = 10_000_000_145; + + struct ScriptClock { + now_ns: AtomicU64, + events: Arc>>, + } + + impl ScriptClock { + fn new(events: Arc>>) -> Self { + Self { + now_ns: AtomicU64::new(0), + events, + } + } + + fn set(&self, now_ns: u64) { + self.now_ns.store(now_ns, Ordering::Release); + } + } + + impl AwakeMonotonicClock for ScriptClock { + fn now_ns(&self) -> u64 { + let now_ns = self.now_ns.load(Ordering::Acquire); + if now_ns == PRODUCT_COMPLETED_NS { + self.events + .lock() + .expect("lock events") + .push("span_started"); + } + now_ns + } + + fn sleep(&self, duration: Duration) { + self.now_ns.fetch_add( + u64::try_from(duration.as_nanos()).expect("test duration fits u64"), + Ordering::AcqRel, + ); + } + + fn snapshot(&self) -> EmbeddingServerClockSnapshot { + EmbeddingServerClockSnapshot { + domain: "awake_monotonic".into(), + api: "script_clock".into(), + boot_id: "test-boot".into(), + resolution_ns: 1, + } + } + } + + struct ScriptedTrueIdleClient { + clock: Arc, + events: Arc>>, + snapshots: Mutex>, + exit_observations: Mutex>, + product_calls: AtomicUsize, + } + + impl TrueIdleMeasurementClient for ScriptedTrueIdleClient { + fn observe_snapshot(&self) -> Result> { + let (snapshot, remaining) = { + let mut snapshots = self.snapshots.lock().expect("lock snapshots"); + let snapshot = snapshots + .pop_front() + .expect("the algorithm observed past its snapshot script"); + (snapshot, snapshots.len()) + }; + let event = if remaining == 1 { + "expected_owner_observed" + } else { + self.clock.set(DRAINED_OWNER_OBSERVED_NS); + "same_owner_drained_observed" + }; + self.events.lock().expect("lock events").push(event); + Ok(Some(snapshot)) + } + + fn complete_product_query_and_stamp( + &self, + input: &str, + clock: &dyn AwakeMonotonicClock, + ) -> Result { + assert_eq!(input, "q".repeat(256)); + self.product_calls.fetch_add(1, Ordering::AcqRel); + self.clock.set(PRODUCT_COMPLETED_NS); + self.events + .lock() + .expect("lock events") + .push("product_completed"); + let start = begin_true_idle_span_at_product_completion(clock)?; + self.clock.set(PRODUCT_RETURNED_NS); + self.events + .lock() + .expect("lock events") + .push("product_returned_after_delay"); + Ok(start) + } + + fn observe_owner_exit(&self) -> Result { + let observation = self + .exit_observations + .lock() + .expect("lock exit observations") + .pop_front() + .expect("the algorithm observed past its exit script"); + let event = match &observation { + OwnerExitObservation::Present(_) => "same_owner_still_present", + OwnerExitObservation::Lost => "owner_connection_lost", + OwnerExitObservation::Absent => "owner_absence_observed", + }; + self.events.lock().expect("lock events").push(event); + Ok(observation) + } + } + + fn true_idle_snapshot( + owner: &str, + active_request_count: u64, + query_depth: u64, + bulk_depth: u64, + lease_count: u64, + ) -> EmbeddingServerSnapshot { + EmbeddingServerSnapshot { + schema_version: PER_USER_EMBEDDING_SERVER_SNAPSHOT_SCHEMA_VERSION, + event_sequence: 1, + lifecycle: "resident".into(), + clock: EmbeddingServerClockSnapshot { + domain: "awake_monotonic".into(), + api: "script_clock".into(), + boot_id: "test-boot".into(), + resolution_ns: 1, + }, + protocol: EmbeddingServerProtocolSnapshot::current(), + authority: EmbeddingServerAuthoritySnapshot { + endpoint_namespace_id: "endpoint".into(), + lifetime_authority_id: "authority".into(), + listener_id: "listener".into(), + peer_verified: true, + }, + process: EmbeddingServerProcessSnapshot { + server_instance_id: owner.into(), + pid: 42, + process_start_id: "server-start".into(), + executable_sha256: "a".repeat(64), + executable_version: "0.16.3".into(), + }, + scheduler: EmbeddingServerSchedulerSnapshot { + query_capacity: 64, + query_depth, + bulk_capacity: 64, + bulk_depth, + connection_count: 1, + active_request_count, + lease_count, + active_request: None, + }, + engine: Some(EmbeddingServerEngineSnapshot { + engine_owner_id: owner.into(), + native_worker_id: "native-worker".into(), + load_generation: 1, + model_load_count: 1, + successful_encode_count: 1, + }), + failure: None, + } + } #[test] fn workload_inputs_are_deterministic_ascii_and_distinct_per_ordinal() { @@ -732,4 +1009,88 @@ mod tests { assert_ne!(first, other_repeat); assert_ne!(first, other_ordinal); } + + #[test] + fn true_idle_start_is_stamped_at_product_completion_before_return_and_observation_lag() { + let events = Arc::new(Mutex::new(Vec::new())); + let clock = Arc::new(ScriptClock::new(Arc::clone(&events))); + let client = ScriptedTrueIdleClient { + clock: Arc::clone(&clock), + events: Arc::clone(&events), + snapshots: Mutex::new(VecDeque::from([ + true_idle_snapshot("measured-owner", 0, 0, 0, 0), + true_idle_snapshot("measured-owner", 0, 0, 0, 0), + ])), + exit_observations: Mutex::new(VecDeque::from([ + OwnerExitObservation::Present("measured-owner".into()), + OwnerExitObservation::Absent, + ])), + product_calls: AtomicUsize::new(0), + }; + + let measurement = + run_measure_true_idle(&client, clock.as_ref(), 256).expect("measure true idle"); + + assert_eq!(client.product_calls.load(Ordering::Acquire), 1); + assert_eq!(measurement.span.awake_started_ns, PRODUCT_COMPLETED_NS); + assert!(measurement.span.awake_finished_ns >= DRAINED_OWNER_OBSERVED_NS); + assert_eq!( + measurement.snapshot.process.server_instance_id, + "measured-owner" + ); + assert_eq!( + *events.lock().expect("lock events"), + [ + "expected_owner_observed", + "product_completed", + "span_started", + "product_returned_after_delay", + "same_owner_drained_observed", + "same_owner_still_present", + "owner_absence_observed", + ] + ); + } + + #[test] + fn true_idle_boundary_rejects_each_queued_active_or_leased_shape() { + let occupied = [ + ("active", (1, 0, 0, 0)), + ("query", (0, 1, 0, 0)), + ("bulk", (0, 0, 1, 0)), + ("lease", (0, 0, 0, 1)), + ]; + assert!(true_idle_scheduler_is_drained(0, 0, 0, 0)); + for (label, state) in occupied { + assert!( + !true_idle_scheduler_is_drained(state.0, state.1, state.2, state.3), + "{label} work must keep the true-idle boundary open" + ); + let result = validate_true_idle_boundary( + "measured-owner", + &true_idle_snapshot("measured-owner", state.0, state.1, state.2, state.3), + ); + assert_eq!( + result + .expect_err("occupied scheduler must reject the sample") + .to_string(), + "embedding_qualification_true_idle_not_quiescent", + "{label} work must fail closed rather than shift the boundary" + ); + } + } + + #[test] + fn true_idle_boundary_rejects_a_different_owner_after_product_completion() { + let result = validate_true_idle_boundary( + "measured-owner", + &true_idle_snapshot("replacement-owner", 0, 0, 0, 0), + ); + assert_eq!( + result + .expect_err("replacement owner must reject the sample") + .to_string(), + "embedding_qualification_true_idle_owner_changed" + ); + } } diff --git a/crates/codestory-llama-sys/per-user-embedding-server-constant-set.json b/crates/codestory-llama-sys/per-user-embedding-server-constant-set.json index 36a5e49fc..33c1aed5a 100644 --- a/crates/codestory-llama-sys/per-user-embedding-server-constant-set.json +++ b/crates/codestory-llama-sys/per-user-embedding-server-constant-set.json @@ -1,7 +1,7 @@ { "calibration_required_values": { "capacity_retry_policy": { - "retry_after_ms": 40, + "retry_after_ms": 42, "retry_class": "after_capacity_change", "retry_condition_source": "named_condition_from_typed_capacity_response" }, @@ -9,7 +9,7 @@ "election_backoff_policy": { "initial_backoff_ms": 7, "jitter": "sha256(process_start_id||attempt) modulo inclusive [initial_backoff_ms,maximum_backoff_ms]", - "maximum_backoff_ms": 102 + "maximum_backoff_ms": 104 }, "hard_native_no_progress_ms": 385431, "request_deadlines_ms": { @@ -43,7 +43,22 @@ "query_queue_capacity": 64, "true_idle_observation_grace_ms": 2500 }, - "freeze_record": null, + "freeze_record": { + "calibration_bundle_sha256": "2adaaab974814cf890609bac0f1b6be54fb04cda4812aea33f06dff63a954ed0", + "calibration_freeze_digest": "511ec0e9018d73c1cfb4669de2e1b4bd722ef650cd9e6d38d7179d568913f2d5", + "input_constant_set_sha256": "ea58d298473ddf320469d15dc7c32176a1109d615a689c15ff76b67fb337e109", + "measurement_protocol_sha256": "d1bb9b2c7eb354fe98990aa32eedc0e165b0cf804212966e0a2ee362a2f5bf8e", + "protocol_sha256": "f4a3fa4afb4d5bcd8e707a5e21b687cdd023dc3398b28ff6891a2318e89c5ec7", + "run_artifact_sha256s": [ + "2332db8dfdf81057263a7db45d184e0847f576d1cb2047f2704998d0f20848c8", + "7a0a63f344b0ba0f88cc8e3a7118d8c8d3ec044ee1ea0b71ad3fbc75421ee44c", + "7bb038f4503c333d200f5f532fded590ce74acdb2cabc99cfaf7643b6930767f" + ], + "selected_at": "github-actions-run:30562970311:1", + "selection_rule": "constant_only_three_fresh_generations_one_sample_each+slow_host_floors_v2", + "selection_source_commit": "681ca99098b86006e7dc1f51c27158c757ac05c9", + "selection_source_tree": "1a5494c9b82bcddc030438452285154e9d5b3e2b" + }, "qualification_thresholds": { "backend_observed_accelerator_residency": 1, "bulk_documents_per_second": 2, @@ -60,5 +75,5 @@ }, "schema_version": 1, "selection_protocol": "codestory-per-user-embedding-server-v1", - "status": "unfrozen" + "status": "frozen" } diff --git a/crates/codestory-llama-sys/per-user-embedding-server-measurement-protocol.json b/crates/codestory-llama-sys/per-user-embedding-server-measurement-protocol.json index deb9c385b..d8ddd7f61 100644 --- a/crates/codestory-llama-sys/per-user-embedding-server-measurement-protocol.json +++ b/crates/codestory-llama-sys/per-user-embedding-server-measurement-protocol.json @@ -157,7 +157,7 @@ "named_retry_condition_became_true" ], "true_idle_exit": [ - "last_queued_active_or_leased_work_ended", + "final_product_request_completed", "engine_and_server_absent" ], "total_codestory_process_memory": [ @@ -279,7 +279,7 @@ "measured_request_ordinal": 65 }, "true_idle_exit": { - "workload_id": "true_idle_60000_awake_ms_v1", + "workload_id": "true_idle_after_product_completion_60000_awake_ms_v2", "owner_state": "resident_then_absent", "operation": "observe", "input_generator": "none", diff --git a/release-claims.json b/release-claims.json index e9e07b945..068d2e6f0 100644 --- a/release-claims.json +++ b/release-claims.json @@ -1133,6 +1133,8 @@ "coordinator_workflow": "packaged-platform-pr.yml", "mode": "calibration", "assembly_job": "calibration-assemble", + "pre_collection_source_proof_required": false, + "source_proof_stage": "frozen_candidate_before_qualification", "required_cells": [ { "id": "protected_macos_arm64_metal", @@ -1466,13 +1468,90 @@ "manual_pr_ref_hint": "--ref ", "source_cache_namespace": "source-proof-v2", "packaged_cache_namespace": "codestory-cli-native-v4", - "label_routed_workflows": [ + "label_routed_workflows": [], + "required_events": [] + }, + "release_freeze_barrier": { + "schema": 3, + "script": ".github/scripts/release-freeze-barrier.mjs", + "status_context_prefix": "codestory/release-freeze", + "allowed_future_source_changes": [ + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json" + ], + "required_hostile_mutations": [ + "cpu_backend_rejected", + "calibration_qualification_rejected", + "calibration_three_by_three_rejected", + "calibration_repeated_setup_rejected", + "missing_linux_nonblocking", + "windows_duplicate_build_rejected", + "windows_debug_release_mix_rejected", + "windows_stale_archive_rejected" + ], + "broad_entry_workflows": [ "source-proof.yml", - "packaged-platform-pr.yml" - ], - "required_events": [ - "labeled" - ] + "packaged-platform-pr.yml", + "release.yml" + ], + "invalidation_workflow": "release-freeze-invalidation.yml", + "coordinator_only_workflows": [ + "macos-metal-proof.yml", + "windows-vulkan-proof.yml", + "linux-vulkan-proof.yml" + ], + "acceptance": { + "producer_workflow": "source-proof.yml", + "receipt_authority": "github_actions", + "receipt_artifact": "release-freeze-receipt-attempt-${{ github.run_attempt }}", + "receipt_file": "release-freeze-receipt.json", + "receipt_producer_job": "resolve", + "status_scope": "exact_candidate_head", + "later_commit_revokes": true, + "event": "workflow_dispatch", + "hostile_job": "freeze-hostile-mutations", + "hostile_step": "Execute exact-head hostile mutation matrix", + "windows_job": "freeze-windows-native-probe", + "windows_step": "Run exact-head Windows native probe", + "windows_runner": [ + "self-hosted", + "Windows", + "X64", + "codestory-vulkan" + ], + "windows_probe_max_seconds": 90, + "publisher_job": "freeze-acceptance", + "publisher_step": "Publish executable release freeze", + "status_creator": "github-actions[bot]", + "job_manifest": ".github/scripts/release-freeze-acceptance-jobs.json", + "job_manifest_sha256": "2df6fb76f1ac19acb98e530381ef456f38d517ded6356e61892b75b5fe6f3c79", + "phases": { + "calibration_source": { + "known_future_source_changes": [ + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json" + ], + "planned_actions": [ + "calibration-source-acceptance", + "calibration", + "generated-constant-freeze", + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release" + ], + "next_permitted_mutation": "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json" + }, + "frozen_candidate": { + "known_future_source_changes": [], + "planned_actions": [ + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release" + ], + "next_permitted_mutation": null + } + } + } }, "actionlint": { "version": "1.7.12", diff --git a/scripts/codestory-release-claims.mjs b/scripts/codestory-release-claims.mjs index 7ee478bd3..7a096d8f1 100644 --- a/scripts/codestory-release-claims.mjs +++ b/scripts/codestory-release-claims.mjs @@ -541,8 +541,10 @@ function validateCalibrationPolicy(value) { calibration.coordinator_workflow !== "packaged-platform-pr.yml" || calibration.mode !== "calibration" || calibration.assembly_job !== "calibration-assemble" + || calibration.pre_collection_source_proof_required !== false + || calibration.source_proof_stage !== "frozen_candidate_before_qualification" ) { - fail("workflow_policy.calibration must name the canonical calibration coordinator and assembly job"); + fail("workflow_policy.calibration must collect before the sole frozen-candidate source proof"); } if (calibration.runs_per_required_cell !== 3) { fail("workflow_policy.calibration must require exactly three clean runs per required cell"); @@ -1506,9 +1508,199 @@ export function validateReleaseClaimGraph(graph) { nonEmptyText(promotion.manual_pr_ref_hint, "workflow_policy.promotion.manual_pr_ref_hint"); nonEmptyText(promotion.source_cache_namespace, "workflow_policy.promotion.source_cache_namespace"); nonEmptyText(promotion.packaged_cache_namespace, "workflow_policy.promotion.packaged_cache_namespace"); - stringArray(promotion.label_routed_workflows, "workflow_policy.promotion.label_routed_workflows", { nonEmpty: true }); - stringArray(promotion.required_events, "workflow_policy.promotion.required_events", { nonEmpty: true }); + const labelRouted = stringArray( + promotion.label_routed_workflows, + "workflow_policy.promotion.label_routed_workflows", + ); + const requiredEvents = stringArray( + promotion.required_events, + "workflow_policy.promotion.required_events", + ); + if (labelRouted.length !== 0 || requiredEvents.length !== 0) { + fail("workflow_policy.promotion must not admit label-routed proof workflows"); + } + const freeze = object( + policy.release_freeze_barrier, + "workflow_policy.release_freeze_barrier", + ); + if (freeze.schema !== 3) { + fail("workflow_policy.release_freeze_barrier.schema must be 3"); + } + nonEmptyText(freeze.script, "workflow_policy.release_freeze_barrier.script"); + nonEmptyText( + freeze.status_context_prefix, + "workflow_policy.release_freeze_barrier.status_context_prefix", + ); + stringArray( + freeze.allowed_future_source_changes, + "workflow_policy.release_freeze_barrier.allowed_future_source_changes", + { nonEmpty: true }, + ); + stringArray( + freeze.required_hostile_mutations, + "workflow_policy.release_freeze_barrier.required_hostile_mutations", + { nonEmpty: true }, + ); + stringArray( + freeze.broad_entry_workflows, + "workflow_policy.release_freeze_barrier.broad_entry_workflows", + { nonEmpty: true }, + ); + if (freeze.invalidation_workflow !== "release-freeze-invalidation.yml") { + fail( + "workflow_policy.release_freeze_barrier.invalidation_workflow must name " + + "release-freeze-invalidation.yml", + ); + } + stringArray( + freeze.coordinator_only_workflows, + "workflow_policy.release_freeze_barrier.coordinator_only_workflows", + { nonEmpty: true }, + ); + const acceptance = object( + freeze.acceptance, + "workflow_policy.release_freeze_barrier.acceptance", + ); + for (const field of [ + "producer_workflow", + "receipt_authority", + "receipt_artifact", + "receipt_file", + "receipt_producer_job", + "status_scope", + "event", + "hostile_job", + "hostile_step", + "windows_job", + "windows_step", + "publisher_job", + "publisher_step", + "status_creator", + "job_manifest", + "job_manifest_sha256", + ]) { + nonEmptyText( + acceptance[field], + `workflow_policy.release_freeze_barrier.acceptance.${field}`, + ); + } + const windowsRunner = stringArray( + acceptance.windows_runner, + "workflow_policy.release_freeze_barrier.acceptance.windows_runner", + { nonEmpty: true }, + ); + if ( + JSON.stringify([...windowsRunner].sort()) + !== JSON.stringify([ + "self-hosted", + "Windows", + "X64", + "codestory-vulkan", + ].sort()) + ) { + fail( + "workflow_policy.release_freeze_barrier.acceptance.windows_runner " + + "must name the protected Windows Vulkan runner", + ); + } + if ( + acceptance.producer_workflow !== "source-proof.yml" + || acceptance.receipt_authority !== "github_actions" + || acceptance.receipt_artifact + !== "release-freeze-receipt-attempt-${{ github.run_attempt }}" + || acceptance.receipt_file !== "release-freeze-receipt.json" + || acceptance.receipt_producer_job !== "resolve" + || acceptance.status_scope !== "exact_candidate_head" + || acceptance.later_commit_revokes !== true + || acceptance.event !== "workflow_dispatch" + || acceptance.windows_probe_max_seconds !== 90 + || acceptance.status_creator !== "github-actions[bot]" + || acceptance.job_manifest + !== ".github/scripts/release-freeze-acceptance-jobs.json" + || !SHA256.test(acceptance.job_manifest_sha256) + ) { + fail( + "workflow_policy.release_freeze_barrier.acceptance must bind the exact " + + "Actions receipt authority, immutable artifact, producer, event, protected " + + "probe budget, status scope, revocation, and status creator", + ); + } + const freezePhases = object( + acceptance.phases, + "workflow_policy.release_freeze_barrier.acceptance.phases", + ); + if ( + JSON.stringify(Object.keys(freezePhases).sort()) + !== JSON.stringify(["calibration_source", "frozen_candidate"]) + ) { + fail( + "workflow_policy.release_freeze_barrier.acceptance.phases must define " + + "exactly calibration_source and frozen_candidate", + ); + } + const constantSet = + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json"; + const calibrationSource = object( + freezePhases.calibration_source, + "workflow_policy.release_freeze_barrier.acceptance.phases.calibration_source", + ); + const calibrationFuture = stringArray( + calibrationSource.known_future_source_changes, + "workflow_policy.release_freeze_barrier.acceptance.phases.calibration_source.known_future_source_changes", + { nonEmpty: true }, + ); + const calibrationActions = stringArray( + calibrationSource.planned_actions, + "workflow_policy.release_freeze_barrier.acceptance.phases.calibration_source.planned_actions", + { nonEmpty: true }, + ); + if ( + JSON.stringify(calibrationFuture) !== JSON.stringify([constantSet]) + || JSON.stringify(calibrationActions) !== JSON.stringify([ + "calibration-source-acceptance", + "calibration", + "generated-constant-freeze", + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", + ]) + || calibrationSource.next_permitted_mutation !== constantSet + ) { + fail( + "workflow_policy.release_freeze_barrier.acceptance.phases.calibration_source " + + "must permit only calibration then the generated constant-set freeze before source proof", + ); + } + const frozenCandidate = object( + freezePhases.frozen_candidate, + "workflow_policy.release_freeze_barrier.acceptance.phases.frozen_candidate", + ); + const frozenFuture = stringArray( + frozenCandidate.known_future_source_changes, + "workflow_policy.release_freeze_barrier.acceptance.phases.frozen_candidate.known_future_source_changes", + ); + const frozenActions = stringArray( + frozenCandidate.planned_actions, + "workflow_policy.release_freeze_barrier.acceptance.phases.frozen_candidate.planned_actions", + { nonEmpty: true }, + ); + if ( + frozenFuture.length !== 0 + || JSON.stringify(frozenActions) !== JSON.stringify([ + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", + ]) + || frozenCandidate.next_permitted_mutation !== null + ) { + fail( + "workflow_policy.release_freeze_barrier.acceptance.phases.frozen_candidate " + + "must permit no future source mutation before its sole source proof", + ); + } const actionlint = object(policy.actionlint, "workflow_policy.actionlint"); if (actionlint.version !== "1.7.12") fail("workflow_policy.actionlint.version must be 1.7.12"); nonEmptyText(actionlint.config, "workflow_policy.actionlint.config"); diff --git a/scripts/tests/codestory-release-claims.test.mjs b/scripts/tests/codestory-release-claims.test.mjs index b4cbd4a3f..147772278 100644 --- a/scripts/tests/codestory-release-claims.test.mjs +++ b/scripts/tests/codestory-release-claims.test.mjs @@ -152,11 +152,69 @@ test("versioned claim graph has one deterministic digest and all declared contro ], ); assert.ok(graph.claims.every((claim) => claim.prerequisite_checks.every(({ command }) => command.length > 0))); - assert.deepEqual(graph.workflow_policy.promotion.required_events, ["labeled"]); + assert.deepEqual(graph.workflow_policy.promotion.required_events, []); + assert.deepEqual(graph.workflow_policy.promotion.label_routed_workflows, []); assert.equal(graph.workflow_policy.promotion.proof_run_sha_expression, "${{ github.sha }}"); assert.equal(graph.workflow_policy.promotion.manual_pr_ref_hint, "--ref "); assert.equal(graph.workflow_policy.promotion.source_cache_namespace, "source-proof-v2"); assert.equal(graph.workflow_policy.promotion.packaged_cache_namespace, "codestory-cli-native-v4"); + assert.deepEqual( + graph.workflow_policy.release_freeze_barrier.acceptance, + { + producer_workflow: "source-proof.yml", + receipt_authority: "github_actions", + receipt_artifact: "release-freeze-receipt-attempt-${{ github.run_attempt }}", + receipt_file: "release-freeze-receipt.json", + receipt_producer_job: "resolve", + status_scope: "exact_candidate_head", + later_commit_revokes: true, + event: "workflow_dispatch", + hostile_job: "freeze-hostile-mutations", + hostile_step: "Execute exact-head hostile mutation matrix", + windows_job: "freeze-windows-native-probe", + windows_step: "Run exact-head Windows native probe", + windows_runner: ["self-hosted", "Windows", "X64", "codestory-vulkan"], + windows_probe_max_seconds: 90, + publisher_job: "freeze-acceptance", + publisher_step: "Publish executable release freeze", + status_creator: "github-actions[bot]", + job_manifest: ".github/scripts/release-freeze-acceptance-jobs.json", + job_manifest_sha256: + "2df6fb76f1ac19acb98e530381ef456f38d517ded6356e61892b75b5fe6f3c79", + phases: { + calibration_source: { + known_future_source_changes: [ + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json", + ], + planned_actions: [ + "calibration-source-acceptance", + "calibration", + "generated-constant-freeze", + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", + ], + next_permitted_mutation: + "crates/codestory-llama-sys/per-user-embedding-server-constant-set.json", + }, + frozen_candidate: { + known_future_source_changes: [], + planned_actions: [ + "frozen-candidate-acceptance", + "source-proof", + "qualification", + "release", + ], + next_permitted_mutation: null, + }, + }, + }, + ); + assert.equal( + graph.workflow_policy.release_freeze_barrier.invalidation_workflow, + "release-freeze-invalidation.yml", + ); }); test("claim graph freezes one exact Windows release graph and protected content-addressed reuse", () => { @@ -289,6 +347,11 @@ test("claim graph freezes Mac-only accelerated 3x1 constant calibration", () => assert.equal(calibration.optional_cells[0].feeds_constant_selection, false); assert.equal(calibration.runs_per_required_cell, 3); assert.equal(calibration.samples_per_metric_per_run, 1); + assert.equal(calibration.pre_collection_source_proof_required, false); + assert.equal( + calibration.source_proof_stage, + "frozen_candidate_before_qualification", + ); assert.deepEqual(calibration.forbidden_environment, [ "CODESTORY_EMBED_ALLOW_CPU=1", ]); @@ -312,6 +375,12 @@ test("claim graph freezes Mac-only accelerated 3x1 constant calibration", () => [draft => { draft.workflow_policy.calibration.samples_per_metric_per_run = 3; }, /exactly one sample per metric per run/u], + [draft => { + draft.workflow_policy.calibration.pre_collection_source_proof_required = true; + }, /sole frozen-candidate source proof/u], + [draft => { + draft.workflow_policy.calibration.source_proof_stage = "before_calibration"; + }, /sole frozen-candidate source proof/u], [draft => { draft.workflow_policy.calibration.forbidden_environment = [ "CODESTORY_EMBED_ALLOW_CPU=0", @@ -619,6 +688,87 @@ test("graph rejects ambiguous dependencies and unstructured proof lanes", () => /identity undeclared_identity must declare a format/u, ); + const unprotectedFreezeProbe = structuredClone(graph); + unprotectedFreezeProbe.workflow_policy.release_freeze_barrier + .acceptance.windows_runner = ["windows-latest"]; + assert.throws( + () => validateReleaseClaimGraph(unprotectedFreezeProbe), + /release_freeze_barrier\.acceptance\.windows_runner/u, + ); + + const callerAuthoredFreeze = structuredClone(graph); + callerAuthoredFreeze.workflow_policy.release_freeze_barrier + .acceptance.receipt_authority = "caller"; + assert.throws( + () => validateReleaseClaimGraph(callerAuthoredFreeze), + /release_freeze_barrier\.acceptance/u, + ); + + const mutableFreezeReceipt = structuredClone(graph); + mutableFreezeReceipt.workflow_policy.release_freeze_barrier + .acceptance.receipt_artifact = "release-freeze-receipt"; + assert.throws( + () => validateReleaseClaimGraph(mutableFreezeReceipt), + /release_freeze_barrier\.acceptance/u, + ); + + const persistentFreezeStatus = structuredClone(graph); + persistentFreezeStatus.workflow_policy.release_freeze_barrier + .acceptance.later_commit_revokes = false; + assert.throws( + () => validateReleaseClaimGraph(persistentFreezeStatus), + /release_freeze_barrier\.acceptance/u, + ); + + const unpinnedAcceptanceManifest = structuredClone(graph); + unpinnedAcceptanceManifest.workflow_policy.release_freeze_barrier + .acceptance.job_manifest_sha256 = "not-a-digest"; + assert.throws( + () => validateReleaseClaimGraph(unpinnedAcceptanceManifest), + /release_freeze_barrier\.acceptance/u, + ); + + const substitutedAcceptanceManifest = structuredClone(graph); + substitutedAcceptanceManifest.workflow_policy.release_freeze_barrier + .acceptance.job_manifest = ".github/workflows/source-proof.yml"; + assert.throws( + () => validateReleaseClaimGraph(substitutedAcceptanceManifest), + /release_freeze_barrier\.acceptance/u, + ); + + const preCalibrationSourceProof = structuredClone(graph); + preCalibrationSourceProof.workflow_policy.release_freeze_barrier + .acceptance.phases.calibration_source.planned_actions = [ + "calibration-source-acceptance", + "source-proof", + "calibration", + "generated-constant-freeze", + "qualification", + "release", + ]; + assert.throws( + () => validateReleaseClaimGraph(preCalibrationSourceProof), + /calibration_source.*calibration.*generated constant-set freeze before source proof/u, + ); + + const mutableFrozenCandidate = structuredClone(graph); + mutableFrozenCandidate.workflow_policy.release_freeze_barrier + .acceptance.phases.frozen_candidate.known_future_source_changes = [ + "AGENTS.md", + ]; + assert.throws( + () => validateReleaseClaimGraph(mutableFrozenCandidate), + /frozen_candidate.*no future source mutation/u, + ); + + const missingInvalidation = structuredClone(graph); + delete missingInvalidation.workflow_policy.release_freeze_barrier + .invalidation_workflow; + assert.throws( + () => validateReleaseClaimGraph(missingInvalidation), + /release_freeze_barrier\.invalidation_workflow/u, + ); + // A non-claim that withholds less than the lost host actually produced would leave a live claim // resting on a proof that never ran, so the withheld set is checked against the graph itself. const partialNonClaim = structuredClone(graph); diff --git a/scripts/tests/fixtures/release-claims/positive.json b/scripts/tests/fixtures/release-claims/positive.json index 902934188..57f4a96fe 100644 --- a/scripts/tests/fixtures/release-claims/positive.json +++ b/scripts/tests/fixtures/release-claims/positive.json @@ -17,7 +17,7 @@ "type": "source_behavior", "tier": "source", "status": "pass", - "graph_sha256": "7385b32924d42e94c14b8d1f41fcec50bded200fc0e65fec05f43036e2896087", + "graph_sha256": "9df8f932d66a60baa104bf61252ab318b0c1325740a0edf193463618e0946348", "observed_at": "2026-07-16T11:00:00.000Z", "expires_at": "2026-07-17T11:00:00.000Z", "identity": {