Files
OwnCord/scripts/verify-gate-evidence.mjs
J3vbandClaude 59518c4767 ci: run otel-tagged api tests; pin strict for identical-tree evidence (#1465)
The tag-gated step ran -tags otel only for ./telemetry/..., so
api/recoverer_otel_test.go — B3-9's OC-0346 panic-log test — executed
nowhere in CI. Widen the scope to ./api/... and correct the comment that
lists the tagged files. Both packages pass locally under the tag.

G-03 as amended: integration evidence for a dev squash commit is the full
required matrix on its PR head plus tree identity between the two, which
required_status_checks.strict guarantees by construction. Make that
checkable: verify-gate-evidence.mjs --selftest now fails if the protection
script ever loses "strict": true, and scripts/verify-integration-tree.sh
asserts squash-tree == PR-head-tree for any squash SHA (the three newest
dev commits PASS), for phase-exit and hold-point evidence blocks.


Claude-Session: https://claude.ai/code/session_01B8dwVLEihnGZYtH9X631F4

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-31 04:58:39 +00:00

250 lines
9.6 KiB
JavaScript

#!/usr/bin/env node
// Fail when the commit being released does not carry green evidence for every
// required check (R-09 / RL-16).
//
// node scripts/verify-gate-evidence.mjs <sha> # assert, using $GITHUB_TOKEN
// node scripts/verify-gate-evidence.mjs --selftest
//
// A tag push starts release.yml and nothing else. ci.yml has no `tags:` trigger,
// so the tagged commit is only ever covered by the CI that ran when that same
// commit sat on a branch — and until this script existed, nothing checked that
// it had. release.yml re-runs none of the required contexts: it builds, smokes
// and signs, which is a different question from "did the gate pass".
//
// This is deliberately a script and not a `run:` block. .claude/skills/ci-check
// states the rule: a step that exists only in release.yml first executes at tag
// time, so its own bugs surface on the release. Server/scripts/docker-smoke.sh
// is the worked example — one script, called from both workflows. Here the
// second call site is `--selftest` in ci.yml, which exercises the decision logic
// on fixtures every PR without needing a tag or a network call.
//
// The required set is read from b0-dev-branch-protection.sh rather than
// duplicated, so pinning a new check cannot leave this gate behind.
import { readFileSync, existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const PROTECTION_SCRIPT = "docs/plans/b0-dev-branch-protection.sh";
// The contexts array in the protection script is the single source of truth for
// what "the gate" means. Parsed out of the heredoc rather than re-listed.
export function requiredContexts(scriptSrc) {
const block = scriptSrc.match(/"contexts"\s*:\s*\[([^\]]*)\]/);
if (!block) throw new Error(`no "contexts" array found in ${PROTECTION_SCRIPT}`);
return [...block[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
}
// The identical-tree evidence model (G-03 as amended 2026-08-31) leans on
// required_status_checks.strict: with strict off, a PR that is behind dev can
// be squash-merged into a tree no CI run ever saw, and the PR-head evidence
// this gate trusts stops describing what actually landed. strict: true is what
// makes "the required matrix ran on the merged tree" a construction rather
// than a habit — scripts/verify-integration-tree.sh spot-proves it per squash
// SHA — so losing the flag must fail on a pull request, not in a later audit.
export function strictUpToDate(scriptSrc) {
const block = scriptSrc.match(/"required_status_checks"\s*:\s*\{([\s\S]*?)\}/);
if (!block) {
throw new Error(`no "required_status_checks" object found in ${PROTECTION_SCRIPT}`);
}
const strict = block[1].match(/"strict"\s*:\s*(true|false)/);
if (!strict) {
throw new Error(`no "strict" key found under required_status_checks in ${PROTECTION_SCRIPT}`);
}
return strict[1] === "true";
}
// checkRuns is the API's check_runs array, already collected across pages.
// Returns the reasons this commit is not releasable; empty means it is.
export function evaluate(required, checkRuns) {
const problems = [];
const byName = new Map();
for (const run of checkRuns) {
// A context can report more than once (a re-run). The latest attempt wins,
// which is what the branch-protection UI shows and what a human would read.
const prev = byName.get(run.name);
if (!prev || (run.started_at ?? "") >= (prev.started_at ?? "")) byName.set(run.name, run);
}
for (const name of required) {
const run = byName.get(name);
if (!run) {
problems.push(`${name}: never reported on this commit`);
continue;
}
if (run.status !== "completed") {
problems.push(`${name}: still ${run.status} — the gate is not finished`);
continue;
}
// `neutral` and `skipped` are not success. A required check that skipped on
// the tagged commit proves nothing about it.
if (run.conclusion !== "success") {
problems.push(`${name}: ${run.conclusion}`);
}
}
return problems;
}
async function fetchCheckRuns(repo, sha, token) {
const runs = [];
for (let page = 1; ; page++) {
const url = `https://api.github.com/repos/${repo}/commits/${sha}/check-runs?per_page=100&page=${page}`;
const res = await fetch(url, {
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
"x-github-api-version": "2022-11-28",
},
});
if (!res.ok) throw new Error(`GET ${url}${res.status} ${res.statusText}`);
const body = await res.json();
runs.push(...body.check_runs);
// total_count is the count for the whole commit, not the page.
if (runs.length >= body.total_count || body.check_runs.length === 0) break;
}
return runs;
}
async function main() {
const sha = process.argv[2];
if (!sha) {
console.error("usage: node scripts/verify-gate-evidence.mjs <sha>");
process.exit(2);
}
const repo = process.env.GITHUB_REPOSITORY;
const token = process.env.GITHUB_TOKEN;
if (!repo || !token) {
console.error("GITHUB_REPOSITORY and GITHUB_TOKEN must be set");
process.exit(2);
}
const p = join(ROOT, PROTECTION_SCRIPT);
if (!existsSync(p)) {
console.error(`missing ${PROTECTION_SCRIPT} — it defines the required set`);
process.exit(2);
}
const required = requiredContexts(readFileSync(p, "utf8"));
const runs = await fetchCheckRuns(repo, sha, token);
const problems = evaluate(required, runs);
console.log(`commit ${sha}: ${runs.length} check run(s), ${required.length} required`);
if (problems.length) {
console.error(`\n${problems.length} required check(s) do not evidence a green gate:\n`);
for (const x of problems) console.error(` ${x}`);
console.error(
"\nThis commit is not releasable. Publication consumes exact-SHA gate\n" +
"evidence: the tag must point at a commit whose required checks all passed.",
);
process.exit(1);
}
console.log(`all ${required.length} required check(s) green — releasable`);
}
function selftest() {
let failed = 0;
const assert = (cond, msg) => {
console.log(`${cond ? "PASS" : "FAIL"} ${msg}`);
if (!cond) failed++;
};
// Parsing the real protection script, not a fixture: if its shape changes,
// this gate must find out on a pull request rather than at tag time.
const real = requiredContexts(readFileSync(join(ROOT, PROTECTION_SCRIPT), "utf8"));
assert(real.length >= 10, `reads the required set from ${PROTECTION_SCRIPT} (${real.length})`);
assert(
real.includes("Server Build & Test (ubuntu-latest)"),
"an ampersand name survives parsing",
);
assert(
strictUpToDate(readFileSync(join(ROOT, PROTECTION_SCRIPT), "utf8")),
"strict: true is pinned — identical-tree integration evidence relies on it",
);
assert(
strictUpToDate('"required_status_checks": { "strict": true, "contexts": ["A"] }'),
"strict true parses",
);
assert(
!strictUpToDate('"required_status_checks": { "strict": false, "contexts": ["A"] }'),
"strict false is detected, not glossed as true",
);
const req = ["A", "B"];
const ok = (name, extra = {}) => ({
name,
status: "completed",
conclusion: "success",
...extra,
});
assert(evaluate(req, [ok("A"), ok("B")]).length === 0, "all required green → releasable");
assert(
evaluate(req, [ok("A"), ok("B"), ok("Extra")]).length === 0,
"an unrequired extra check does not block",
);
const why = (runs) => evaluate(req, runs).join(" | ");
assert(why([ok("A")]).includes("B: never reported"), "a missing required check is caught");
assert(
why([ok("A"), { name: "B", status: "completed", conclusion: "failure" }]).includes(
"B: failure",
),
"a failed required check is caught",
);
assert(
why([ok("A"), { name: "B", status: "in_progress", conclusion: null }]).includes("still"),
"a still-running required check is caught, not treated as absent",
);
assert(
why([ok("A"), { name: "B", status: "completed", conclusion: "skipped" }]).includes(
"B: skipped",
),
"skipped is not success — a skipped required check proves nothing",
);
assert(
why([ok("A"), { name: "B", status: "completed", conclusion: "neutral" }]).includes(
"B: neutral",
),
"neutral is not success",
);
// Re-runs: the latest attempt decides, in both directions.
assert(
evaluate(req, [
ok("A"),
{ name: "B", status: "completed", conclusion: "failure", started_at: "2020-01-01T00:00:00Z" },
ok("B", { started_at: "2020-01-02T00:00:00Z" }),
]).length === 0,
"a green re-run supersedes an earlier failure",
);
assert(
why([
ok("A"),
ok("B", { started_at: "2020-01-01T00:00:00Z" }),
{ name: "B", status: "completed", conclusion: "failure", started_at: "2020-01-02T00:00:00Z" },
]).includes("B: failure"),
"a failed re-run supersedes an earlier success",
);
assert(evaluate(req, []).length === 2, "a commit with no checks at all is not releasable");
console.log(
failed ? `\nselftest: ${failed} assertion(s) failed` : "\nselftest: all assertions pass",
);
process.exit(failed ? 1 : 0);
}
// Run only when invoked directly, so `evaluate` and `requiredContexts` can be
// imported and exercised without the module trying to reach the network.
// Compared against argv[1] rather than `import.meta.main`, which needs Node
// 24.2 while package.json's engines floor is >=24 — on 24.0 it is undefined and
// the script would silently do nothing.
const invokedDirectly =
process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (invokedDirectly) {
if (process.argv.includes("--selftest")) selftest();
else await main();
}