diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0823a6c..370ddab1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,16 +84,18 @@ jobs: # Tag-gated tests (DC-06 / T-2026-07-25-16). The build-tag matrix above # only COMPILES the otel/wazero variants; the tests behind those tags - # (plugin/sandbox_wazero_test.go, telemetry/telemetry_otel_test.go) ran - # nowhere until this step. Scoped to the two packages that carry tagged - # files — every other package is tag-invariant and already covered by the - # race run above. One leg is enough; no -race (the runtime under the tag - # is the concern, not new concurrency). + # (plugin/sandbox_wazero_test.go, telemetry/telemetry_otel_test.go, + # api/recoverer_otel_test.go — the OC-0346 panic-log test, which this + # step never executed until ./api/... was added) ran nowhere until this + # step. Scoped to the packages that carry tagged files — every other + # package is tag-invariant and already covered by the race run above. + # One leg is enough; no -race (the runtime under the tag is the concern, + # not new concurrency). - name: Run tag-gated tests (-tags wazero, -tags otel) if: matrix.os == 'ubuntu-latest' run: | go test -tags wazero -count=1 ./plugin/... - go test -tags otel -count=1 ./telemetry/... + go test -tags otel -count=1 ./telemetry/... ./api/... # Coverage ratchet (B3-6 item 1). Reads the profile the race step wrote, # but placed after the other test steps so a floor miss does not hide diff --git a/scripts/verify-gate-evidence.mjs b/scripts/verify-gate-evidence.mjs index 08626581..8850f142 100644 --- a/scripts/verify-gate-evidence.mjs +++ b/scripts/verify-gate-evidence.mjs @@ -36,6 +36,25 @@ export function requiredContexts(scriptSrc) { 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) { @@ -138,6 +157,18 @@ function selftest() { 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 = {}) => ({ diff --git a/scripts/verify-integration-tree.sh b/scripts/verify-integration-tree.sh new file mode 100644 index 00000000..faa0ae8e --- /dev/null +++ b/scripts/verify-integration-tree.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Integration evidence for a dev squash commit (G-03 as amended 2026-08-31). +# +# dev is squash-merge-only and its pushes deliberately run no ci.yml matrix +# (the pull_request trigger already ran it on the PR head), so the squash +# commit itself carries only CodeQL runs. What makes the PR-head evidence +# transfer to the squash commit is required_status_checks.strict: an +# up-to-date PR's head names the same tree the squash commit lands. This +# script turns that construction into a per-commit statement a phase-exit or +# hold-point evidence block can cite as a command with output, instead of an +# assumption: +# +# bash scripts/verify-integration-tree.sh [...] +# +# For each squash commit: parse the pull-request number from the "(#N)" +# subject suffix every squash merge carries, fetch refs/pull/N/head from +# origin, and assert both commits name the same git tree. Exits non-zero if +# any commit fails. Needs network access to origin; verify-gate-evidence.mjs +# --selftest is what keeps the strict flag itself pinned. +set -euo pipefail + +if [ "$#" -lt 1 ]; then + echo "usage: bash scripts/verify-integration-tree.sh [...]" >&2 + exit 2 +fi + +fail=0 +for sha in "$@"; do + if ! subject=$(git log -1 --format=%s "$sha" 2>/dev/null); then + echo "FAIL ${sha}: unknown commit (fetch the branch that carries it first)" >&2 + fail=1 + continue + fi + if [[ "$subject" =~ \(#([0-9]+)\)$ ]]; then + pr="${BASH_REMATCH[1]}" + else + echo "FAIL ${sha}: subject does not end in (#N), so it is not the squash of a PR: ${subject}" >&2 + fail=1 + continue + fi + git fetch -q --no-tags origin "refs/pull/${pr}/head" + head_tree=$(git rev-parse "FETCH_HEAD^{tree}") + squash_tree=$(git rev-parse "${sha}^{tree}") + if [ "$head_tree" = "$squash_tree" ]; then + echo "PASS ${sha} (PR #${pr}): squash tree == PR head tree ${head_tree}" + else + echo "FAIL ${sha} (PR #${pr}): squash tree ${squash_tree} != PR head tree ${head_tree} — the merged tree was never CI-tested as-is" >&2 + fail=1 + fi +done +exit "$fail"