diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c7f6f4d26..359acade90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: - name: Sections (the determinism raster returns first; the rest keep running) run: >- python3 scripts/ci/fat_driver.py run - --sections 'sov.*,guard-tree,guard-cargo,vendored-schemas,pr-review-shadow,pr-review-sign,mutants,determinism[X64]' + --sections 'sov.*,changes,guard-tree,guard-cargo,vendored-schemas,pr-review-shadow,pr-review-sign,mutants,determinism[X64]' --external-job workspace-test --results "$RUNNER_TEMP/fat/results.json" --background-until 'determinism[X64]' @@ -443,7 +443,8 @@ jobs: # cuda-unit were never in the gate; they still are not. # workspace-test is its own job now (three shard jobs + the Σ fan-in). [ "$WT" = success ] || { echo "workspace-test failed: $WT"; exit 1; } - for pair in X86:guard-tree X86:guard-cargo X86:sov.gate DET:determinism-compare; do + # guard-cargo and determinism-compare are read below under the docs-only rule (#3668). + for pair in X86:guard-tree X86:sov.gate; do job="${pair%%:*}" sec="${pair#*:}" r=$(res "${!job}" "$sec") if [ "$r" != success ]; then @@ -451,6 +452,29 @@ jobs: exit 1 fi done + # guard-cargo is the cargo half of the same guard universe (BSE-01), and + # determinism-compare is APEX-001 EV-2a's two-architecture comparison. A + # required check that `gate` does not read is not a required check, so both + # are read here -- and each may be `skipped` ONLY on a docs-only PR (#3668). + DOCS=$(printf '%s' "${X86:-null}" | jq -r '(. // {}).changes.outputs.docs_only // ""') + CHG=$(res "$X86" changes) + GC=$(res "$X86" guard-cargo) + DC=$(res "$DET" determinism-compare) + # --- GATE-DOCS-ONLY-RULE-BEGIN (#3668; scripts/check_ci_gate_docs_only_rule.sh runs this block) --- + # A skip is the docs-only skip only when the `changes` section SUCCEEDED and + # said docs_only=true. Any other skip -- changes skipped, failed, missing or + # said false -- means a section that should have run did not: a failure. + skip_ok=0 + if [ "$CHG" = success ] && [ "$DOCS" = true ]; then skip_ok=1; fi + for pair in "guard-cargo=$GC" "determinism-compare=$DC"; do + job=${pair%%=*}; res=${pair#*=} + case "$res:$skip_ok" in + success:*) echo "$job: success" ;; + skipped:1) echo "$job: skipped -- docs-only PR (changes: docs_only=true, #3668)" ;; + *) echo "$job failed: $res (changes=$CHG docs_only='$DOCS'; a skip passes only on a docs-only PR)"; exit 1 ;; + esac + done + # --- GATE-DOCS-ONLY-RULE-END --- MUT=$(res "$X86" mutants) # --- GATE-MUTANTS-RULE-BEGIN (#3676; scripts/check_ci_gate_mutants_rule.sh runs this block) --- # `skipped` is a pass ONLY when the skip is the job's own filter: diff --git a/ci/sections.yml b/ci/sections.yml index 25d2b01fc8..16d1237dec 100644 --- a/ci/sections.yml +++ b/ci/sections.yml @@ -990,6 +990,43 @@ jobs: # discriminating runner label, so a bare [self-hosted, X64, Linux] selector # can never again silently land a job on a GPU/dev runner without the # sovereign-ci registry. Pure text check; runs on the clean-room pool. + # #3668: ONE docs-only decision for the run. A roadmap-only PR paid ~26 min of + # guard-cargo and ~6 of determinism for a diff neither reads. This job asks + # scripts/ci_docs_only.sh (which asks ci_test_tier.sh's #3658 definition: + # every path under docs/roadmaps/ or docs/audits/ and present at HEAD) and + # publishes docs_only. The consumers are written FAIL-CLOSED: they read + # `!cancelled() && needs.changes.outputs.docs_only != 'true'`, so this job + # being skipped (every non-PR event), failing, or printing anything but + # `true` runs everything. `gate` accepts their `skipped` only when this job + # SUCCEEDED and said true (GATE-DOCS-ONLY-RULE, + # scripts/check_ci_gate_docs_only_rule.sh). + changes: + runs-on: [self-hosted, Linux, clean-room] # arch-neutral: intel, yoga or gx10 (#3100) + timeout-minutes: 10 # shallow checkout + one git diff + one script + if: github.event_name == 'pull_request' + outputs: + docs_only: ${{ steps.decide.outputs.docs_only }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 1 + persist-credentials: false + - name: Is this PR docs-only? (#3668) + id: decide + run: | + set -euo pipefail + git fetch --no-tags --depth=1 origin \ + "+refs/heads/${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}" + # --no-renames (#3664): a source moved into docs/ must list BOTH paths. + git diff --no-renames --name-only "origin/${GITHUB_BASE_REF}" HEAD > "$RUNNER_TEMP/touched.txt" + printf 'touched (tip vs tip): %s path(s)\n' "$(wc -l < "$RUNNER_TEMP/touched.txt" | tr -d ' ')" + bash scripts/ci_docs_only.sh --event "${GITHUB_EVENT_NAME}" --diff-from "$RUNNER_TEMP/touched.txt" \ + > "$RUNNER_TEMP/docs_only.txt" + cat "$RUNNER_TEMP/docs_only.txt" + grep -E '^docs_only=(true|false)$' "$RUNNER_TEMP/docs_only.txt" >> "$GITHUB_OUTPUT" + - name: The docs-only decision can still go RED (case table) + run: bash scripts/ci_docs_only.sh --self-test + guard-tree: runs-on: [self-hosted, Linux, clean-room] # arch-neutral: intel, yoga or gx10 (#3100) # 20: the operator's hard PR budget (rule E, 2026-09-10), and it OVERRIDES @@ -1126,6 +1163,63 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: bash scripts/guard_tree.sh --no-cargo + # #3668: these three ran in guard-cargo, but none of them invokes cargo -- + # guard_tree.sh --no-cargo files them as cargo guards only because their + # SOURCE says `cargo doc` / `cargo install` in a comment or a pattern. They + # are the guard-cargo steps whose reads include docs/roadmaps/ or docs/audits/ + # (docs/**/*.md, a `git ls-files | grep dogfood.sh$`), so they live here: + # guard-cargo is skipped on a docs-only PR (the `changes` job) and guard-tree + # never is, so a docs-only diff is still judged by every guard that reads it. + # One runner, one prose, and the user-scope copy stays a <=70-line shim + # that fails CLOSED. The cap lives here, in the repo on protected main; + # the file it caps lives at user scope, where it could otherwise raise its + # own ceiling. + - name: One dogfood runner, and the user-scope copy is a gated shim + run: setsid --wait bash scripts/check_dogfood_shim.sh + # §4.1 names BOTH of these in the merge-phase blocking set, alongside the + # fabricated-baseline guard above. They shipped unwired, which is why the + # five `2.93x Ollama` claims in book/ and the four deleted harnesses were + # each caught by a person rather than by CI. + # + # Neither is baselined as an exemption, deliberately. The tip commit of + # this epic says PERF-009 is "enforced by a shrink-only guard"; a guard + # named by no workflow enforces nothing, and recording that in a baseline + # file would make the claim true only in the sense that the gap is + # written down. + # + # This step is ALSO the I-12 `[X]`-figure gate, which v2.2 §5 listed as + # "not written" against this file. It was right: the comparator pattern + # required the competitor's name to sit immediately after the ratio, in + # ASCII, so of the six shapes §0.1 names it caught one. PERF-010 admits + # the connector ("36.9x over FasterTransformer") and U+00D7, and adds + # placeholders bound to a unit ("XX tok/s"). Twelve pre-existing sites + # surfaced and are baselined shrink-only; one was a LIVE comparator claim + # in book/ and is deleted. + - name: No claim literals on user-facing surfaces + run: setsid --wait bash scripts/check_no_claim_literals.sh + - name: No claim-literal guard regression (case table) + run: setsid --wait bash scripts/check_no_claim_literals.sh --selftest + # PERF-010, the receipt rule's namesake check. The guard above is the + # NEGATIVE half -- a comparator ratio may not appear at all. This is the + # POSITIVE half over the class that is allowed to stay: a speed comparison + # naming no competitor is legal iff it cites the evidence/ receipt that + # produced it (v2.2 section 3.2). Text-only and deterministic, so it + # belongs in the merge phase (section 4.1) rather than at release. + # + # It found 146 uncited speed comparisons across 30 files and ZERO citing a + # receipt -- a 0% citation rate on the very number class this epic is + # named for. Baselined shrink-only; the count is the finding. + - name: Performance claims cite their receipts (PERF-010) + run: setsid --wait bash scripts/check_perf_claims_cite_receipts.sh + - name: Claims-cite-receipts guard can still go RED (case table) + run: setsid --wait bash scripts/check_perf_claims_cite_receipts.sh --selftest + # The gate's docs-only skip rule, executed from ci.yml's own marker block. + # It reads only ci.yml and invokes no build tool, so it runs here -- the job + # that runs on every PR, including the one that edits the gate (#3668). + - name: gate accepts a skipped guard-cargo/determinism only on a docs-only PR (#3668) + run: setsid --wait bash scripts/check_ci_gate_docs_only_rule.sh + - name: gate docs-only rule can still go RED (#3668) + run: setsid --wait bash scripts/check_ci_gate_docs_only_rule.sh --self-test # scripts/tests/guard_tree_test.sh is guard_tree.sh's OWN case table # (BSE-01, PMAT-1062) -- it proves guard_tree.sh's no-fail-fast # behaviour against a fixture, proves a fail-fast MUTANT of it goes RED @@ -1542,6 +1636,11 @@ jobs: # minutes onto every green run for no ordering that anything requires. guard-cargo: runs-on: [self-hosted, Linux, clean-room] # arch-neutral: intel, yoga or gx10 (#3100) + # #3668: skipped on a docs-only PR -- no step here reads docs/roadmaps/ or + # docs/audits/ (the three that did moved to guard-tree). Fail-closed: a + # skipped/failed `changes` job leaves docs_only empty, and this runs. + needs: [changes] + if: ${{ !cancelled() && needs.changes.outputs.docs_only != 'true' }} # 20: the operator's hard PR budget (rule E, 2026-09-10), and it OVERRIDES # the BSE-05 formula rather than being derived from it. # @@ -1712,12 +1811,6 @@ jobs: # It found `pmat analyze reachability` bare in BOTH copies on its first run. - name: The release runner must not resolve a verifier through PATH run: setsid --wait bash scripts/check_verifier_pinning.sh - # One runner, one prose, and the user-scope copy stays a <=70-line shim - # that fails CLOSED. The cap lives here, in the repo on protected main; - # the file it caps lives at user scope, where it could otherwise raise its - # own ceiling. - - name: One dogfood runner, and the user-scope copy is a gated shim - run: setsid --wait bash scripts/check_dogfood_shim.sh # deny.toml is 7.2 KB of licence / banned-crate / source-allowlist policy # that ran in NO workflow -- only `make deny`, which is not a prerequisite # of any tier, so nothing but a human typing it ever invoked it. Meanwhile @@ -2048,43 +2141,6 @@ jobs: # ollama never invoked (#2679). - name: No fabricated comparator baselines run: setsid --wait bash scripts/check_no_fabricated_baselines.sh - # §4.1 names BOTH of these in the merge-phase blocking set, alongside the - # fabricated-baseline guard above. They shipped unwired, which is why the - # five `2.93x Ollama` claims in book/ and the four deleted harnesses were - # each caught by a person rather than by CI. - # - # Neither is baselined as an exemption, deliberately. The tip commit of - # this epic says PERF-009 is "enforced by a shrink-only guard"; a guard - # named by no workflow enforces nothing, and recording that in a baseline - # file would make the claim true only in the sense that the gap is - # written down. - # - # This step is ALSO the I-12 `[X]`-figure gate, which v2.2 §5 listed as - # "not written" against this file. It was right: the comparator pattern - # required the competitor's name to sit immediately after the ratio, in - # ASCII, so of the six shapes §0.1 names it caught one. PERF-010 admits - # the connector ("36.9x over FasterTransformer") and U+00D7, and adds - # placeholders bound to a unit ("XX tok/s"). Twelve pre-existing sites - # surfaced and are baselined shrink-only; one was a LIVE comparator claim - # in book/ and is deleted. - - name: No claim literals on user-facing surfaces - run: setsid --wait bash scripts/check_no_claim_literals.sh - - name: No claim-literal guard regression (case table) - run: setsid --wait bash scripts/check_no_claim_literals.sh --selftest - # PERF-010, the receipt rule's namesake check. The guard above is the - # NEGATIVE half -- a comparator ratio may not appear at all. This is the - # POSITIVE half over the class that is allowed to stay: a speed comparison - # naming no competitor is legal iff it cites the evidence/ receipt that - # produced it (v2.2 section 3.2). Text-only and deterministic, so it - # belongs in the merge phase (section 4.1) rather than at release. - # - # It found 146 uncited speed comparisons across 30 files and ZERO citing a - # receipt -- a 0% citation rate on the very number class this epic is - # named for. Baselined shrink-only; the count is the finding. - - name: Performance claims cite their receipts (PERF-010) - run: setsid --wait bash scripts/check_perf_claims_cite_receipts.sh - - name: Claims-cite-receipts guard can still go RED (case table) - run: setsid --wait bash scripts/check_perf_claims_cite_receipts.sh --selftest # The single source of truth needs a truth check. An authorless policy # number, a `inherited_from` pointing at a document with no history in any # ref, a compute_class no build in this tree reaches, and a cell exception @@ -3155,6 +3211,10 @@ jobs: # font, a locale or a cached artefact from a previous job and quietly agree for the wrong reason. determinism: name: determinism (${{ matrix.arch }}) + # #3668 does NOT skip this section under #4433's fat jobs: its raster is uploaded + # by the fat job's own step with if-no-files-found: error (x86-main for X64, the + # determinism job for ARM64), so an unrendered raster would fail the fat job on + # every docs-only PR. It always runs; gate's docs-only rule then reads success. strategy: fail-fast: false # if one arch fails we still want the other's receipt to compare against matrix: diff --git a/docs/roadmaps/entries/PMAT-3668.yaml b/docs/roadmaps/entries/PMAT-3668.yaml new file mode 100644 index 0000000000..1524ee820e --- /dev/null +++ b/docs/roadmaps/entries/PMAT-3668.yaml @@ -0,0 +1,22 @@ +- id: PMAT-3668 + github_issue: 3668 + item_type: task + title: 'Docs-only PRs still spend ~45 min in guard-cargo and guard-tree: gate them on a single docs_only decision so a roadmap PR''s CI is < 10 min' + status: inprogress + priority: critical + assigned_to: aprender-a2 + created: 2026-09-21T07:19:59Z + updated: 2026-09-24T19:00:00Z + spec: null + acceptance_criteria: + - scripts/ci_docs_only.sh prints docs_only=true only when every touched path is under docs/roadmaps/ or docs/audits/ and present at HEAD (ci_test_tier.sh docs-only, #3658); every other event, diff or error gives false. The 13-row --self-test has discriminator rows + - guard-cargo and determinism are skipped when needs.changes.outputs.docs_only == 'true' and run otherwise, including when changes is skipped or fails + - gate's GATE-DOCS-ONLY-RULE accepts a skip only when changes succeeded with docs_only=true. check_ci_gate_docs_only_rule.sh runs 14 rows over the block, and 3 planted wrong rules go RED + - the guard-cargo steps that read docs/** run in guard-tree unconditionally + - a real docs-only PR run finishes in under 10 min (observed after landing) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 145d8aedf2..662c52f675 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -20212,6 +20212,28 @@ roadmap: labels: - kind:code notes: null +- id: PMAT-3668 + github_issue: 3668 + item_type: task + title: 'Docs-only PRs still spend ~45 min in guard-cargo and guard-tree: gate them on a single docs_only decision so a roadmap PR''s CI is < 10 min' + status: inprogress + priority: critical + assigned_to: aprender-a2 + created: 2026-09-21T07:19:59Z + updated: 2026-09-24T19:00:00Z + spec: null + acceptance_criteria: + - scripts/ci_docs_only.sh prints docs_only=true only when every touched path is under docs/roadmaps/ or docs/audits/ and present at HEAD (ci_test_tier.sh docs-only, #3658); every other event, diff or error gives false. The 13-row --self-test has discriminator rows + - guard-cargo and determinism are skipped when needs.changes.outputs.docs_only == 'true' and run otherwise, including when changes is skipped or fails + - gate's GATE-DOCS-ONLY-RULE accepts a skip only when changes succeeded with docs_only=true. check_ci_gate_docs_only_rule.sh runs 14 rows over the block, and 3 planted wrong rules go RED + - the guard-cargo steps that read docs/** run in guard-tree unconditionally + - a real docs-only PR run finishes in under 10 min (observed after landing) + phases: [] + subtasks: [] + estimated_effort: null + labels: + - kind:code + notes: null - id: PMAT-3670 github_issue: 3670 item_type: task diff --git a/scripts/check_ci_gate_docs_only_rule.sh b/scripts/check_ci_gate_docs_only_rule.sh new file mode 100755 index 0000000000..c0ae93c228 --- /dev/null +++ b/scripts/check_ci_gate_docs_only_rule.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# check_ci_gate_docs_only_rule.sh -- the `gate` job accepts a SKIPPED guard-cargo or +# determinism-compare only when the `changes` job SUCCEEDED and said docs_only=true +# (#3668). +# +# guard-cargo and determinism are skipped on a docs-only PR (every touched path +# under docs/roadmaps/ or docs/audits/, scripts/ci_docs_only.sh). Their `if:` is +# fail-closed, so a skipped/failed `changes` job makes them RUN; the gate must be +# fail-closed too: any skip without a successful docs_only=true is a failure. +# +# This guard does not re-implement the rule. It EXTRACTS the block between the +# GATE-DOCS-ONLY-RULE-BEGIN/END markers from .github/workflows/ci.yml and executes +# it for every (docs_only, changes, guard-cargo, determinism-compare) row, so it +# judges the code the gate runs. A missing or empty block is ENV rc=2, never a pass. +# +# check_ci_gate_docs_only_rule.sh the case table over ci.yml's block +# check_ci_gate_docs_only_rule.sh --self-test planted wrong rules must go RED +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" || exit 2 +WF="${GATE_RULE_WORKFLOW:-$ROOT/.github/workflows/ci.yml}" + +# extract -> the rule block, dedented; empty when the markers are absent +extract() { + awk '/GATE-DOCS-ONLY-RULE-BEGIN/{f=1; next} /GATE-DOCS-ONLY-RULE-END/{f=0} f' "$1" | sed -E 's/^ {10}//' +} + +# verdict -> exit status of the block; "-" is empty +verdict() { + local docs=$2 + [ "$docs" = "-" ] && docs="" + DOCS=$docs CHG=$3 GC=$4 DC=$5 bash -c "$1" >/dev/null 2>&1 +} + +table() { # table -> 0 iff every row holds + local blk bad=0 n=0 docs chg gc dc want got + blk=$(extract "$1") + [ -n "$blk" ] || { printf 'ENV no GATE-DOCS-ONLY-RULE block in %s -- cannot judge, not a pass\n' "$1" >&2; return 2; } + while read -r docs chg gc dc want; do + [ -n "$docs" ] || continue + n=$((n + 1)); got=0; verdict "$blk" "$docs" "$chg" "$gc" "$dc" || got=1 + if [ "$got" = "$want" ]; then printf 'ok row %-2s docs=%-5s changes=%-8s gc=%-9s dc=%-9s -> %s\n' "$n" "$docs" "$chg" "$gc" "$dc" "$([ "$want" = 0 ] && echo pass || echo FAIL)" + else printf 'FAIL row %-2s docs=%-5s changes=%-8s gc=%-9s dc=%-9s wanted %s, got %s\n' "$n" "$docs" "$chg" "$gc" "$dc" "$want" "$got" >&2; bad=1; fi + done <<'ROWS' +true success success success 0 +true success skipped skipped 0 +false success skipped success 1 +false success success skipped 1 +- skipped skipped skipped 1 +true failure skipped skipped 1 +true success failure skipped 1 +true success cancelled skipped 1 +true success skipped failure 1 +false success success success 0 +- skipped success success 0 +- failure success success 0 +false success failure success 1 +- skipped success failure 1 +ROWS + return "$bad" +} + +case "${1:-}" in -h|--help) sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac + +if [ "${1:-}" = "--self-test" ]; then + echo "=== gate docs-only rule: planted wrong rules must turn the table RED ===" + d=$(mktemp -d "${TMPDIR:-/tmp}/gate-docs-rule.XXXXXX") || exit 2 + rmtree() { case "${1:-}" in ''|/) return 0 ;; *) [ -d "$1" ] && rm -rf -- "$1" ;; esac; return 0; } + trap 'rmtree "${d:-}"' EXIT + # the pre-#3668 rule: require success, never accept a skip + printf ' # --- GATE-DOCS-ONLY-RULE-BEGIN ---\n [ "$GC" = success ] && [ "$DC" = success ] || exit 1\n # --- GATE-DOCS-ONLY-RULE-END ---\n' > "$d/old.yml" + # the permissive mutant: a skip always passes, whatever `changes` said + printf ' # --- GATE-DOCS-ONLY-RULE-BEGIN ---\n for r in "$GC" "$DC"; do case "$r" in success|skipped) ;; *) exit 1 ;; esac; done\n # --- GATE-DOCS-ONLY-RULE-END ---\n' > "$d/loose.yml" + # trusts the output alone: a docs_only=true from a job that did not succeed + printf ' # --- GATE-DOCS-ONLY-RULE-BEGIN ---\n for r in "$GC" "$DC"; do case "$r:$DOCS" in success:*|skipped:true) ;; *) exit 1 ;; esac; done\n # --- GATE-DOCS-ONLY-RULE-END ---\n' > "$d/nochg.yml" + printf 'jobs: {}\n' > "$d/none.yml" + bad=0 + for m in old loose nochg; do + if table "$d/$m.yml" > "$d/out" 2>&1; then printf 'FAIL the planted %s rule passed the table\n' "$m"; bad=1 + else printf 'ok the planted %-5s rule is RED: %s row(s), e.g. %s\n' "$m" "$(grep -c '^FAIL' "$d/out")" "$(grep -m1 '^FAIL' "$d/out" | cut -c7-80)"; fi + done + rc=0; table "$d/none.yml" > /dev/null 2>&1 || rc=$? + [ "$rc" = 2 ] && printf 'ok a workflow with no rule block is ENV rc=2, never a pass\n' || { printf 'FAIL no rule block gave rc=%s\n' "$rc"; bad=1; } + [ "$bad" = 0 ] && { echo "SELF-TEST PASSED"; exit 0; } + echo "SELF-TEST FAILED" >&2; exit 1 +fi + +echo "=== gate accepts a skipped guard-cargo/determinism-compare only on a docs-only PR (check_ci_gate_docs_only_rule.sh) ===" +[ -f "$WF" ] || { printf 'ENV %s is missing\n' "$WF" >&2; exit 2; } +table "$WF"; rc=$? +[ "$rc" = 0 ] && echo PASS || echo "FAIL (rc=$rc)" >&2 +exit "$rc" diff --git a/scripts/ci/fat_driver.py b/scripts/ci/fat_driver.py index 06c670a3d6..ceee1031ae 100644 --- a/scripts/ci/fat_driver.py +++ b/scripts/ci/fat_driver.py @@ -1244,6 +1244,11 @@ def emit_results_output(res: dict) -> None: return compact = {n: {"result": r["result"], "continue_on_error": r.get("continue_on_error", False)} for n, r in res.items()} + # A section's job-level outputs ride along when it has any: gate reads + # `changes.outputs.docs_only` for the #3668 docs-only rule. + for n, r in res.items(): + if r.get("outputs"): + compact[n]["outputs"] = r["outputs"] # The actions a section staged for the fat job's own steps (codecov, attest). kinds = set() dpath = Path(os.environ.get("RUNNER_TEMP", "/tmp")) / "fat" / "deferred.jsonl" diff --git a/scripts/ci_docs_only.sh b/scripts/ci_docs_only.sh new file mode 100755 index 0000000000..ea5488cafc --- /dev/null +++ b/scripts/ci_docs_only.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# ci_docs_only.sh -- the ONE docs-only decision the whole ci.yml run reads (#3668). +# +# A roadmap-only PR used to pay ~26 minutes of guard-cargo and ~6 of determinism +# for a diff neither of them reads. The `changes` job runs this once; guard-cargo +# and determinism skip on `docs_only=true`, guard-tree runs the docs readers +# guard-cargo would have run, and `gate` accepts those skips ONLY when this said +# true (scripts/check_ci_gate_docs_only_rule.sh runs gate's block). +# +# The definition is NOT re-implemented here. docs-only is exactly what +# scripts/ci_test_tier.sh already calls it (#3658): every touched path under +# docs/roadmaps/ or docs/audits/ AND present at HEAD. This script asks that +# oracle and answers true only on its docs-only verdict. Everything else answers +# false: another event, an empty diff, a missing diff file, an oracle that +# errors or prints something else. It FAILS CLOSED -- the answer is always +# printed and the exit is 0, so a broken decision runs the full CI, never less. +# +# ci_docs_only.sh --event EVENT --diff-from FILE -> docs_only=true|false, reason=... +# ci_docs_only.sh --self-test -> the case table +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" || exit 2 +TIER="${CI_DOCS_ONLY_TIER_SCRIPT:-$ROOT/scripts/ci_test_tier.sh}" + +answer() { printf 'docs_only=%s\nreason=%s\n' "$1" "$2"; exit 0; } + +decide() { # decide EVENT FILE -> prints the answer, always exit 0 + local evt=$1 diff=$2 out + [ "$evt" = pull_request ] || answer false "event '$evt': only a pull_request is judged docs-only; every other event runs everything" + [ -f "$diff" ] || answer false "no diff file '$diff' -- cannot judge, so everything runs" + [ -s "$diff" ] || answer false "empty diff -- nothing to classify, so everything runs" + # Pre-filter, narrower than the oracle and so only ever in the fail-closed + # direction: a path outside the two directories is an immediate false, and a + # code PR never pays the oracle's crate selection for a question it lost. + # The oracle alone can say true. + # grep reads the FILE, never a pipe (check_no_pipe_into_grep_q.sh): `grep -q` + # exiting early SIGPIPEs its writer, and under pipefail that reads as no-match. + local outside + outside=$(grep -vE '^(docs/(roadmaps|audits)/|$)' "$diff") || outside='' + if [ -n "$outside" ]; then + answer false "a touched path is outside docs/roadmaps/ and docs/audits/: ${outside%%$'\n'*}" + fi + out=$(bash "$TIER" --event pull_request --diff-from "$diff" 2>&1) \ + || answer false "ci_test_tier.sh failed ($(printf '%s' "$out" | tail -1 | cut -c1-120)) -- fail closed" + if grep -qx 'tier=none' <<< "$out" && grep -q '^reason=.*docs-only' <<< "$out"; then + answer true "all $(grep -c . "$diff") touched path(s) are docs/roadmaps/** or docs/audits/** present at HEAD (ci_test_tier.sh docs-only, #3658)" + fi + answer false "not docs-only per ci_test_tier.sh: tier=$(printf '%s\n' "$out" | sed -n 's/^tier=//p' | head -1)" +} + +self_test() { + local td bad=0 n=0 rm au + td=$(mktemp -d "${TMPDIR:-/tmp}/docs-only.XXXXXX") || exit 2 + rmtree() { case "${1:-}" in ''|/) return 0 ;; *) [ -d "$1" ] && rm -rf -- "$1" ;; esac; return 0; } + trap 'rmtree "${td:-}"' EXIT + # row WANT DESCRIPTION EVENT FILE [TIER-SCRIPT] + row() { + local want=$1 desc=$2 evt=$3 f=$4 got + n=$((n + 1)) + got=$(CI_DOCS_ONLY_TIER_SCRIPT="${5:-$TIER}" bash "$0" --event "$evt" --diff-from "$f" | sed -n 's/^docs_only=//p') + if [ "$got" = "$want" ]; then printf 'ok row %-2s %-5s %s\n' "$n" "$want" "$desc" + else printf 'FAIL row %-2s wanted %s, got "%s": %s\n' "$n" "$want" "$got" "$desc"; bad=1; fi + } + # Real paths at HEAD of this tree, so the oracle's present-at-HEAD test passes. + rm=$(git -C "$ROOT" ls-files 'docs/roadmaps/*' | head -1) + au=$(git -C "$ROOT" ls-files 'docs/audits/*' | head -1) + { [ -n "$rm" ] && [ -n "$au" ]; } || { echo "ENV no tracked docs/roadmaps or docs/audits file to build fixtures from" >&2; exit 2; } + printf '%s\n%s\n' "$rm" "$au" > "$td/docs.txt" + printf '%s\ncrates/aprender-core/src/lib.rs\n' "$rm" > "$td/mixed.txt" + printf '%s\n.github/workflows/ci.yml\n' "$rm" > "$td/wf.txt" + printf 'docs/roadmaps/entries/PMAT-DELETED-3668.yaml\n' > "$td/deleted.txt" + printf 'docs/specifications/aprender-monorepo-consolidation.md\n' > "$td/spec.txt" + : > "$td/empty.txt" + printf '#!/usr/bin/env bash\necho boom >&2; exit 3\n' > "$td/tier-dies.sh" + printf '#!/usr/bin/env bash\nprintf "tier=none\\nreason=x: empty diff\\n"\n' > "$td/tier-none-not-docs.sh" + printf '#!/usr/bin/env bash\nprintf "tier=none\\nreason=x: docs-only\\n"\n' > "$td/tier-says-docs.sh" + row true "roadmap + audit only, both present at HEAD" pull_request "$td/docs.txt" + row false "roadmap + one .rs file (the control: everything runs)" pull_request "$td/mixed.txt" + row false "roadmap + ci.yml (a workflow edit is never docs-only)" pull_request "$td/wf.txt" + row false "a DELETED roadmap path (a tree reader asserts cited paths exist)" pull_request "$td/deleted.txt" + row false "docs/specifications/ is read by tests, not docs-only" pull_request "$td/spec.txt" + row false "empty diff" pull_request "$td/empty.txt" + row false "missing diff file" pull_request "$td/nope.txt" + row false "merge_group with a docs-only diff (only pull_request is judged)" merge_group "$td/docs.txt" + row false "push with a docs-only diff" push "$td/docs.txt" + row false "the oracle ERRORS -> fail closed" pull_request "$td/docs.txt" "$td/tier-dies.sh" + row false "the oracle says tier=none for a non-docs reason" pull_request "$td/docs.txt" "$td/tier-none-not-docs.sh" + row false "(discriminator) a non-docs path is false even if the oracle says docs-only" pull_request "$td/mixed.txt" "$td/tier-says-docs.sh" + row true "(discriminator) on a docs diff the oracle's docs-only verdict is what flips it" pull_request "$td/docs.txt" "$td/tier-says-docs.sh" + [ "$bad" = 0 ] && { echo "SELF-TEST PASSED ($n rows)"; return 0; } + echo "SELF-TEST FAILED" >&2; return 1 +} + +EVENT="" DIFF="" +while [ $# -gt 0 ]; do + case "$1" in + --event) EVENT=${2:-}; shift 2 ;; + --diff-from) DIFF=${2:-}; shift 2 ;; + --self-test) self_test; exit $? ;; + -h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) printf 'usage: %s --event EVENT --diff-from FILE | --self-test\n' "$0" >&2; exit 2 ;; + esac +done +[ -n "$EVENT" ] || { printf 'usage: %s --event EVENT --diff-from FILE | --self-test\n' "$0" >&2; exit 2; } +decide "$EVENT" "$DIFF"