From a64b563177af99c736dc9204b7a4dbe83a05d32c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 12:29:52 +0200 Subject: [PATCH 01/10] ci: skip drafts and docs-only PRs via ci.yml scope job (Refs #5743) A scope job classifies the pull_request diff; x86-main and the workspace-test shards skip on a draft or an archive/postmortem/report/image-only diff, and ci-gate, gate and workspace-test short-circuit to success so the required checks still report. merge_group and push runs are untouched. scripts/ ci_scope.sh --self-test pins the classifier and the wiring; ci_mg_reuse.sh wiring pins now name scope. Co-Authored-By: Claude Sonnet 5.5 Agent: aprender-w5382 --- .github/workflows/ci.yml | 67 +++++++++++++++--- docs/roadmaps/entries/PMAT-5743.yaml | 18 +++++ scripts/ci_mg_reuse.sh | 14 ++-- scripts/ci_scope.sh | 101 +++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 15 deletions(-) create mode 100644 docs/roadmaps/entries/PMAT-5743.yaml create mode 100755 scripts/ci_scope.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0f3850566..fccc275942 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ on: tags: ['v*'] pull_request: branches: [main, master] + # ready_for_review: a draft's run is skipped by the `scope` job below, so marking it + # ready must start the real one. NO `paths`/`paths-ignore` here: a filtered workflow + # never reports and the required checks would stay "expected" (scripts/ci_scope.sh). + types: [opened, synchronize, reopened, ready_for_review] # Merge queue: GitHub dispatches a `merge_group` event for each batch it forms. # Both required checks (`ci / gate` via the reusable sovereign-ci call, and the # top-level `workspace-test` job) live in THIS workflow, so triggering here makes @@ -53,6 +57,37 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} jobs: + # CI demand: a draft PR and a docs-only PR skip the fat jobs. pull_request ONLY; the + # verdict jobs (ci / gate, gate, workspace-test) still run and report success, so no + # required check is left "expected". merge_group and push skip this job and run in + # full. Rule and case table: scripts/ci_scope.sh. + scope: + if: ${{ github.event_name == 'pull_request' }} + runs-on: [self-hosted, Linux, clean-room] + timeout-minutes: 5 + permissions: + contents: read + outputs: + skip: ${{ steps.scope.outputs.skip }} + reason: ${{ steps.scope.outputs.reason }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Classify the PR (draft / docs-only / full) + id: scope + env: + DRAFT: ${{ github.event.pull_request.draft }} + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: | + set -uo pipefail + # Any failure to read the diff is `full`: a skip needs a positive read. + if ! paths=$(git diff --name-only "$BASE...$HEAD"); then paths=""; fi + out=$(printf '%s\n' "$paths" | bash scripts/ci_scope.sh classify "${DRAFT:-false}") || out="skip=0 reason=full" + echo "$out" + printf '%s\n' "$out" | sed 's/ /\n/' >> "$GITHUB_OUTPUT" + # ========================================================================== # #4433 — FIVE FAT JOBS (operator ruling 2026-09-25, items 1+4): # "CI = 5 fat jobs (x86 main, gx10, yoga, mac-refusal, determinism). Each @@ -152,10 +187,12 @@ jobs: run: | setsid --wait bash scripts/check_ci_wait_cancel.sh --self-test setsid --wait bash scripts/check_ci_wait_cancel.sh + - name: The skip scope (draft / docs-only) and its wiring in ci.yml + run: setsid --wait bash scripts/ci_scope.sh --self-test x86-main: - needs: [mg-reuse] - if: ${{ !cancelled() && needs.mg-reuse.outputs.x86 != '1' }} + needs: [mg-reuse, scope] + if: ${{ !cancelled() && needs.mg-reuse.outputs.x86 != '1' && needs.scope.outputs.skip != '1' }} # pull_request/push stay on the shared clean-room pool; a merge_group entry takes the mq lane (custom label exactly mq) runs-on: ${{ github.event_name == 'merge_group' && fromJSON('["self-hosted","Linux","X64","mq"]') || fromJSON('["self-hosted","Linux","X64","clean-room"]') }} timeout-minutes: 240 @@ -440,6 +477,8 @@ jobs: # explicit runner's round-robin N/3), in parallel on the X64 clean-room pool. # The section body is ci/sections.yml `workspace-test-shard`, unchanged. workspace-test-shard: + needs: [scope] + if: ${{ !cancelled() && needs.scope.outputs.skip != '1' }} # same lanes as x86-main: clean-room for pull_request/push, mq for a merge_group entry runs-on: ${{ github.event_name == 'merge_group' && fromJSON('["self-hosted","Linux","X64","mq"]') || fromJSON('["self-hosted","Linux","X64","clean-room"]') }} timeout-minutes: 180 @@ -490,18 +529,22 @@ jobs: workspace-test: runs-on: [self-hosted, Linux, clean-room] timeout-minutes: 10 - needs: [workspace-test-shard] + needs: [workspace-test-shard, scope] if: always() steps: - name: Every shard passed env: SHARD_RESULT: ${{ needs.workspace-test-shard.result }} + SCOPE_SKIP: ${{ needs.scope.outputs.skip }} run: | set -euo pipefail + [ "${SCOPE_SKIP:-}" = 1 ] && { echo "scope skip (draft or docs-only PR): no shards owed"; exit 0; } echo "workspace-test-shard matrix result: $SHARD_RESULT" [ "$SHARD_RESULT" = success ] || { echo "::error::workspace-test: shard matrix result is '$SHARD_RESULT' (a cancelled or skipped shard is not a pass)"; exit 1; } - - uses: actions/checkout@v7 - - uses: actions/download-artifact@v4 + - if: ${{ needs.scope.outputs.skip != '1' }} + uses: actions/checkout@v7 + - if: ${{ needs.scope.outputs.skip != '1' }} + uses: actions/download-artifact@v4 with: pattern: sigma-shard-* path: ${{ runner.temp }}/sigma @@ -509,7 +552,8 @@ jobs: # owed set, by id. Each shard ran a partition; the UNION of their executed # ids is compared with the universe shard 1 listed. A shard whose ids did # not arrive is RED, never a smaller universe. - - name: "Σ-executed: the union of the shards is exactly the owed set (#4433)" + - if: ${{ needs.scope.outputs.skip != '1' }} + name: "Σ-executed: the union of the shards is exactly the owed set (#4433)" run: | set -euo pipefail d="$RUNNER_TEMP/sigma" @@ -540,7 +584,7 @@ jobs: name: ci / gate runs-on: [self-hosted, Linux, clean-room] timeout-minutes: 10 - needs: [x86-main, mg-reuse, x86-main-advisories] + needs: [x86-main, mg-reuse, x86-main-advisories, scope] if: always() steps: - name: The sovereign-ci gate section's result @@ -552,8 +596,11 @@ jobs: MG_DET: ${{ needs.mg-reuse.outputs.det }} MG_CITE: ${{ needs.mg-reuse.outputs.cite }} ADV: ${{ needs.x86-main-advisories.result }} + SCOPE_SKIP: ${{ needs.scope.outputs.skip }} + SCOPE_WHY: ${{ needs.scope.outputs.reason }} run: | set -euo pipefail + [ "${SCOPE_SKIP:-}" = 1 ] && { echo "ci / gate: scope skip (${SCOPE_WHY:-}), fat jobs not owed on this pull_request"; exit 0; } # --- MG-REUSE-VERDICT-BEGIN (T43, Refs #4678; scripts/ci_mg_reuse.sh --self-test runs this block) --- # On merge_group, mg-reuse may skip x86-main and determinism -- both or neither -- when the PR # head's same-named jobs and its gate passed on this exact tree. On a push to main it may skip @@ -576,7 +623,7 @@ jobs: gate: runs-on: [self-hosted, Linux, clean-room] # arch-neutral: intel, yoga or gx10 (#3100) timeout-minutes: 22 - needs: [x86-main, workspace-test, determinism, mg-reuse, x86-main-advisories, mg-reuse-self-test] + needs: [x86-main, workspace-test, determinism, mg-reuse, x86-main-advisories, mg-reuse-self-test, scope] if: always() steps: - name: Check required sections @@ -591,8 +638,12 @@ jobs: MG_CITE: ${{ needs.mg-reuse.outputs.cite }} ADV: ${{ needs.x86-main-advisories.result }} MST: ${{ needs.mg-reuse-self-test.result }} + SCOPE_SKIP: ${{ needs.scope.outputs.skip }} + SCOPE_WHY: ${{ needs.scope.outputs.reason }} run: | set -euo pipefail + [ "$MST" = success ] || { echo "mg-reuse-self-test failed: $MST"; exit 1; } + [ "${SCOPE_SKIP:-}" = 1 ] && { echo "gate: scope skip (${SCOPE_WHY:-}), fat jobs not owed on this pull_request"; exit 0; } res() { printf '%s' "${1:-null}" | SEC="$2" jq -r '(. // {})[env.SEC].result // "missing"'; } # The old gate's needs, in its order: guard-tree, guard-cargo, ci (the # sovereign-ci gate), workspace-test, determinism-compare. gpu-quick and diff --git a/docs/roadmaps/entries/PMAT-5743.yaml b/docs/roadmaps/entries/PMAT-5743.yaml new file mode 100644 index 0000000000..d1efdcc869 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-5743.yaml @@ -0,0 +1,18 @@ +- id: PMAT-5743 + github_issue: 5743 + item_type: task + title: 'ci: skip drafts and docs-only PRs inside ci.yml scope job; verdict jobs report success' + status: planned + priority: high + assigned_to: null + created: 2026-10-19T10:29:43Z + updated: 2026-10-19T10:29:43Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5510 diff --git a/scripts/ci_mg_reuse.sh b/scripts/ci_mg_reuse.sh index 86573f5aff..e603341c4d 100755 --- a/scripts/ci_mg_reuse.sh +++ b/scripts/ci_mg_reuse.sh @@ -504,8 +504,8 @@ STUB wire mg-reuse 'bash scripts/ci_mg_reuse.sh resolve-push --event "$EVT" --ref "$GITHUB_REF" --sha "$GITHUB_SHA" --before "$PBEFORE"' wire mg-reuse 'PBEFORE: ${{ github.event.before }}' wire mg-reuse-self-test 'bash scripts/ci_mg_reuse.sh --self-test' - wire x86-main 'needs: [mg-reuse]' - wire x86-main "if: \${{ !cancelled() && needs.mg-reuse.outputs.x86 != '1' }}" + wire x86-main 'needs: [mg-reuse, scope]' + wire x86-main "if: \${{ !cancelled() && needs.mg-reuse.outputs.x86 != '1' && needs.scope.outputs.skip != '1' }}" wire determinism 'needs: [mg-reuse, x86-main]' # T42: determinism also waits on x86-main wire determinism "if: \${{ !cancelled() && needs.mg-reuse.outputs.det != '1' && github.event_name != 'pull_request' }}" # Q6 quorum, C324: mac-check is LAB and left ci.yml (nightly.yml now). The `mac` key is @@ -518,9 +518,9 @@ STUB wire x86-main-advisories 'cargo deny check advisories' wire x86-main-advisories 'bash scripts/check_deny_exemptions_live.sh' wire ci-gate 'MG-REUSE-VERDICT-BEGIN' - wire ci-gate 'needs: [x86-main, mg-reuse, x86-main-advisories]' + wire ci-gate 'needs: [x86-main, mg-reuse, x86-main-advisories, scope]' wire gate 'MG-REUSE-VERDICT-BEGIN' - wire gate 'mg-reuse, x86-main-advisories, mg-reuse-self-test]' + wire gate 'mg-reuse, x86-main-advisories, mg-reuse-self-test, scope]' wire gate '[ "$MST" = success ]' # #5472 variant: the pr-only label is retired; pull_request and push take the one shared clean-room # pool. The merge_group half (this row): x86-main and workspace-test-shard take the mq lane on a @@ -592,10 +592,10 @@ STUB fail=$((fail + 1)); echo "FAIL mutant mgshape-unreadable: an unreadable workflow passed green (fails open)"; fi mgmut mg-if-old-merge_group "mg-reuse if names merge_group" "s/^ if: github.event_name == 'push' && github.ref == 'refs\/heads\/main'/ if: github.event_name == 'merge_group' || (github.event_name == 'push' \&\& github.ref == 'refs\/heads\/main')/" mgmut mg-if-removed-string-elsewhere "is not the push-only key" "/^ if: github.event_name == 'push' && github.ref == 'refs\/heads\/main'/{s/.*/ # if: github.event_name == 'push' \&\& github.ref == 'refs\/heads\/main'/}" - mgmut x86-main-if-loses-cancelled "job x86-main needs mg-reuse but its if lacks" "s/^ if: \\$\{\{ !cancelled\(\) && needs.mg-reuse.outputs.x86 != '1' \}\}/ if: \${{ needs.mg-reuse.outputs.x86 != '1' }}/" + mgmut x86-main-if-loses-cancelled "job x86-main needs mg-reuse but its if lacks" "s/^ if: \\$\{\{ !cancelled\(\) && needs.mg-reuse.outputs.x86 != '1' && needs.scope.outputs.skip != '1' \}\}/ if: \${{ needs.mg-reuse.outputs.x86 != '1' }}/" mgmut determinism-if-loses-cancelled "job determinism needs mg-reuse but its if lacks" '/^ determinism:$/,/^ runs-on:/{s/^ if: \$\{\{ !cancelled\(\) && / if: ${{ /}' - mgmut x86-main-needs-lookalike "job x86-main no longer needs mg-reuse" '/^ x86-main:$/,/^ if:/{s/^ needs: \[mg-reuse\]$/ needs: [mg-reuse-self-test]/}' - mgmut x86-main-drops-needs "job x86-main no longer needs mg-reuse" '/^ x86-main:$/,/^ if:/{/^ needs: \[mg-reuse\]$/d}' + mgmut x86-main-needs-lookalike "job x86-main no longer needs mg-reuse" '/^ x86-main:$/,/^ if:/{s/^ needs: \[mg-reuse, scope\]$/ needs: [mg-reuse-self-test]/}' + mgmut x86-main-drops-needs "job x86-main no longer needs mg-reuse" '/^ x86-main:$/,/^ if:/{/^ needs: \[mg-reuse, scope\]$/d}' mgmut gate-if-loses-always "job gate needs mg-reuse but its if lacks" "/^ gate:\$/,/^ steps:/{s/^ if: always\(\)/ if: success()/}" # Positive control for wire itself: one job of more than 64 KiB (a pipe buffer) whose diff --git a/scripts/ci_scope.sh b/scripts/ci_scope.sh new file mode 100755 index 0000000000..a57ce7d1bb --- /dev/null +++ b/scripts/ci_scope.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# ci_scope.sh -- decide whether a pull_request needs the fat CI jobs at all. +# +# ci_scope.sh classify < changed paths, one per line +# prints `skip=1 reason=draft`, `skip=1 reason=docs-only` or `skip=0 reason=full` +# ci_scope.sh --self-test case table + wiring checks on ci.yml +# +# WHY. CI demand: a draft PR and a PR that only touches inert documentation each cost +# a full x86-main + three workspace-test shards. Skipping happens INSIDE ci.yml, never +# with a workflow-level `paths-ignore`: a filtered-out workflow never reports, and the +# required checks (`ci / gate`, `gate`, `workspace-test`) would sit at "expected" +# forever. Here the verdict jobs still run and report success on a skipped scope. +# +# SCOPE. pull_request ONLY. merge_group and push never reach this script's verdict +# (the `scope` job is `if: github.event_name == 'pull_request'`), so a queue build or +# a main push is never skipped or cancelled by it. A draft gets a fresh run when it is +# marked ready (`ready_for_review` is in the trigger types). +# +# DOCS-ONLY is an ALLOWLIST of trees no test or gate reads, found by searching the +# repo for readers (docs/audits, docs/roadmaps, docs/specifications, docs/findings, +# docs/BEATS.md, README.md, CLAUDE.md are READ by gates and are therefore NOT here). +# Every changed path must match; any other path, an empty list or a `..` is `full`. +# Growing the list is a gate-content change: prove no reader first. +set -uo pipefail +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" || exit 2 +DOCS_ONLY_RE='^docs/(archive|postmortems|reports|qa-reports|images)/[^.][^[:space:]]*$' + +classify() { # classify ; paths on stdin + local draft=${1:-false} n=0 p + if [ "$draft" = true ]; then cat >/dev/null; echo "skip=1 reason=draft"; return 0; fi + while IFS= read -r p; do + [ -n "$p" ] || continue + n=$((n + 1)) + case "$p" in *..*) echo "skip=0 reason=full"; return 0 ;; esac + printf '%s\n' "$p" | grep -Eq "$DOCS_ONLY_RE" || { echo "skip=0 reason=full"; return 0; } + done + [ "$n" -gt 0 ] && echo "skip=1 reason=docs-only" || echo "skip=0 reason=full" +} + +self_test() { + local fail=0 got + t() { # t NAME WANT DRAFT PATHS... + local name=$1 want=$2 draft=$3; shift 3 + got=$(printf '%s\n' "$@" | classify "$draft") + [ "$got" = "$want" ] || { echo "FAIL $name: want [$want] got [$got]"; fail=1; } + } + t "draft skips" "skip=1 reason=draft" true crates/a/src/lib.rs + t "draft, no paths" "skip=1 reason=draft" true + t "docs archive only" "skip=1 reason=docs-only" false docs/archive/a.md docs/images/x.png + t "postmortem only" "skip=1 reason=docs-only" false docs/postmortems/2026-10-19.md + t "code" "skip=0 reason=full" false crates/a/src/lib.rs + t "docs + code" "skip=0 reason=full" false docs/archive/a.md crates/a/src/lib.rs + t "README is gated" "skip=0 reason=full" false README.md + t "CLAUDE.md is gated" "skip=0 reason=full" false CLAUDE.md + t "BEATS is gated" "skip=0 reason=full" false docs/BEATS.md + t "roadmap entries are gated" "skip=0 reason=full" false docs/roadmaps/entries/PMAT-1.yaml + t "specifications are gated" "skip=0 reason=full" false docs/specifications/a.md + t "audits are read" "skip=0 reason=full" false docs/audits/surface_audit.csv + t "findings ledger is gated" "skip=0 reason=full" false docs/findings/w1.jsonl + t "workflow file" "skip=0 reason=full" false .github/workflows/ci.yml + t "contracts" "skip=0 reason=full" false contracts/a-v1.yaml + t "dot-dot escape" "skip=0 reason=full" false docs/archive/../../Cargo.toml + t "hidden file under docs" "skip=0 reason=full" false docs/archive/.hidden + t "prefix lookalike" "skip=0 reason=full" false docs/archive-x/a.md + t "empty diff is full" "skip=0 reason=full" false + local wf="$REPO_ROOT/.github/workflows/ci.yml" py + py=$(python3 - "$wf" <<'PY' +import sys, yaml +w = yaml.safe_load(open(sys.argv[1])) +on = w.get(True, w.get("on")) +bad = [] +pr = on.get("pull_request") or {} +if "paths-ignore" in pr or "paths" in pr: bad.append("pull_request has a paths filter (stuck required checks)") +if "ready_for_review" not in (pr.get("types") or []): bad.append("pull_request types lack ready_for_review") +for k in ("push", "merge_group"): + v = on.get(k) or {} + if "paths-ignore" in v or "paths" in v: bad.append(k + " has a paths filter") +j = w["jobs"] +sc = j.get("scope") +if not sc or sc.get("if") != "${{ github.event_name == 'pull_request' }}": bad.append("scope job missing or not pull_request-only") +for name in ("x86-main", "workspace-test-shard"): + if "scope" not in (j[name].get("needs") or []) or "needs.scope.outputs.skip != '1'" not in str(j[name].get("if")): bad.append(name + " does not honour scope") +for name in ("ci-gate", "gate", "workspace-test"): + if "scope" not in (j[name].get("needs") or []): bad.append(name + " does not need scope") + if name != "workspace-test" and "needs.scope.outputs.skip" not in str(j[name]): bad.append(name + " does not read scope") +if "needs.scope.outputs.skip" not in str(j["workspace-test"]): bad.append("workspace-test does not read scope") +c = w.get("concurrency") or {} +if "pull_request" not in str(c.get("cancel-in-progress")): bad.append("concurrency no longer cancels superseded PR runs") +print("\n".join(bad)) +PY + ) || { echo "FAIL wiring: cannot read ci.yml"; fail=1; } + [ -z "$py" ] || { printf 'FAIL wiring: %s\n' "$py"; fail=1; } + [ "$fail" -eq 0 ] && echo "ci_scope: self-test ok" + return "$fail" +} + +case "${1:-}" in + classify) shift; classify "${1:-false}" ;; + --self-test) self_test ;; + *) echo "usage: $0 classify | --self-test" >&2; exit 2 ;; +esac From 5ce1079ffaebbab735a2fe2718d2c45f0c5e95d2 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 03:09:20 +0200 Subject: [PATCH 02/10] Revert "ci: drop X64 from arch-neutral runs-on, pin the FULL tier's shards to X64 ... (#5891)" REVERT RULE (operator, 2026-10-18): merged/h below 0.46 for 2 consecutive hours after the change went live. 0 merges from 23:29:35Z (#5891 merged) to 01:30Z. Plain revert of the squash b410374203. Restores ci/sections.yml and removes scripts/ci/decide_tier.sh and scripts/ci/sigma_arch_uniform.sh. #5906 (ci_test_tier row 67 retargeted to decide_tier.sh) must be DROPPED if this lands: row 67 asserts the old ci/sections.yml text again. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 36 +------- ci/sections.yml | 62 ++++++++++++- .../src/kernel_registry_parity.rs | 22 +---- evidence/kreg/parity/cpu.matvec.apr_q4.json | 9 -- evidence/kreg/parity/cpu.matvec.apr_q8.json | 9 -- evidence/kreg/parity/cpu.matvec.bf16.json | 9 -- evidence/kreg/parity/cpu.matvec.f16.json | 9 -- evidence/kreg/parity/cpu.matvec.f32.json | 9 -- evidence/kreg/parity/cpu.matvec.q4_0.json | 9 -- evidence/kreg/parity/cpu.matvec.q4_k.json | 9 -- evidence/kreg/parity/cpu.matvec.q5_k.json | 9 -- evidence/kreg/parity/cpu.matvec.q6_k.json | 9 -- evidence/kreg/parity/cpu.matvec.q8_0.json | 9 -- scripts/check_ci_push_reuse.sh | 7 +- scripts/ci/decide_tier.sh | 66 -------------- scripts/ci/sigma_arch_uniform.sh | 91 ------------------- scripts/ci_mg_reuse.sh | 13 +-- 17 files changed, 72 insertions(+), 315 deletions(-) delete mode 100755 scripts/ci/decide_tier.sh delete mode 100755 scripts/ci/sigma_arch_uniform.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d3fa6f001..905ae731c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,7 @@ jobs: # its own. It runs on push only; on merge_group it is skipped at t=0, which the verdict blocks # below read as `skipped:::skipped` = "x86-main + determinism ran". if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: [self-hosted, Linux, clean-room] + runs-on: [self-hosted, Linux, X64, clean-room] timeout-minutes: 10 permissions: contents: read @@ -316,7 +316,7 @@ jobs: x86-main-advisories: needs: [mg-reuse] if: ${{ !cancelled() && needs.mg-reuse.outputs.x86 == '1' }} - runs-on: [self-hosted, Linux, clean-room] + runs-on: [self-hosted, Linux, X64, clean-room] timeout-minutes: 30 permissions: contents: read @@ -439,35 +439,12 @@ jobs: # failure, never a pass. # ========================================================================== # The workspace tests, one shard per job (nextest hash partition N/12 plus the - # explicit runner's round-robin N/12), in parallel on the clean-room pool (arch-neutral, PMAT-3138). + # explicit runner's round-robin N/12), in parallel on the X64 clean-room pool. # The section body is ci/sections.yml `workspace-test-shard`, unchanged. - # #5882: which tier this run owes decides WHERE the shards may run. The full tier's Σ-executed fan-in - # compares every shard with ONE universe list, and arch-gated tests (target_arch cfgs) differ per - # arch, so a full tier needs one architecture: X64. The quick/PR tier owes no Σ and stays - # arch-neutral. The tier is only knowable from the diff, so it is decided HERE, by the same script - # the shards run (scripts/ci/decide_tier.sh), and workspace-test-shard reads it in its runs-on. - # scripts/ci/sigma_arch_uniform.sh in the fan-in is the backstop: a mixed full tier is RED. - workspace-test-arch: - runs-on: [self-hosted, Linux, clean-room] - timeout-minutes: 15 - outputs: - tier: ${{ steps.tier.outputs.tier }} - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: Decide the test tier (the shards' own step, one script) - id: tier - env: - GH_TOKEN: ${{ github.token }} - MG_BASE_SHA: ${{ github.event.merge_group.base_sha }} - run: bash scripts/ci/decide_tier.sh workspace-test-shard: - needs: [workspace-test-arch] if: ${{ github.event.pull_request.draft != true }} - # arch-neutral (operator 2026-10-21 14:18Z) EXCEPT the full tier, which needs one arch for the Σ (#5882, workspace-test-arch) - # clean-room for pull_request/push, mq for a merge_group entry - runs-on: ${{ needs.workspace-test-arch.outputs.tier == 'full' && (github.event_name == 'merge_group' && fromJSON('["self-hosted","Linux","X64","mq"]') || fromJSON('["self-hosted","Linux","X64","clean-room"]')) || (github.event_name == 'merge_group' && fromJSON('["self-hosted","Linux","mq"]') || fromJSON('["self-hosted","Linux","clean-room"]')) }} + # same lanes as x86-main: clean-room for pull_request/push, mq for a merge_group entry + runs-on: ${{ github.event_name == 'merge_group' && fromJSON('["self-hosted","Linux","X64","mq"]') || fromJSON('["self-hosted","Linux","X64","clean-room"]') }} timeout-minutes: 180 strategy: fail-fast: false @@ -546,7 +523,6 @@ jobs: set -euo pipefail d="$RUNNER_TEMP/sigma" python3 scripts/ci/sigma_executed.py --self-test - bash scripts/ci/sigma_arch_uniform.sh --self-test # ONE shard count: the pins in ci/sections.yml (check_shard_count_single_source.sh) SHARDS="$(awk -v ev="$GITHUB_EVENT_NAME" '/^matrix-pins:/{p=1;next} p&&/^ workspace-test-shard:/{k="pr";next} p&&/^ workspace-test-shard-merge_group:/{k="mg";next} p&&/^ [a-z]/{k=""} k&&/^ - \{shard:/{c[k]++} END{print (ev=="merge_group"&&c["mg"]?c["mg"]:c["pr"])+0}' ci/sections.yml)" [ "${SHARDS:-0}" -ge 1 ] 2>/dev/null || { echo "::error::Σ: no workspace-test-shard pins"; exit 1; } @@ -563,8 +539,6 @@ jobs: [ "$#" -eq "$SHARDS" ] || { echo "::error::Σ: $# tier files for $SHARDS shards:$tiers"; exit 1; } for t in "$@"; do [ "$t" = "$1" ] || { echo "::error::Σ: the shards disagree on the tier:$tiers"; exit 1; }; done [ "$1" = full ] || { echo "every shard ran tier '$1': the lib/explicit Σ is not owed"; exit 0; } - # #5882: a full tier on two architectures is RED (arch-gated tests, one universe): placement is workspace-test-arch's, this is the backstop - bash scripts/ci/sigma_arch_uniform.sh "$d" "$SHARDS" j=() l=() for n in $(seq 1 "$SHARDS"); do j+=(--junit "$d/sigma-shard-$n/lib.junit.xml") diff --git a/ci/sections.yml b/ci/sections.yml index 4632770e37..4cc17744ef 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -378,7 +378,66 @@ jobs: GH_TOKEN: ${{ github.token }} MG_BASE_SHA: ${{ github.event.merge_group.base_sha }} run: | - bash scripts/ci/decide_tier.sh + set -euo pipefail + args=(--event "${GITHUB_EVENT_NAME}") + # PMAT-1098 67-E2: the queue and a push to main re-derive their own + # diff from HEAD's FIRST PARENT (HEAD^1..HEAD is the PR's diff on the + # new base). actions/checkout is shallow, so that parent is not in the + # object store until we deepen -- and without it ci_test_tier.sh falls + # closed to `full` every single time, which would make the whole + # optimisation inert while looking like it worked. Best effort by + # design: if the deepen fails the decision still falls closed, and the + # warning below says so instead of the run silently paying the hour. + case "${GITHUB_EVENT_NAME}" in + merge_group) + # Operator 2026-10-17: the queue runs the quick tier over the entry's diff (base_sha = prior entry's head), + # MG_BASE_SHA..HEAD. Fetch the base; if that fails the tier falls closed to full. + bash scripts/ci/fetch_p6.sh --no-tags --depth=1 origin "${GITHUB_SHA}" "${MG_BASE_SHA}" 2>/dev/null \ + || bash scripts/ci/fetch_p6.sh --no-tags --depth=1 origin "${MG_BASE_SHA}" 2>/dev/null \ + || true + git cat-file -e "${MG_BASE_SHA}^{commit}" 2>/dev/null \ + || printf '::warning::merge_group base sha unavailable -- the tier falls closed to full\n' + ;; + push) + bash scripts/ci/fetch_p6.sh --no-tags --depth=2 origin "${GITHUB_SHA}" 2>/dev/null \ + || bash scripts/ci/fetch_p6.sh --no-tags --deepen=1 origin 2>/dev/null \ + || true + if git rev-parse -q --verify 'HEAD^1' >/dev/null 2>&1; then + printf 'first parent available: %s\n' "$(git rev-parse HEAD^1)" + else + printf '::warning::first parent unavailable after deepen -- the tier falls closed to full\n' + fi + ;; + esac + case "${GITHUB_EVENT_NAME}" in + pull_request) + bash scripts/ci/fetch_p6.sh --no-tags --depth=1 origin "+refs/heads/${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}" + # #5391: the PR's change is the diff from merge-base(base, HEAD), not the + # base tip -- main's later commits are not the PR's. --no-renames (#3664) + # lives in the helper. + bash scripts/ci/pr_touched_paths.sh "origin/${GITHUB_BASE_REF}" HEAD > "$RUNNER_TEMP/touched.txt" + args+=(--diff-from "$RUNNER_TEMP/touched.txt") + ;; + merge_group) + # Operator 2026-10-17 (cop window), verbatim: "only gating on tags/release". + # ci_test_tier.sh diffs MG_BASE_SHA..HEAD (env above) and selects like a PR. + ;; + push) + # T36: the merge queue already tested this very sha. Hand its + # workspace-test result to the tier script, which reuses it only + # on a success for HEAD itself. A failed lookup is the push's own + # tier, as before -- never a reuse, never a red step. + mg=$(bash scripts/ci_mg_workspace_result.sh "${GITHUB_REPOSITORY}" "${GITHUB_SHA}") || mg="" + if [ -n "$mg" ]; then + mapfile -t mgargs <<<"$mg" + args+=("${mgargs[@]}") + else + printf 'no merge_group workspace-test result for %s: the tier is decided from the push diff\n' "${GITHUB_SHA}" + fi + ;; + esac + bash scripts/ci_test_tier.sh "${args[@]}" | tee "$RUNNER_TEMP/tier.txt" + grep -E '^(tier|crates|targets|check_workspace|reason|cite)=' "$RUNNER_TEMP/tier.txt" >> "$GITHUB_OUTPUT" # #4502 row 2: lint_passes_on_real_contracts (aprender-contracts) requires the # refinement gate to RUN whenever CI is set, and that gate compares HEAD with # merge-base(HEAD, origin/main), else the origin/main tip. This section clones at @@ -715,7 +774,6 @@ jobs: sig="$RUNNER_TEMP/sigma" mkdir -p sigma-shard printf '%s\n' "$TIER" > sigma-shard/tier - uname -m > sigma-shard/arch if [ "$TIER" = full ]; then cp "$sig/lib.junit.xml" sigma-shard/lib.junit.xml cp "${FAT_SECTION_LOG:?the fat driver sets FAT_SECTION_LOG}" sigma-shard/section.log diff --git a/crates/aprender-serve/src/kernel_registry_parity.rs b/crates/aprender-serve/src/kernel_registry_parity.rs index bdb6e3a798..0f47c86e85 100644 --- a/crates/aprender-serve/src/kernel_registry_parity.rs +++ b/crates/aprender-serve/src/kernel_registry_parity.rs @@ -743,7 +743,6 @@ fn committed() -> Vec<(serde_json::Value, serde_json::Value)> { #[test] fn committed_parity_receipts_hold_on_this_host() { let r = registry().expect("registry"); - let mut no_arch_receipt = Vec::new(); for (entry, rc) in committed() { let id = rc["kernel_id"].as_str().expect("kernel_id"); let path = entry["receipt"].as_str().expect("receipt path"); @@ -790,22 +789,8 @@ fn committed_parity_receipts_hold_on_this_host() { seed: wl["seed"].as_u64().expect("seed"), trials: dim("trials"), }; + let bound = rc["tolerance_rel"].as_f64().expect("tolerance_rel"); let now = measure(kernel(id), w, false); - // A receipt's `tolerance_rel` is the bound measured on its own `host_arch`; libm and FMA - // contraction move the last ulps between architectures. Any other arch must carry its own - // measured bound under `per_arch..tolerance_rel` -- never a borrowed one, never a skip. - let bound = if rc["host_arch"] == host_arch() { - rc["tolerance_rel"].as_f64().expect("tolerance_rel") - } else if let Some(b) = rc["per_arch"][host_arch()]["tolerance_rel"].as_f64() { - b - } else { - no_arch_receipt.push(format!( - "{path}: no {} receipt (host measured max_rel_err {})", - host_arch(), - now.max_rel_err - )); - continue; - }; assert_eq!( rc["margin"]["verdict"], "pass", "{path}: KTEST-02 margin verdict (max ≤ 1 under EM-DOT)" @@ -857,11 +842,6 @@ fn committed_parity_receipts_hold_on_this_host() { p => panic!("{id}: no precision check for {p}"), } } - assert!( - no_arch_receipt.is_empty(), - "committed receipts lack a bound for this arch:\n{}", - no_arch_receipt.join("\n") - ); } /// `KREG_INPUT_SETS_OUT`: the input-set hashes recomputed from the tree at `KREG_GIT_SHA`, one diff --git a/evidence/kreg/parity/cpu.matvec.apr_q4.json b/evidence/kreg/parity/cpu.matvec.apr_q4.json index da9c1fc945..e7929dfd71 100644 --- a/evidence/kreg/parity/cpu.matvec.apr_q4.json +++ b/evidence/kreg/parity/cpu.matvec.apr_q4.json @@ -35,15 +35,6 @@ }, "oracle": "apr_format_decode_f64", "oracle_independent": true, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 5.568608963671898e-07, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 2e-6, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": null, "registry_precision": "f32", "schema": "kernel-parity-receipt/v1", diff --git a/evidence/kreg/parity/cpu.matvec.apr_q8.json b/evidence/kreg/parity/cpu.matvec.apr_q8.json index 9f6f4ac2cf..e95fb4c5c9 100644 --- a/evidence/kreg/parity/cpu.matvec.apr_q8.json +++ b/evidence/kreg/parity/cpu.matvec.apr_q8.json @@ -35,15 +35,6 @@ }, "oracle": "apr_format_decode_f64", "oracle_independent": true, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 5.982009671170112e-07, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 2e-6, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": null, "registry_precision": "f32", "schema": "kernel-parity-receipt/v1", diff --git a/evidence/kreg/parity/cpu.matvec.bf16.json b/evidence/kreg/parity/cpu.matvec.bf16.json index 57309f51c6..e6aa4240c0 100644 --- a/evidence/kreg/parity/cpu.matvec.bf16.json +++ b/evidence/kreg/parity/cpu.matvec.bf16.json @@ -33,15 +33,6 @@ }, "oracle": "le_decode_half_f64", "oracle_independent": true, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 4.068146011742945e-07, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 9e-7, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": null, "registry_precision": "f32", "schema": "kernel-parity-receipt/v1", diff --git a/evidence/kreg/parity/cpu.matvec.f16.json b/evidence/kreg/parity/cpu.matvec.f16.json index af25b8ed9d..17b3754d5a 100644 --- a/evidence/kreg/parity/cpu.matvec.f16.json +++ b/evidence/kreg/parity/cpu.matvec.f16.json @@ -33,15 +33,6 @@ }, "oracle": "le_decode_half_f64", "oracle_independent": true, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 3.920968817889049e-07, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 8e-7, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": null, "registry_precision": "f32", "schema": "kernel-parity-receipt/v1", diff --git a/evidence/kreg/parity/cpu.matvec.f32.json b/evidence/kreg/parity/cpu.matvec.f32.json index a41e599001..9926666b94 100644 --- a/evidence/kreg/parity/cpu.matvec.f32.json +++ b/evidence/kreg/parity/cpu.matvec.f32.json @@ -33,15 +33,6 @@ }, "oracle": "le_decode_half_f64", "oracle_independent": true, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 7.356022484897627e-07, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 2e-6, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": null, "registry_precision": "f32", "schema": "kernel-parity-receipt/v1", diff --git a/evidence/kreg/parity/cpu.matvec.q4_0.json b/evidence/kreg/parity/cpu.matvec.q4_0.json index d1a94f3c46..38ab583a76 100644 --- a/evidence/kreg/parity/cpu.matvec.q4_0.json +++ b/evidence/kreg/parity/cpu.matvec.q4_0.json @@ -35,15 +35,6 @@ }, "oracle": "in_tree_dequant_f64", "oracle_independent": false, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 0.003859434297246209, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 0.008, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": { "max_rel_err": 1.69613194525447e-7 }, diff --git a/evidence/kreg/parity/cpu.matvec.q4_k.json b/evidence/kreg/parity/cpu.matvec.q4_k.json index 0e1ccc3ba7..501b8c768a 100644 --- a/evidence/kreg/parity/cpu.matvec.q4_k.json +++ b/evidence/kreg/parity/cpu.matvec.q4_k.json @@ -36,15 +36,6 @@ }, "oracle": "in_tree_dequant_f64", "oracle_independent": false, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 0.005569836531516333, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 0.02, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": { "max_rel_err": 1.3720446505396093e-7 }, diff --git a/evidence/kreg/parity/cpu.matvec.q5_k.json b/evidence/kreg/parity/cpu.matvec.q5_k.json index d65c8a5ca9..01913cf231 100644 --- a/evidence/kreg/parity/cpu.matvec.q5_k.json +++ b/evidence/kreg/parity/cpu.matvec.q5_k.json @@ -36,15 +36,6 @@ }, "oracle": "in_tree_dequant_f64", "oracle_independent": false, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 1.3110594897372703e-06, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 3e-6, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": null, "registry_precision": "f32", "schema": "kernel-parity-receipt/v1", diff --git a/evidence/kreg/parity/cpu.matvec.q6_k.json b/evidence/kreg/parity/cpu.matvec.q6_k.json index ce60279cc6..220f53b4a7 100644 --- a/evidence/kreg/parity/cpu.matvec.q6_k.json +++ b/evidence/kreg/parity/cpu.matvec.q6_k.json @@ -36,15 +36,6 @@ }, "oracle": "in_tree_dequant_f64", "oracle_independent": false, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 1.6505971099429934e-6, - "source": "CI job 119280981403 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 4e-6, - "ts": "2026-10-21T14:37:43Z" - } - }, "quantized_activation_oracle": null, "registry_precision": "f32", "schema": "kernel-parity-receipt/v1", diff --git a/evidence/kreg/parity/cpu.matvec.q8_0.json b/evidence/kreg/parity/cpu.matvec.q8_0.json index 0f6306509e..b69febb636 100644 --- a/evidence/kreg/parity/cpu.matvec.q8_0.json +++ b/evidence/kreg/parity/cpu.matvec.q8_0.json @@ -36,15 +36,6 @@ }, "oracle": "in_tree_dequant_f64", "oracle_independent": false, - "per_arch": { - "aarch64": { - "host": "gx10-pool1", - "max_rel_err": 0.003738839019401381, - "source": "CI job 119289824035 (PR #5871 shard 1), same seed and workload", - "tolerance_rel": 0.008, - "ts": "2026-10-21T14:51:19Z" - } - }, "quantized_activation_oracle": { "max_rel_err": 2.0240994602880252e-7 }, diff --git a/scripts/check_ci_push_reuse.sh b/scripts/check_ci_push_reuse.sh index a62bb9021d..0532c5d4e3 100755 --- a/scripts/check_ci_push_reuse.sh +++ b/scripts/check_ci_push_reuse.sh @@ -285,13 +285,10 @@ hasnt "D11 pull_request ignores --mg-*: never reuse" "tier=reuse" "$OUT" # -- W: the CI wiring ------------------------------------------------------------------------------ SEC="$ROOT/ci/sections.yml" -DT="$ROOT/scripts/ci/decide_tier.sh" # the tier step body lives in one script (#5882): the section step and workspace-test-arch both run it -want "W0 the section step runs the shared decide_tier.sh" 1 \ - "$(grep -cF "bash scripts/ci/decide_tier.sh" "$SEC")" want "W1 the tier step's push arm calls the lookup for GITHUB_SHA" 1 \ - "$(grep -cF 'bash scripts/ci_mg_workspace_result.sh "${GITHUB_REPOSITORY}" "${GITHUB_SHA}"' "$DT")" + "$(grep -cF 'bash scripts/ci_mg_workspace_result.sh "${GITHUB_REPOSITORY}" "${GITHUB_SHA}"' "$SEC")" want "W2 a failed lookup is not fatal to the step (it falls to today's tier)" 1 \ - "$(grep -cF '"${GITHUB_SHA}") || mg=""' "$DT")" + "$(grep -cF '"${GITHUB_SHA}") || mg=""' "$SEC")" W3PAT="push) printf 'cited merge_group run: %s" if grep -qF "$W3PAT" "$SEC"; then w3=1; else w3=0; fi want "W3 the reuse step labels a push citation as a merge_group run, not a PR head" 1 "$w3" diff --git a/scripts/ci/decide_tier.sh b/scripts/ci/decide_tier.sh deleted file mode 100755 index 5cfa8a6d5d..0000000000 --- a/scripts/ci/decide_tier.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -# decide_tier.sh -- which test tier this run owes (BSE-17, PMAT-1077); the one body of the -# "Decide the test tier" step, shared by the workspace-test-shard section and by the -# workspace-test-arch job that places the shards (a full tier needs ONE architecture: the -# cross-shard Σ compares every shard with one universe list). Env: GITHUB_EVENT_NAME, -# GITHUB_SHA, GITHUB_REPOSITORY, GITHUB_BASE_REF, MG_BASE_SHA, GH_TOKEN, RUNNER_TEMP, GITHUB_OUTPUT. -set -euo pipefail -args=(--event "${GITHUB_EVENT_NAME}") -# PMAT-1098 67-E2: the queue and a push to main re-derive their own -# diff from HEAD's FIRST PARENT (HEAD^1..HEAD is the PR's diff on the -# new base). actions/checkout is shallow, so that parent is not in the -# object store until we deepen -- and without it ci_test_tier.sh falls -# closed to `full` every single time, which would make the whole -# optimisation inert while looking like it worked. Best effort by -# design: if the deepen fails the decision still falls closed, and the -# warning below says so instead of the run silently paying the hour. -case "${GITHUB_EVENT_NAME}" in - merge_group) - # Operator 2026-10-17: the queue runs the quick tier over the entry's diff (base_sha = prior entry's head), - # MG_BASE_SHA..HEAD. Fetch the base; if that fails the tier falls closed to full. - bash scripts/ci/fetch_p6.sh --no-tags --depth=1 origin "${GITHUB_SHA}" "${MG_BASE_SHA}" 2>/dev/null \ - || bash scripts/ci/fetch_p6.sh --no-tags --depth=1 origin "${MG_BASE_SHA}" 2>/dev/null \ - || true - git cat-file -e "${MG_BASE_SHA}^{commit}" 2>/dev/null \ - || printf '::warning::merge_group base sha unavailable -- the tier falls closed to full\n' - ;; - push) - bash scripts/ci/fetch_p6.sh --no-tags --depth=2 origin "${GITHUB_SHA}" 2>/dev/null \ - || bash scripts/ci/fetch_p6.sh --no-tags --deepen=1 origin 2>/dev/null \ - || true - if git rev-parse -q --verify 'HEAD^1' >/dev/null 2>&1; then - printf 'first parent available: %s\n' "$(git rev-parse HEAD^1)" - else - printf '::warning::first parent unavailable after deepen -- the tier falls closed to full\n' - fi - ;; -esac -case "${GITHUB_EVENT_NAME}" in - pull_request) - bash scripts/ci/fetch_p6.sh --no-tags --depth=1 origin "+refs/heads/${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}" - # #5391: the PR's change is the diff from merge-base(base, HEAD), not the - # base tip -- main's later commits are not the PR's. --no-renames (#3664) - # lives in the helper. - bash scripts/ci/pr_touched_paths.sh "origin/${GITHUB_BASE_REF}" HEAD > "$RUNNER_TEMP/touched.txt" - args+=(--diff-from "$RUNNER_TEMP/touched.txt") - ;; - merge_group) - # Operator 2026-10-17 (cop window), verbatim: "only gating on tags/release". - # ci_test_tier.sh diffs MG_BASE_SHA..HEAD (env above) and selects like a PR. - ;; - push) - # T36: the merge queue already tested this very sha. Hand its - # workspace-test result to the tier script, which reuses it only - # on a success for HEAD itself. A failed lookup is the push's own - # tier, as before -- never a reuse, never a red step. - mg=$(bash scripts/ci_mg_workspace_result.sh "${GITHUB_REPOSITORY}" "${GITHUB_SHA}") || mg="" - if [ -n "$mg" ]; then - mapfile -t mgargs <<<"$mg" - args+=("${mgargs[@]}") - else - printf 'no merge_group workspace-test result for %s: the tier is decided from the push diff\n' "${GITHUB_SHA}" - fi - ;; -esac -bash scripts/ci_test_tier.sh "${args[@]}" | tee "$RUNNER_TEMP/tier.txt" -grep -E '^(tier|crates|targets|check_workspace|reason|cite)=' "$RUNNER_TEMP/tier.txt" >> "$GITHUB_OUTPUT" diff --git a/scripts/ci/sigma_arch_uniform.sh b/scripts/ci/sigma_arch_uniform.sh deleted file mode 100755 index 29b0b7bf26..0000000000 --- a/scripts/ci/sigma_arch_uniform.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# sigma_arch_uniform.sh -- the FULL-tier Σ needs every shard on ONE architecture (#5882). -# -# The fan-in compares the union of every shard's executed ids with ONE universe, the list shard 1 -# made on ITS machine. Tests gated on target_arch (crates/aprender-zram-core/src/zstd/avx2.rs has 18 -# x86_64-only ones, 141 files carry a cfg) exist in one arch's list and not the other's, so a mixed -# x86 + aarch64 shard set is red or green by PLACEMENT, and a test owed on x86 whose hash slice drew -# an aarch64 shard runs nowhere. A per-arch compare cannot fix that: each arch's shards run only -# their own slices, so each arch would be owed tests it was never given. The sound rule is -# placement: ci.yml job workspace-test-arch decides the tier first and pins the full tier's shards to -# X64. This script is the backstop that keeps the rule true after the fact: at tier full, every shard -# must have staged x86_64; a missing, blank or other arch, or a blank tier, is RED. Any other agreed -# tier owes no Σ: not checked. -# -# sigma_arch_uniform.sh DIR SHARDS DIR/sigma-shard-N/{tier,arch}, N = 1..SHARDS -# sigma_arch_uniform.sh --self-test the case table (incl. the mixed placement that went red) -set -euo pipefail - -check() { # check DIR SHARDS - local d=$1 n=$2 i t tier arch first="" tiers="" arches="" - for i in $(seq 1 "$n"); do - [ -f "$d/sigma-shard-$i/tier" ] || { echo "::error::Σ arch: shard $i staged no tier file"; return 1; } - tier=$(tr -d '[:space:]' < "$d/sigma-shard-$i/tier") - [ -n "$tier" ] || { echo "::error::Σ arch: shard $i staged a blank tier (fail closed, never read as 'not full')"; return 1; } - tiers="$tiers $tier" - done - for t in $tiers; do [ "$t" = full ] || { echo "Σ arch: tier '$t' in this run, the lib/explicit Σ is not owed, arch not checked"; return 0; }; done - # Agreement alone is not enough: 12 aarch64 shards agree and avx2.rs's x86-only tests run nowhere. - # The full tier is pinned to X64, so every shard must say x86_64. - for i in $(seq 1 "$n"); do - [ -f "$d/sigma-shard-$i/arch" ] || { echo "::error::Σ arch: shard $i staged no arch at the full tier (a missing arch is RED, never uniform)"; return 1; } - arch=$(tr -d '[:space:]' < "$d/sigma-shard-$i/arch") - [ -n "$arch" ] || { echo "::error::Σ arch: shard $i staged a blank arch at the full tier (fail closed)"; return 1; } - arches="$arches $i=$arch" - if [ "$arch" != x86_64 ]; then - echo "::error::Σ arch: full tier shard $i ran on $arch, not x86_64:$arches" - echo "::error::Σ arch: x86_64-gated tests are owed and would run nowhere; workspace-test-arch must pin the full tier to X64" - return 1 - fi - done - echo "Σ arch: every shard of the full tier ran on x86_64" -} - -self_test() { - local td rows=0 bad=0 want got - td=$(mktemp -d) - mk() { # mk TIER ARCH... -> one shard dir per arch argument ("-" = no arch file) - local t=$1 i=0 a; shift; rm -rf "${td:?}/s"; mkdir -p "$td/s" - for a in "$@"; do - i=$((i + 1)); mkdir -p "$td/s/sigma-shard-$i"; printf '%s\n' "$t" > "$td/s/sigma-shard-$i/tier" - [ "$a" = - ] || printf '%s\n' "$a" > "$td/s/sigma-shard-$i/arch" - done - } - row() { # row WANT LABEL TIER ARCH... - want=$1; local label=$2; shift 2; mk "$@" - set +e; check "$td/s" "$(($# - 1))" > /dev/null 2>&1; got=$?; set -e - rows=$((rows + 1)); [ "$got" = "$want" ] || bad=$((bad + 1)) - printf '%s want_rc=%s got_rc=%s %s\n' "$([ "$got" = "$want" ] && echo 'ok ' || echo 'BAD')" "$want" "$got" "$label" - } - row 0 "full, 12 shards all x86_64" full x86_64 x86_64 x86_64 x86_64 x86_64 x86_64 x86_64 x86_64 x86_64 x86_64 x86_64 x86_64 - row 1 "full, 12 shards all aarch64: they agree, but x86-only tests run nowhere" full aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 - row 1 "full, one shard staged a BLANK arch" full x86_64 "" x86_64 - row 1 "full, every shard staged a blank arch" full "" "" - row 1 "full, one shard staged a whitespace-only arch" full x86_64 " " x86_64 - row 1 "a blank tier refuses (never read as 'not full')" "" x86_64 x86_64 - row 1 "full, MIXED: shard 3 x86_64, the rest aarch64 (run 39620701544's placement)" full aarch64 aarch64 x86_64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 aarch64 - row 1 "full, MIXED: shard 1 x86_64, the rest aarch64" full x86_64 aarch64 aarch64 aarch64 - row 1 "full, one shard staged no arch" full x86_64 - x86_64 - row 0 "quick, MIXED: no Σ owed, not checked (the PR tier stays arch-neutral)" quick aarch64 x86_64 aarch64 - mk full x86_64; rm -f "${td:?}/s/sigma-shard-1/tier" - set +e; check "$td/s" 1 > /dev/null 2>&1; got=$?; set -e - rows=$((rows + 1)); [ "$got" = 1 ] || bad=$((bad + 1)) - printf '%s want_rc=1 got_rc=%s a missing tier file refuses\n' "$([ "$got" = 1 ] && echo 'ok ' || echo 'BAD')" "$got" - # Why a per-arch compare is not the fix, on the real comparator: shard 1's list is aarch64 (no - # avx2 test), the x86 shard executed one -> "executed but not in the universe". - local j="$td/j.xml" l="$td/l.json" - printf '' > "$j" - printf '{"rust-suites":{"z":{"testcases":{"a::neon":{"filter-match":{"status":"matches"}}}}}}' > "$l" - set +e; python3 "$(dirname "$0")/sigma_executed.py" nextest --junit "$j" --list-json "$l" > /dev/null 2>&1; got=$?; set -e - rows=$((rows + 1)); [ "$got" = 1 ] || bad=$((bad + 1)) - printf '%s want_rc=1 got_rc=%s sigma_executed over a mixed-arch pair (x86 test, aarch64 universe) is RED\n' "$([ "$got" = 1 ] && echo 'ok ' || echo 'BAD')" "$got" - rm -rf "${td:?}" - echo "Σ arch self-test: $((rows - bad))/$rows rows as expected" - [ "$bad" = 0 ] -} - -case "${1:-}" in - --self-test) self_test ;; - "" | -h | --help) sed -n 2,16p "$0" ;; - *) check "$1" "${2:?SHARDS}" ;; -esac diff --git a/scripts/ci_mg_reuse.sh b/scripts/ci_mg_reuse.sh index 5f133ce19e..03aa3c1e37 100755 --- a/scripts/ci_mg_reuse.sh +++ b/scripts/ci_mg_reuse.sh @@ -528,13 +528,11 @@ STUB # Each row is compared as the EXACT line (trailing comment stripped), so a third state cannot slip in. poolrows() { # poolrows WF -> one line per violated pool row local j l want - local plain="runs-on: [self-hosted, Linux, clean-room]" # arch-neutral job: cargo-deny, no X64 pin + local plain="runs-on: [self-hosted, Linux, X64, clean-room]" local lane="runs-on: \${{ github.event_name == 'merge_group' && fromJSON('[\"self-hosted\",\"Linux\",\"X64\",\"mq\"]') || fromJSON('[\"self-hosted\",\"Linux\",\"X64\",\"clean-room\"]') }}" - # #5882: the full tier is pinned X64 (one universe for the Σ), every other tier stays arch-neutral - local nlane="runs-on: \${{ needs.workspace-test-arch.outputs.tier == 'full' && (github.event_name == 'merge_group' && fromJSON('[\"self-hosted\",\"Linux\",\"X64\",\"mq\"]') || fromJSON('[\"self-hosted\",\"Linux\",\"X64\",\"clean-room\"]')) || (github.event_name == 'merge_group' && fromJSON('[\"self-hosted\",\"Linux\",\"mq\"]') || fromJSON('[\"self-hosted\",\"Linux\",\"clean-room\"]')) }}" - for j in x86-main x86-main-advisories workspace-test-arch workspace-test-shard; do + for j in x86-main x86-main-advisories workspace-test-shard; do l=$(awk -v j=" $j:" '$0 == j {f = 1; next} f && /^ [A-Za-z0-9_-]+:$/ {exit} f' "$1" 2> /dev/null | grep -m1 '^ runs-on:' | sed -E 's/^ +//; s/ +#.*$//') - want=$plain; case $j in x86-main) want=$lane ;; workspace-test-shard) want=$nlane ;; esac + want=$plain; case $j in x86-main | workspace-test-shard) want=$lane ;; esac if [ "$l" != "$want" ]; then echo "$j does not land on the clean-room pool (mq on merge_group for x86-main/workspace-test-shard) (runs-on: $l)"; fi done @@ -554,11 +552,8 @@ STUB poolmut pool-advisories-event-conditional "does not land on the clean-room pool" '/^ x86-main-advisories:$/,/^ steps:/{s/^ runs-on: .*/ runs-on: ${{ github.event_name == '"'"'merge_group'"'"' \&\& fromJSON('"'"'["clean-room"]'"'"') || fromJSON('"'"'["clean-room"]'"'"') }}/}' poolmut mq-lane-lost-on-x86-main "does not land on the clean-room pool" '/^ x86-main:$/,/^ steps:/{s/"mq"/"clean-room"/}' poolmut mq-lane-dropped-on-shard "does not land on the clean-room pool" '/^ workspace-test-shard:$/,/^ steps:/{s/^ runs-on: .*/ runs-on: [self-hosted, Linux, X64, clean-room]/}' - poolmut pr-path-takes-mq "does not land on the clean-room pool" '/^ workspace-test-shard:$/,/^ steps:/{s/\|\| fromJSON\(.\["self-hosted","Linux","clean-room"\].\)/|| fromJSON('"'"'["self-hosted","Linux","mq"]'"'"')/}' + poolmut pr-path-takes-mq "does not land on the clean-room pool" '/^ workspace-test-shard:$/,/^ steps:/{s/\|\| fromJSON\(.\["self-hosted","Linux","X64","clean-room"\].\)/|| fromJSON('"'"'["self-hosted","Linux","X64","mq"]'"'"')/}' poolmut mq-lane-third-label "does not land on the clean-room pool" '/^ x86-main:$/,/^ steps:/{s/"X64","mq"/"X64","mq","pr-only"/}' - poolmut full-tier-loses-x64-pin "does not land on the clean-room pool" '/^ workspace-test-shard:$/,/^ steps:/{s/"Linux","X64",/"Linux",/g}' - poolmut every-tier-pinned-x64 "does not land on the clean-room pool" '/^ workspace-test-shard:$/,/^ steps:/{s/^ runs-on: .*/ runs-on: ${{ github.event_name == '"'"'merge_group'"'"' \&\& fromJSON('"'"'["self-hosted","Linux","X64","mq"]'"'"') || fromJSON('"'"'["self-hosted","Linux","X64","clean-room"]'"'"') }}/}' - poolmut tier-job-off-the-pool "does not land on the clean-room pool" '/^ workspace-test-arch:$/,/^ steps:/{s/clean-room/build/}' # S1 (20X): mg-reuse runs on push only, so on merge_group it is SKIPPED and every dependent # runs only because its own `if:` carries !cancelled()/always() (a skipped need otherwise skips From f6209d4fb1543f6e7985bb1f380e76acab1e6124 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 05:49:56 +0200 Subject: [PATCH 03/10] ci_scope: replace inline python3/yaml in self-test with yq (never rule, Refs #5744) Co-Authored-By: Claude Sonnet 5.5 --- scripts/ci_scope.sh | 48 ++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/scripts/ci_scope.sh b/scripts/ci_scope.sh index a57ce7d1bb..07a2f99e2e 100755 --- a/scripts/ci_scope.sh +++ b/scripts/ci_scope.sh @@ -63,33 +63,27 @@ self_test() { t "hidden file under docs" "skip=0 reason=full" false docs/archive/.hidden t "prefix lookalike" "skip=0 reason=full" false docs/archive-x/a.md t "empty diff is full" "skip=0 reason=full" false - local wf="$REPO_ROOT/.github/workflows/ci.yml" py - py=$(python3 - "$wf" <<'PY' -import sys, yaml -w = yaml.safe_load(open(sys.argv[1])) -on = w.get(True, w.get("on")) -bad = [] -pr = on.get("pull_request") or {} -if "paths-ignore" in pr or "paths" in pr: bad.append("pull_request has a paths filter (stuck required checks)") -if "ready_for_review" not in (pr.get("types") or []): bad.append("pull_request types lack ready_for_review") -for k in ("push", "merge_group"): - v = on.get(k) or {} - if "paths-ignore" in v or "paths" in v: bad.append(k + " has a paths filter") -j = w["jobs"] -sc = j.get("scope") -if not sc or sc.get("if") != "${{ github.event_name == 'pull_request' }}": bad.append("scope job missing or not pull_request-only") -for name in ("x86-main", "workspace-test-shard"): - if "scope" not in (j[name].get("needs") or []) or "needs.scope.outputs.skip != '1'" not in str(j[name].get("if")): bad.append(name + " does not honour scope") -for name in ("ci-gate", "gate", "workspace-test"): - if "scope" not in (j[name].get("needs") or []): bad.append(name + " does not need scope") - if name != "workspace-test" and "needs.scope.outputs.skip" not in str(j[name]): bad.append(name + " does not read scope") -if "needs.scope.outputs.skip" not in str(j["workspace-test"]): bad.append("workspace-test does not read scope") -c = w.get("concurrency") or {} -if "pull_request" not in str(c.get("cancel-in-progress")): bad.append("concurrency no longer cancels superseded PR runs") -print("\n".join(bad)) -PY - ) || { echo "FAIL wiring: cannot read ci.yml"; fail=1; } - [ -z "$py" ] || { printf 'FAIL wiring: %s\n' "$py"; fail=1; } + local wf="$REPO_ROOT/.github/workflows/ci.yml" bad="" n + if ! command -v yq > /dev/null 2>&1 || ! yq '.jobs' "$wf" > /dev/null 2>&1; then + echo "FAIL wiring: cannot read ci.yml (yq missing or file unparsable)"; fail=1 + else + y() { yq -r "$1" "$wf"; } + [ "$(y '.on.pull_request | has("paths") or has("paths-ignore")')" = false ] || bad="$bad; pull_request has a paths filter (stuck required checks)" + [ "$(y '.on.pull_request.types // [] | contains(["ready_for_review"])')" = true ] || bad="$bad; pull_request types lack ready_for_review" + for n in push merge_group; do + [ "$(y ".on.$n // {} | has(\"paths\") or has(\"paths-ignore\")")" = false ] || bad="$bad; $n has a paths filter" + done + [ "$(y '.jobs.scope.if')" = "\${{ github.event_name == 'pull_request' }}" ] || bad="$bad; scope job missing or not pull_request-only" + for n in x86-main workspace-test-shard; do + [ "$(y ".jobs.\"$n\".needs // [] | contains([\"scope\"])")" = true ] && y ".jobs.\"$n\".if" | grep -qF "needs.scope.outputs.skip != '1'" || bad="$bad; $n does not honour scope" + done + for n in ci-gate gate workspace-test; do + [ "$(y ".jobs.\"$n\".needs // [] | contains([\"scope\"])")" = true ] || bad="$bad; $n does not need scope" + y ".jobs.\"$n\"" | grep -qF 'needs.scope.outputs.skip' || bad="$bad; $n does not read scope" + done + y '.concurrency."cancel-in-progress"' | grep -qF pull_request || bad="$bad; concurrency no longer cancels superseded PR runs" + fi + [ -z "$bad" ] || { printf 'FAIL wiring: %s\n' "${bad#; }"; fail=1; } [ "$fail" -eq 0 ] && echo "ci_scope: self-test ok" return "$fail" } From 8d148928907a2b747c42bc488ba7deefaa64d296 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 05:55:31 +0200 Subject: [PATCH 04/10] ci_scope: images-only allowlist, --no-renames, parenthesise yq has() (triage 5744) Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 2 +- scripts/ci_scope.sh | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96f80752f5..2ac03928c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: run: | set -uo pipefail # Any failure to read the diff is `full`: a skip needs a positive read. - if ! paths=$(git diff --name-only "$BASE...$HEAD"); then paths=""; fi + if ! paths=$(git diff --no-renames --name-only "$BASE...$HEAD"); then paths=""; fi out=$(printf '%s\n' "$paths" | bash scripts/ci_scope.sh classify false) || out="skip=0 reason=full" echo "$out" printf '%s\n' "$out" | sed 's/ /\n/' >> "$GITHUB_OUTPUT" diff --git a/scripts/ci_scope.sh b/scripts/ci_scope.sh index 07a2f99e2e..129d88703e 100755 --- a/scripts/ci_scope.sh +++ b/scripts/ci_scope.sh @@ -18,12 +18,13 @@ # # DOCS-ONLY is an ALLOWLIST of trees no test or gate reads, found by searching the # repo for readers (docs/audits, docs/roadmaps, docs/specifications, docs/findings, -# docs/BEATS.md, README.md, CLAUDE.md are READ by gates and are therefore NOT here). +# docs/BEATS.md, README.md, CLAUDE.md are READ by gates and are therefore NOT here; neither is any +# docs/**/*.md, which check_no_claim_literals.sh and check_perf_claims_cite_receipts.sh scan). # Every changed path must match; any other path, an empty list or a `..` is `full`. # Growing the list is a gate-content change: prove no reader first. set -uo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" || exit 2 -DOCS_ONLY_RE='^docs/(archive|postmortems|reports|qa-reports|images)/[^.][^[:space:]]*$' +DOCS_ONLY_RE='^docs/images/[^.][^[:space:]]*\.(png|jpg|jpeg|gif|svg|webp)$' classify() { # classify ; paths on stdin local draft=${1:-false} n=0 p @@ -46,10 +47,12 @@ self_test() { } t "draft skips" "skip=1 reason=draft" true crates/a/src/lib.rs t "draft, no paths" "skip=1 reason=draft" true - t "docs archive only" "skip=1 reason=docs-only" false docs/archive/a.md docs/images/x.png - t "postmortem only" "skip=1 reason=docs-only" false docs/postmortems/2026-10-19.md + t "images only" "skip=1 reason=docs-only" false docs/images/x.png docs/images/y.svg + t "docs archive md is gated" "skip=0 reason=full" false docs/archive/a.md + t "images md is gated" "skip=0 reason=full" false docs/images/README.md + t "postmortem md is gated" "skip=0 reason=full" false docs/postmortems/2026-10-19.md t "code" "skip=0 reason=full" false crates/a/src/lib.rs - t "docs + code" "skip=0 reason=full" false docs/archive/a.md crates/a/src/lib.rs + t "images + code" "skip=0 reason=full" false docs/images/a.png crates/a/src/lib.rs t "README is gated" "skip=0 reason=full" false README.md t "CLAUDE.md is gated" "skip=0 reason=full" false CLAUDE.md t "BEATS is gated" "skip=0 reason=full" false docs/BEATS.md @@ -59,19 +62,19 @@ self_test() { t "findings ledger is gated" "skip=0 reason=full" false docs/findings/w1.jsonl t "workflow file" "skip=0 reason=full" false .github/workflows/ci.yml t "contracts" "skip=0 reason=full" false contracts/a-v1.yaml - t "dot-dot escape" "skip=0 reason=full" false docs/archive/../../Cargo.toml - t "hidden file under docs" "skip=0 reason=full" false docs/archive/.hidden - t "prefix lookalike" "skip=0 reason=full" false docs/archive-x/a.md + t "dot-dot escape" "skip=0 reason=full" false docs/images/../../Cargo.toml + t "hidden file under docs" "skip=0 reason=full" false docs/images/.hidden + t "prefix lookalike" "skip=0 reason=full" false docs/images-x/a.png t "empty diff is full" "skip=0 reason=full" false local wf="$REPO_ROOT/.github/workflows/ci.yml" bad="" n if ! command -v yq > /dev/null 2>&1 || ! yq '.jobs' "$wf" > /dev/null 2>&1; then echo "FAIL wiring: cannot read ci.yml (yq missing or file unparsable)"; fail=1 else y() { yq -r "$1" "$wf"; } - [ "$(y '.on.pull_request | has("paths") or has("paths-ignore")')" = false ] || bad="$bad; pull_request has a paths filter (stuck required checks)" + [ "$(y '.on.pull_request | (has("paths") or has("paths-ignore"))')" = false ] || bad="$bad; pull_request has a paths filter (stuck required checks)" [ "$(y '.on.pull_request.types // [] | contains(["ready_for_review"])')" = true ] || bad="$bad; pull_request types lack ready_for_review" for n in push merge_group; do - [ "$(y ".on.$n // {} | has(\"paths\") or has(\"paths-ignore\")")" = false ] || bad="$bad; $n has a paths filter" + [ "$(y ".on.$n // {} | (has(\"paths\") or has(\"paths-ignore\"))")" = false ] || bad="$bad; $n has a paths filter" done [ "$(y '.jobs.scope.if')" = "\${{ github.event_name == 'pull_request' }}" ] || bad="$bad; scope job missing or not pull_request-only" for n in x86-main workspace-test-shard; do @@ -82,6 +85,7 @@ self_test() { y ".jobs.\"$n\"" | grep -qF 'needs.scope.outputs.skip' || bad="$bad; $n does not read scope" done y '.concurrency."cancel-in-progress"' | grep -qF pull_request || bad="$bad; concurrency no longer cancels superseded PR runs" + grep -qF 'git diff --no-renames --name-only' "$wf" || bad="$bad; scope diff may report renames by new path only (fails open)" fi [ -z "$bad" ] || { printf 'FAIL wiring: %s\n' "${bad#; }"; fail=1; } [ "$fail" -eq 0 ] && echo "ci_scope: self-test ok" From e9135049ceefe0d1f73b6d58b4ddd5082bbf1bc8 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 06:14:52 +0200 Subject: [PATCH 05/10] check_ci_draft_skip: x86-main-unguarded mutant removes the draft clause wherever it sits (Refs #5744) Co-Authored-By: Claude Sonnet 5.5 --- scripts/check_ci_draft_skip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_ci_draft_skip.sh b/scripts/check_ci_draft_skip.sh index 8db62fc5ac..0b24918979 100755 --- a/scripts/check_ci_draft_skip.sh +++ b/scripts/check_ci_draft_skip.sh @@ -64,7 +64,7 @@ self_test() { if got="$(check "$td/m.yml")"; then echo "FAIL mutant $name stayed GREEN"; fails=1; else echo "ok red mutant $name"; fi } mutant no-ready-for-review 's/, ready_for_review\]/]/' - mutant x86-main-unguarded 's/ && github\.event\.pull_request\.draft != true \}\}/ }}/' + mutant x86-main-unguarded '/outputs\.x86 != .1. &&/s/ && github\.event\.pull_request\.draft != true//' mutant shard-unguarded '/^ workspace-test-shard:$/,/runs-on:/{/draft != true/d;}' mutant gate-no-draft-exit '/echo "gate: draft PR/d' mutant ci-gate-no-draft-flag '/^ ci-gate:/,/^ gate:/{/DRAFT: \$/d}' From 076567b1443392e26dccdaffc1cbf8d59c12e833 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 06:43:33 +0200 Subject: [PATCH 06/10] fix(ci): ci_scope self-test reads ci.yml with awk, not yq (absent on clean-room runner) Root cause of mg-reuse-self-test red (run 39721317355): 'cannot read ci.yml (yq missing)'. All 7 wiring mutants stay RED. Co-Authored-By: Claude Sonnet 5.5 --- scripts/ci_scope.sh | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/scripts/ci_scope.sh b/scripts/ci_scope.sh index 129d88703e..a7873d8fe6 100755 --- a/scripts/ci_scope.sh +++ b/scripts/ci_scope.sh @@ -67,24 +67,31 @@ self_test() { t "prefix lookalike" "skip=0 reason=full" false docs/images-x/a.png t "empty diff is full" "skip=0 reason=full" false local wf="$REPO_ROOT/.github/workflows/ci.yml" bad="" n - if ! command -v yq > /dev/null 2>&1 || ! yq '.jobs' "$wf" > /dev/null 2>&1; then - echo "FAIL wiring: cannot read ci.yml (yq missing or file unparsable)"; fail=1 + # No yq on the clean-room runner (run 39721317355): read the YAML with awk. Full-line comments are + # stripped first (the ci.yml comments name `paths`). blk KEY INDENT = body of the top-level (INDENT 0) + # or second-level (INDENT 2) key KEY, up to the next key at the same or lower indent. + if [ ! -r "$wf" ] || ! grep -q '^jobs:' "$wf"; then + echo "FAIL wiring: cannot read ci.yml"; fail=1 else - y() { yq -r "$1" "$wf"; } - [ "$(y '.on.pull_request | (has("paths") or has("paths-ignore"))')" = false ] || bad="$bad; pull_request has a paths filter (stuck required checks)" - [ "$(y '.on.pull_request.types // [] | contains(["ready_for_review"])')" = true ] || bad="$bad; pull_request types lack ready_for_review" + nc() { grep -vE '^[[:space:]]*#' "$wf"; } + top() { nc | awk -v k="$1" '$0 ~ "^"k":" {f=1; next} f && /^[^ ]/ {f=0} f'; } + job() { top jobs | awk -v k="$1" '$0 == " "k":" || $0 == " \""k"\":" {f=1; next} f && /^ [^ ]/ {f=0} f'; } + ev() { top on | awk -v k="$1" '$0 ~ "^ "k":" {f=1; next} f && /^ [^ ]/ {f=0} f'; } + top on | grep -qE '^ pull_request:' || bad="$bad; no pull_request trigger" + ev pull_request | grep -qE '^ paths(-ignore)?:' && bad="$bad; pull_request has a paths filter (stuck required checks)" + ev pull_request | grep -qE '^ types:.*ready_for_review' || bad="$bad; pull_request types lack ready_for_review" for n in push merge_group; do - [ "$(y ".on.$n // {} | (has(\"paths\") or has(\"paths-ignore\"))")" = false ] || bad="$bad; $n has a paths filter" + ev "$n" | grep -qE '^ paths(-ignore)?:' && bad="$bad; $n has a paths filter" done - [ "$(y '.jobs.scope.if')" = "\${{ github.event_name == 'pull_request' }}" ] || bad="$bad; scope job missing or not pull_request-only" + job scope | grep -qF "if: \${{ github.event_name == 'pull_request' }}" || bad="$bad; scope job missing or not pull_request-only" for n in x86-main workspace-test-shard; do - [ "$(y ".jobs.\"$n\".needs // [] | contains([\"scope\"])")" = true ] && y ".jobs.\"$n\".if" | grep -qF "needs.scope.outputs.skip != '1'" || bad="$bad; $n does not honour scope" + job "$n" | grep -E '^ needs:' | grep -qF scope && job "$n" | grep -E '^ if:' | grep -qF "needs.scope.outputs.skip != '1'" || bad="$bad; $n does not honour scope" done for n in ci-gate gate workspace-test; do - [ "$(y ".jobs.\"$n\".needs // [] | contains([\"scope\"])")" = true ] || bad="$bad; $n does not need scope" - y ".jobs.\"$n\"" | grep -qF 'needs.scope.outputs.skip' || bad="$bad; $n does not read scope" + job "$n" | grep -E '^ needs:' | grep -qF scope || bad="$bad; $n does not need scope" + job "$n" | grep -qF 'needs.scope.outputs.skip' || bad="$bad; $n does not read scope" done - y '.concurrency."cancel-in-progress"' | grep -qF pull_request || bad="$bad; concurrency no longer cancels superseded PR runs" + top concurrency | grep -E '^ cancel-in-progress:' | grep -qF pull_request || bad="$bad; concurrency no longer cancels superseded PR runs" grep -qF 'git diff --no-renames --name-only' "$wf" || bad="$bad; scope diff may report renames by new path only (fails open)" fi [ -z "$bad" ] || { printf 'FAIL wiring: %s\n' "${bad#; }"; fail=1; } From 3a2e80aa43917d07236b4e9bd8c99fc2d7b9612f Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 06:59:03 +0200 Subject: [PATCH 07/10] fix(ci): ci_scope wiring reader pins job-level lines, strips trailing comments, 13 in-self-test mutants Triage 65e1ed9c52: trailing-comment cancel-in-progress, step-level if, one-line push paths now RED. Co-Authored-By: Claude Sonnet 5.5 --- scripts/ci_scope.sh | 69 +++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/scripts/ci_scope.sh b/scripts/ci_scope.sh index a7873d8fe6..6cfa4d37a5 100755 --- a/scripts/ci_scope.sh +++ b/scripts/ci_scope.sh @@ -66,35 +66,68 @@ self_test() { t "hidden file under docs" "skip=0 reason=full" false docs/images/.hidden t "prefix lookalike" "skip=0 reason=full" false docs/images-x/a.png t "empty diff is full" "skip=0 reason=full" false - local wf="$REPO_ROOT/.github/workflows/ci.yml" bad="" n - # No yq on the clean-room runner (run 39721317355): read the YAML with awk. Full-line comments are - # stripped first (the ci.yml comments name `paths`). blk KEY INDENT = body of the top-level (INDENT 0) - # or second-level (INDENT 2) key KEY, up to the next key at the same or lower indent. + local wf="${CI_SCOPE_WF:-$REPO_ROOT/.github/workflows/ci.yml}" bad="" n body line + # No yq on the clean-room runner (run 39721317355): read the YAML with awk. Full-line and trailing + # ` #` comments are stripped first (ci.yml comments name `paths`, and a trailing comment must not + # satisfy a check). Results are captured and matched with [[ ]], never piped into `grep -q`. + # top KEY = body of top-level KEY; job J / ev E = body of that job / trigger; jl J KEY = the job-level + # (4-space) KEY line only, so a step-level `if:` or `needs:` cannot stand in for it. if [ ! -r "$wf" ] || ! grep -q '^jobs:' "$wf"; then echo "FAIL wiring: cannot read ci.yml"; fail=1 else - nc() { grep -vE '^[[:space:]]*#' "$wf"; } - top() { nc | awk -v k="$1" '$0 ~ "^"k":" {f=1; next} f && /^[^ ]/ {f=0} f'; } + nc() { sed -E -e '/^[[:space:]]*#/d' -e 's/[[:space:]]+#.*$//' -e 's/[[:space:]]+$//' "$wf"; } + top() { nc | awk -v k="$1" '$0 == k":" {f=1; next} f && /^[^ ]/ {f=0} f'; } job() { top jobs | awk -v k="$1" '$0 == " "k":" || $0 == " \""k"\":" {f=1; next} f && /^ [^ ]/ {f=0} f'; } - ev() { top on | awk -v k="$1" '$0 ~ "^ "k":" {f=1; next} f && /^ [^ ]/ {f=0} f'; } - top on | grep -qE '^ pull_request:' || bad="$bad; no pull_request trigger" - ev pull_request | grep -qE '^ paths(-ignore)?:' && bad="$bad; pull_request has a paths filter (stuck required checks)" - ev pull_request | grep -qE '^ types:.*ready_for_review' || bad="$bad; pull_request types lack ready_for_review" - for n in push merge_group; do - ev "$n" | grep -qE '^ paths(-ignore)?:' && bad="$bad; $n has a paths filter" + ev() { top on | awk -v k="$1" '$0 == " "k":" {f=1; next} f && /^ [^ ]/ {f=0} f'; } + jl() { job "$1" | awk -v k="$2" 'index($0, " "k": ") == 1 {print; exit}'; } + has_scope() { [[ "$1" =~ [\[,][[:space:]]*scope[[:space:]]*[],] ]]; } + [ "$(nc | grep -cE '^on:$')" = 1 ] || bad="$bad; on: is not a block mapping" + for n in push pull_request merge_group; do + [ "$(top on | grep -cE "^ $n:$")" = 1 ] || bad="$bad; trigger $n is not a plain block key" + body="$(ev "$n")" + [[ "$body" =~ (^|[^A-Za-z_-])paths(-ignore)?[[:space:]]*: ]] && bad="$bad; $n has a paths filter (stuck required checks)" done - job scope | grep -qF "if: \${{ github.event_name == 'pull_request' }}" || bad="$bad; scope job missing or not pull_request-only" + line="$(ev pull_request | awk '/^ types:/ {print; exit}')" + [[ "$line" =~ ^[[:space:]]+types:[[:space:]]*\[.*ready_for_review.*\]$ ]] || bad="$bad; pull_request types lack ready_for_review" + [ "$(jl scope if)" = " if: \${{ github.event_name == 'pull_request' }}" ] || bad="$bad; scope job missing or not pull_request-only" for n in x86-main workspace-test-shard; do - job "$n" | grep -E '^ needs:' | grep -qF scope && job "$n" | grep -E '^ if:' | grep -qF "needs.scope.outputs.skip != '1'" || bad="$bad; $n does not honour scope" + has_scope "$(jl "$n" needs)" && [[ "$(jl "$n" if)" == *"needs.scope.outputs.skip != '1'"* ]] || bad="$bad; $n does not honour scope" done - for n in ci-gate gate workspace-test; do - job "$n" | grep -E '^ needs:' | grep -qF scope || bad="$bad; $n does not need scope" - job "$n" | grep -qF 'needs.scope.outputs.skip' || bad="$bad; $n does not read scope" + has_scope "$(jl workspace-test needs)" && [[ "$(jl workspace-test if)" == *"needs.scope.outputs.skip != '1'"* ]] || bad="$bad; workspace-test does not honour scope" + for n in ci-gate gate; do + has_scope "$(jl "$n" needs)" || bad="$bad; $n does not need scope" + body="$(job "$n")" + [[ "$body" == *'SCOPE_SKIP: ${{ needs.scope.outputs.skip }}'* && "$body" == *'"${SCOPE_SKIP:-}" = 1'* ]] || bad="$bad; $n does not read scope" done - top concurrency | grep -E '^ cancel-in-progress:' | grep -qF pull_request || bad="$bad; concurrency no longer cancels superseded PR runs" + [ "$(top concurrency | awk '/^ cancel-in-progress:/ {print; exit}')" = " cancel-in-progress: \${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}" ] || bad="$bad; concurrency no longer cancels superseded PR runs (exact expression pinned)" grep -qF 'git diff --no-renames --name-only' "$wf" || bad="$bad; scope diff may report renames by new path only (fails open)" fi [ -z "$bad" ] || { printf 'FAIL wiring: %s\n' "${bad#; }"; fail=1; } + # MUTANTS: each slip-through of a weaker reader must turn the wiring RED on a mutated copy of ci.yml. + if [ -z "${CI_SCOPE_WF:-}" ]; then + local md mn mexp + md="$(mktemp -d)" + mutant() { # LABEL SED-EXPR + sed -E "$2" "$wf" > "$md/ci.yml" + if cmp -s "$wf" "$md/ci.yml"; then echo "FAIL mutant $1: sed changed nothing"; fail=1 + elif CI_SCOPE_WF="$md/ci.yml" bash "${BASH_SOURCE[0]}" --self-test > /dev/null 2>&1; then echo "FAIL mutant $1: self-test stayed green"; fail=1 + else echo "ok red mutant $1"; fi + } + mutant paths-filter-pr '0,/^ types:/s// paths-ignore: [docs\/**]\n types:/' + mutant paths-oneline-push 's/^ push:$/ push: {paths: [x]}/' + mutant paths-flow-under-push '/^ push:$/{n;s/^( branches:.*)$/\1\n paths: [x]/}' + mutant no-ready_for_review 's/, ready_for_review//' + mutant scope-if "s/^( if: .\{\{ github.event_name == 'pull_request') \}\}/\1 || true }}/" + mutant cancel-false-trailing-comment 's/^ cancel-in-progress: .*/ cancel-in-progress: false # pull_request/' + mutant cancel-true 's/^ cancel-in-progress: .*/ cancel-in-progress: true/' + mutant no-renames 's/git diff --no-renames --name-only/git diff --name-only/' + mutant x86-main-no-scope-clause "/^ x86-main:\$/,/^ [a-z]/s/ && needs.scope.outputs.skip != '1'//" + mutant shard-needs-comment 's/^( workspace-test-shard:)$/\1/;/^ workspace-test-shard:$/,/^ [a-z]/s/^ needs: \[scope\]/ needs: [mg-reuse] # [scope]/' + mutant gate-needs-scope-dropped '/^ gate:$/,/^ [a-z]/s/, scope\]/]/' + mutant ci-gate-reads-scope-dropped '/^ ci-gate:$/,/^ [a-z]/s/SCOPE_SKIP: .*/SCOPE_SKIP: ""/' + mutant workspace-test-if-dropped "/^ workspace-test:\$/,/^ [a-z]/s/ && needs.scope.outputs.skip != '1'//" + rm -rf "${md:?}" + fi [ "$fail" -eq 0 ] && echo "ci_scope: self-test ok" return "$fail" } From 5f2b3e6fbe4e1da5eff8fb56d1ad54d66916ce96 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 07:00:03 +0200 Subject: [PATCH 08/10] fix(ci): ci_scope trigger-key check without SC1078 (lint ratchet back to baseline) Co-Authored-By: Claude Sonnet 5.5 --- scripts/ci_scope.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci_scope.sh b/scripts/ci_scope.sh index 6cfa4d37a5..7aee86c042 100755 --- a/scripts/ci_scope.sh +++ b/scripts/ci_scope.sh @@ -83,7 +83,7 @@ self_test() { has_scope() { [[ "$1" =~ [\[,][[:space:]]*scope[[:space:]]*[],] ]]; } [ "$(nc | grep -cE '^on:$')" = 1 ] || bad="$bad; on: is not a block mapping" for n in push pull_request merge_group; do - [ "$(top on | grep -cE "^ $n:$")" = 1 ] || bad="$bad; trigger $n is not a plain block key" + [ "$(top on | awk -v k="$n" '$0 == " "k":" {c++} END {print c+0}')" = 1 ] || bad="$bad; trigger $n is not a plain block key" body="$(ev "$n")" [[ "$body" =~ (^|[^A-Za-z_-])paths(-ignore)?[[:space:]]*: ]] && bad="$bad; $n has a paths filter (stuck required checks)" done From 8fbac591b0fdeabf0c11ced609844ea923c6c668 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 07:07:05 +0200 Subject: [PATCH 09/10] fix(ci): ci_scope rename check reads comment-stripped ci.yml (+mutant); drop pipe-into-grep-q at classify Co-Authored-By: Claude Sonnet 5.5 --- scripts/ci_scope.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/ci_scope.sh b/scripts/ci_scope.sh index 7aee86c042..81f50cc80b 100755 --- a/scripts/ci_scope.sh +++ b/scripts/ci_scope.sh @@ -33,7 +33,7 @@ classify() { # classify ; paths on stdin [ -n "$p" ] || continue n=$((n + 1)) case "$p" in *..*) echo "skip=0 reason=full"; return 0 ;; esac - printf '%s\n' "$p" | grep -Eq "$DOCS_ONLY_RE" || { echo "skip=0 reason=full"; return 0; } + [[ $p =~ $DOCS_ONLY_RE ]] || { echo "skip=0 reason=full"; return 0; } done [ "$n" -gt 0 ] && echo "skip=1 reason=docs-only" || echo "skip=0 reason=full" } @@ -100,7 +100,7 @@ self_test() { [[ "$body" == *'SCOPE_SKIP: ${{ needs.scope.outputs.skip }}'* && "$body" == *'"${SCOPE_SKIP:-}" = 1'* ]] || bad="$bad; $n does not read scope" done [ "$(top concurrency | awk '/^ cancel-in-progress:/ {print; exit}')" = " cancel-in-progress: \${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}" ] || bad="$bad; concurrency no longer cancels superseded PR runs (exact expression pinned)" - grep -qF 'git diff --no-renames --name-only' "$wf" || bad="$bad; scope diff may report renames by new path only (fails open)" + [[ "$(nc)" == *"git diff --no-renames --name-only"* ]] || bad="$bad; scope diff may report renames by new path only (fails open)" fi [ -z "$bad" ] || { printf 'FAIL wiring: %s\n' "${bad#; }"; fail=1; } # MUTANTS: each slip-through of a weaker reader must turn the wiring RED on a mutated copy of ci.yml. @@ -121,6 +121,7 @@ self_test() { mutant cancel-false-trailing-comment 's/^ cancel-in-progress: .*/ cancel-in-progress: false # pull_request/' mutant cancel-true 's/^ cancel-in-progress: .*/ cancel-in-progress: true/' mutant no-renames 's/git diff --no-renames --name-only/git diff --name-only/' + mutant no-renames-comment-only 's/^( *)(.*)git diff --no-renames --name-only(.*)$/\1# git diff --no-renames --name-only/' mutant x86-main-no-scope-clause "/^ x86-main:\$/,/^ [a-z]/s/ && needs.scope.outputs.skip != '1'//" mutant shard-needs-comment 's/^( workspace-test-shard:)$/\1/;/^ workspace-test-shard:$/,/^ [a-z]/s/^ needs: \[scope\]/ needs: [mg-reuse] # [scope]/' mutant gate-needs-scope-dropped '/^ gate:$/,/^ [a-z]/s/, scope\]/]/' From 464f41df032a771e5438c21bc567be7d93538c51 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 22 Oct 2026 07:46:43 +0200 Subject: [PATCH 10/10] chore(roadmap): regenerate roadmap.yaml for PMAT-5743 fragment (make roadmap-aggregate) Co-Authored-By: Claude Sonnet 5.5 --- docs/roadmaps/roadmap.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 277fbb376d..eab07e0bc2 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -25340,6 +25340,24 @@ Not done, pending the ruling: moving the 31 fixture files to an obviously synthe - kind:code notes: null epic: 5510 +- id: PMAT-5743 + github_issue: 5743 + item_type: task + title: 'ci: skip drafts and docs-only PRs inside ci.yml scope job; verdict jobs report success' + status: planned + priority: high + assigned_to: null + created: 2026-10-19T10:29:43Z + updated: 2026-10-19T10:29:43Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null + epic: 5510 - id: PMAT-5792 github_issue: 5792 item_type: task