Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 93 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,46 @@ jobs:
# `workspace-test`, `ci / gate` and `gate`, each reading the fat jobs' results
# with the same rule the old job of that name applied.
# ==========================================================================
# #4472 CHANGE-CLASS: what kind of change is this diff? Decided ONCE, here, by
# scripts/ci_change_class.sh; x86-main, determinism and mac-check scale down on
# class=docs, and the verdict jobs read the same output, so a section is absent
# only because this job said docs, and `gate`/`ci / gate` say so by name.
# class=docs: every path is an added/modified *.md outside .github/ that no Rust
# source compiles in (include_str!/include!/build.rs). Anything else, any other
# event, and a FAILED decision all mean the full set: every consumer tests
# `== 'docs'`, never `!= 'full'`. What still runs for docs is every check that
# reads doc content: guard-tree, guard-cargo, vendored-schemas, both pr-review
# sections, and the workspace-test shards (their tier still runs the tree readers).
change-class:
runs-on: [self-hosted, Linux, clean-room] # arch-neutral: bash + git only
timeout-minutes: 10
outputs:
class: ${{ steps.decide.outputs.class }}
reason: ${{ steps.decide.outputs.reason }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: change-class case table must still turn RED
run: bash scripts/ci_change_class.sh --self-test
- name: Classify the diff
id: decide
env:
EVT: ${{ github.event_name }}
MG_BASE: ${{ github.event.merge_group.base_sha }}
run: |
set -euo pipefail
bash scripts/ci_change_class.sh --event "$EVT" --base "$MG_BASE" > "$RUNNER_TEMP/class.txt"
cat "$RUNNER_TEMP/class.txt"
cat "$RUNNER_TEMP/class.txt" >> "$GITHUB_OUTPUT"
{ echo '### change-class (#4472)'; sed 's/^/ /' "$RUNNER_TEMP/class.txt"; } >> "$GITHUB_STEP_SUMMARY"

x86-main:
runs-on: [self-hosted, Linux, X64, clean-room]
timeout-minutes: 240
# #4472: !cancelled(), not success(): a failed change-class still runs the full set.
needs: [change-class]
if: ${{ !cancelled() }}
# The sections carry their own permissions blocks (the sov provenance section
# needs id-token + attestations); one job token must cover all of them.
permissions: write-all
Expand Down Expand Up @@ -122,14 +159,27 @@ jobs:
- name: every section secret is plumbed under its full name
run: setsid --wait bash scripts/check_ci_fat_secrets_plumbed.sh
- name: Sections (the determinism raster returns first; the rest keep running)
if: needs.change-class.outputs.class != 'docs'
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],provable-ladder'
--external-job workspace-test
--results "$RUNNER_TEMP/fat/results.json"
--background-until 'determinism[X64]'
# #4472 class=docs: only the sections that read doc content. sov.*, mutants,
# determinism[X64] and provable-ladder compile or run Rust that no Markdown in
# a docs diff can reach (the classifier refuses any .md a crate compiles in);
# gate and ci / gate report them as not-triggered, by name.
- name: Sections (class=docs -- the doc-content sections only, #4472)
if: needs.change-class.outputs.class == 'docs'
run: >-
python3 scripts/ci/fat_driver.py run
--sections 'vendored-schemas,guard-tree,guard-cargo,pr-review-shadow,pr-review-sign'
--results "$RUNNER_TEMP/fat/results.json"
--background-until 'vendored-schemas'
# Published mid-run: the determinism job's compare section is waiting for it.
- uses: actions/upload-artifact@v7
if: needs.change-class.outputs.class != 'docs'
with:
name: determinism-X64
path: ${{ runner.temp }}/fat/artifacts/determinism-X64/
Expand Down Expand Up @@ -261,6 +311,10 @@ jobs:
determinism:
runs-on: [self-hosted, Linux, clean-room, ARM64]
timeout-minutes: 120
# #4472: a docs diff cannot move a raster; gate reports determinism-compare
# as not-triggered for it. A failed change-class still runs this.
needs: [change-class]
if: ${{ !cancelled() && needs.change-class.outputs.class != 'docs' }}
outputs:
results: ${{ steps.wait.outputs.results }}
env:
Expand Down Expand Up @@ -313,6 +367,9 @@ jobs:
mac-check:
runs-on: [self-hosted, macOS, ARM64, apple-silicon, m4, mini]
timeout-minutes: 60
# #4472: cargo check + apr-cli lib tests; a docs diff reaches neither.
needs: [change-class]
if: ${{ !cancelled() && needs.change-class.outputs.class != 'docs' }}
env:
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: never
Expand Down Expand Up @@ -598,14 +655,32 @@ jobs:
name: ci / gate
runs-on: [self-hosted, Linux, clean-room]
timeout-minutes: 10
needs: [x86-main]
needs: [change-class, x86-main]
if: always()
steps:
- name: The sovereign-ci gate section's result
env:
RESULTS: ${{ needs.x86-main.outputs.results }}
X86_RESULT: ${{ needs.x86-main.result }}
CLASS_JOB: ${{ needs.change-class.result }}
CLASS: ${{ needs.change-class.outputs.class }}
CLASS_REASON: ${{ needs.change-class.outputs.reason }}
run: |
set -euo pipefail
# #4472: class=docs ran no sov.* section. not-triggered, by name; the
# doc-content sections x86-main did run must have passed. Executed against
# a case table by scripts/check_ci_gate_docs_class_rule.sh.
if [ "$CLASS_JOB" = success ] && [ "$CLASS" = docs ]; then
echo "sov.gate: not-triggered: docs-only change ($CLASS_REASON)"
echo "x86-main (doc-content sections): $X86_RESULT"
[ "$X86_RESULT" = success ] || { echo "::error::ci / gate (docs): x86-main result is '$X86_RESULT'"; exit 1; }
for sec in guard-tree guard-cargo; do
r=$(printf '%s' "${RESULTS:-null}" | SEC="$sec" jq -r '(. // {})[env.SEC].result // "missing"')
echo "$sec: $r"
[ "$r" = success ] || { echo "::error::ci / gate (docs): $sec result is '$r'"; exit 1; }
done
exit 0
fi
r=$(printf '%s' "${RESULTS:-null}" | jq -r '(. // {})."sov.gate".result // "missing"')
echo "sovereign-ci gate section: $r"
[ "$r" = success ] || { echo "::error::ci / gate: section result is '$r'"; exit 1; }
Expand All @@ -614,7 +689,7 @@ jobs:
runs-on: [self-hosted, Linux, clean-room] # arch-neutral: intel, yoga or gx10 (#3100)
timeout-minutes: 22
# the GPU sections are advisory (68-C3 / 68-D2); yoga is needed for its mutants-cuda section alone (#4621)
needs: [x86-main, workspace-test, determinism, mutants-table-scope, mutants-table, yoga]
needs: [x86-main, workspace-test, determinism, mutants-table-scope, mutants-table, yoga, change-class]
if: always()
steps:
- name: Check required sections
Expand All @@ -627,16 +702,31 @@ jobs:
SCOPE: ${{ needs.mutants-table-scope.result }}
IN_SCOPE: ${{ needs.mutants-table-scope.outputs.in_scope }}
TABLE: ${{ needs.mutants-table.result }}
CLASS_JOB: ${{ needs.change-class.result }}
CLASS: ${{ needs.change-class.outputs.class }}
CLASS_REASON: ${{ needs.change-class.outputs.reason }}
run: |
set -euo pipefail
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
# cuda-unit were never in the gate; they still are not.
# workspace-test is its own job now (three shard jobs + the Σ fan-in).
# The two a docs run may excuse come FIRST: an excuse that ended the loop
# instead of skipping one pair would then skip the guards, and the case
# table sees it. Every pair must pass either way; only the order moved.
[ "$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
for pair in X86:sov.gate DET:determinism-compare X86:guard-tree X86:guard-cargo; do
job="${pair%%:*}" sec="${pair#*:}"
# #4472: on class=docs (from a change-class job that SUCCEEDED) these two
# did not run, and say so by name; guard-tree and guard-cargo (they read
# doc content) did, and must pass. scripts/check_ci_gate_docs_class_rule.sh
# executes this step against its case table.
case "$CLASS_JOB:$CLASS:$sec" in
success:docs:sov.gate | success:docs:determinism-compare)
echo "$sec: not-triggered: docs-only change ($CLASS_REASON)"
continue ;;
esac
r=$(res "${!job}" "$sec")
if [ "$r" != success ]; then
echo "$sec failed: $r"
Expand Down
8 changes: 8 additions & 0 deletions ci/sections.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,14 @@ jobs:
run: setsid --wait bash scripts/check_ci_gate_mutants_rule.sh
- name: gate mutants rule can still go RED (#3676)
run: setsid --wait bash scripts/check_ci_gate_mutants_rule.sh --self-test
# #4472: on class=docs the verdict jobs excuse sov.gate and determinism-compare
# by name. This executes both verdict steps from ci.yml against the case table:
# docs only from a change-class job that succeeded, and guard-tree/guard-cargo
# are never excused. Reads only ci.yml, so it runs here, on every PR.
- name: gate excuses a section only on a succeeded class=docs (#4472)
run: setsid --wait bash scripts/check_ci_gate_docs_class_rule.sh
- name: gate docs-class rule can still go RED (#4472)
run: setsid --wait bash scripts/check_ci_gate_docs_class_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
Expand Down
127 changes: 127 additions & 0 deletions scripts/check_ci_gate_docs_class_rule.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# check_ci_gate_docs_class_rule.sh -- `gate` and `ci / gate` accept an absent
# section only on a class=docs run, and only for the sections a docs run skips
# (#4472).
#
# On class=docs (scripts/ci_change_class.sh, the `change-class` job) x86-main runs
# only the doc-content sections, and determinism does not run. The two verdict
# jobs then report sov.gate and determinism-compare as "not-triggered: docs-only".
# The rule this guard holds them to:
# * docs counts ONLY when change-class SUCCEEDED and said class=docs; an empty
# class, a failed job or class=full judges every section as before;
# * guard-tree and guard-cargo must still succeed on a docs run, and so must
# workspace-test (gate) and x86-main itself (ci / gate);
# * nothing else is ever excused.
#
# It does not re-implement the rule. It EXTRACTS the two steps' `run:` scripts
# from .github/workflows/ci.yml and executes them for every row, so it judges the
# code the verdict jobs run. A step it cannot find is ENV rc=2, never a pass.
#
# check_ci_gate_docs_class_rule.sh the case table over ci.yml
# check_ci_gate_docs_class_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}"
GATE_STEP="Check required sections"
CIGATE_STEP="The sovereign-ci gate section's result"

# step_run <workflow> <step name> -> that step's `run: |` body, dedented; empty if absent
step_run() {
awk -v want="- name: $2" '
index($0, want) && !found { found = 1; ind = -1; next }
found && !body && /^ *run: \|/ { body = 1; next }
found && !body && /^ *- (name|uses):/ { exit }
body {
if ($0 ~ /^[[:space:]]*$/) { print ""; next }
match($0, /^ */)
if (ind < 0) ind = RLENGTH
if (RLENGTH < ind) exit
print substr($0, ind + 1)
}' "$1"
}

DOCS_X86='{"guard-tree":{"result":"success"},"guard-cargo":{"result":"success"},"vendored-schemas":{"result":"success"}}'
GT_FAIL='{"guard-tree":{"result":"failure"},"guard-cargo":{"result":"success"}}'
GC_MISS='{"guard-tree":{"result":"success"}}'
FULL_X86='{"guard-tree":{"result":"success"},"guard-cargo":{"result":"success"},"sov.gate":{"result":"success"}}'
FULL_DET='{"determinism-compare":{"result":"success"}}'

# run_gate <script> CLASS_JOB CLASS X86 DET WT -> exit status
run_gate() {
CLASS_JOB=$2 CLASS=$3 CLASS_REASON=t X86=$4 DET=$5 WT=$6 YM='' EVT=pull_request \
SCOPE=success IN_SCOPE=false TABLE=skipped bash -c "$1" > /dev/null 2>&1
}
# run_cigate <script> CLASS_JOB CLASS X86_RESULT RESULTS -> exit status
run_cigate() {
CLASS_JOB=$2 CLASS=$3 CLASS_REASON=t X86_RESULT=$4 RESULTS=$5 bash -c "$1" > /dev/null 2>&1
}

table() { # table <workflow> -> 0 iff every row holds, 2 when a step is missing
local g c bad=0 n=0 want got label
g=$(step_run "$1" "$GATE_STEP")
c=$(step_run "$1" "$CIGATE_STEP")
[ -n "$g" ] && [ -n "$c" ] || { printf 'ENV no "%s" or "%s" step in %s -- cannot judge, not a pass\n' "$GATE_STEP" "$CIGATE_STEP" "$1" >&2; return 2; }
row() { # row WANT LABEL got-status
n=$((n + 1)); want=$1 label=$2 got=$3
if [ "$got" = "$want" ]; then printf 'ok row %-2s %-4s %s\n' "$n" "$want" "$label"
else printf 'FAIL row %-2s wanted %s, got %s: %s\n' "$n" "$want" "$got" "$label" >&2; bad=1; fi
}
st() { if "$@"; then echo pass; else echo fail; fi; }
row pass "gate: docs, guard-tree+guard-cargo ok, no sov.gate/determinism" "$(st run_gate "$g" success docs "$DOCS_X86" '' success)"
row fail "gate: docs, guard-tree failed" "$(st run_gate "$g" success docs "$GT_FAIL" '' success)"
row fail "gate: docs, guard-cargo missing" "$(st run_gate "$g" success docs "$GC_MISS" '' success)"
row fail "gate: docs, workspace-test failed" "$(st run_gate "$g" success docs "$DOCS_X86" '' failure)"
row fail "gate: docs output from a FAILED change-class" "$(st run_gate "$g" failure docs "$DOCS_X86" '' success)"
row fail "gate: empty class (no decision), docs-shaped results" "$(st run_gate "$g" success '' "$DOCS_X86" '' success)"
row fail "gate: class=full, docs-shaped results" "$(st run_gate "$g" success full "$DOCS_X86" '' success)"
row fail "gate: class=full, determinism-compare missing" "$(st run_gate "$g" success full "$FULL_X86" '' success)"
row pass "gate: class=full, every section ok" "$(st run_gate "$g" success full "$FULL_X86" "$FULL_DET" success)"
row pass "gate: change-class skipped/failed, every section ok" "$(st run_gate "$g" failure '' "$FULL_X86" "$FULL_DET" success)"
row pass "ci/gate: docs, x86-main + guard-tree + guard-cargo ok" "$(st run_cigate "$c" success docs success "$DOCS_X86")"
row fail "ci/gate: docs, x86-main failed" "$(st run_cigate "$c" success docs failure "$DOCS_X86")"
row fail "ci/gate: docs, x86-main ok but guard-tree failed" "$(st run_cigate "$c" success docs success "$GT_FAIL")"
row fail "ci/gate: docs, x86-main ok but guard-cargo missing" "$(st run_cigate "$c" success docs success "$GC_MISS")"
row fail "ci/gate: docs, results empty" "$(st run_cigate "$c" success docs success '')"
row fail "ci/gate: docs output from a FAILED change-class" "$(st run_cigate "$c" failure docs success "$DOCS_X86")"
row fail "ci/gate: empty class, no sov.gate" "$(st run_cigate "$c" success '' success "$DOCS_X86")"
row fail "ci/gate: class=full, no sov.gate" "$(st run_cigate "$c" success full success "$DOCS_X86")"
row pass "ci/gate: class=full, sov.gate ok" "$(st run_cigate "$c" success full success "$FULL_X86")"
return "$bad"
}

case "${1:-}" in -h | --help) sed -n '2,21p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac

if [ "${1:-}" = "--self-test" ]; then
echo "=== gate docs-class rule: planted wrong rules must turn the table RED ==="
d=$(mktemp -d "${TMPDIR:-/tmp}/gate-docs-class.XXXXXX") || exit 2
trap 'rm -rf -- "${d:?}"' EXIT
[ -f "$WF" ] || { printf 'ENV %s is missing\n' "$WF" >&2; exit 2; }
# trusts the output alone: a docs class from a change-class job that did not succeed
sed 's/case "\$CLASS_JOB:\$CLASS:\$sec" in/case "success:$CLASS:$sec" in/; s/if \[ "\$CLASS_JOB" = success \] && \[ "\$CLASS" = docs \]/if [ "$CLASS" = docs ]/' "$WF" > "$d/nojob.yml"
# the permissive mutant: on docs every section is excused, guard-tree included
sed 's/success:docs:sov.gate | success:docs:determinism-compare)/success:docs:*)/' "$WF" > "$d/loose.yml"
# ci / gate trusts x86-main's job result alone, not the doc-content sections
awk '/for sec in guard-tree guard-cargo; do/ { skip = 1 } skip && /^ *done$/ { skip = 0; next } !skip' "$WF" > "$d/cig.yml"
# ci / gate drops guard-cargo from the doc-content sections it still requires
sed 's/for sec in guard-tree guard-cargo; do/for sec in guard-tree; do/' "$WF" > "$d/cigc.yml"
# gate ends the whole check at the first excused section instead of skipping it
awk '/success:docs:sov.gate \| success:docs:determinism-compare\)/ { hit = 1 } hit && /continue ;;/ { sub(/continue ;;/, "exit 0 ;;"); hit = 0 } 1' "$WF" > "$d/exit.yml"
printf 'jobs: {}\n' > "$d/none.yml"
bad=0
for m in nojob loose cig cigc exit; do
if cmp -s "$WF" "$d/$m.yml"; then printf 'FAIL the %s mutant did not apply (its anchor is gone)\n' "$m"; bad=1; continue; fi
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-90)"; fi
done
rc=0; table "$d/none.yml" > /dev/null 2>&1 || rc=$?
if [ "$rc" = 2 ]; then printf 'ok a workflow with no verdict steps is ENV rc=2, never a pass\n'
else printf 'FAIL no verdict steps gave rc=%s\n' "$rc"; bad=1; fi
[ "$bad" = 0 ] && { echo "SELF-TEST PASSED"; exit 0; }
echo "SELF-TEST FAILED" >&2; exit 1
fi

echo "=== gate / ci / gate excuse sov.gate+determinism-compare only on a succeeded class=docs (check_ci_gate_docs_class_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"
Loading
Loading