diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e4ef8bd46..edd024f275 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,9 @@ on: tags: ['v*'] pull_request: branches: [main, master] - # Drafts skip the heavy jobs (x86-main, workspace-test-shard); ready_for_review fires the full run. + # 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 @@ -55,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 --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" + # ========================================================================== # #4433 — FIVE FAT JOBS (operator ruling 2026-09-25, items 1+4): # "CI = 5 fat jobs (x86 main, gx10, yoga, mac-refusal, determinism). Each @@ -154,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' && github.event.pull_request.draft != true }} + needs: [mg-reuse, scope] + if: ${{ !cancelled() && needs.mg-reuse.outputs.x86 != '1' && github.event.pull_request.draft != true && 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 @@ -475,7 +510,8 @@ jobs: # 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. workspace-test-shard: - if: ${{ github.event.pull_request.draft != true }} + needs: [scope] + if: ${{ !cancelled() && github.event.pull_request.draft != true && 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 @@ -527,8 +563,10 @@ jobs: workspace-test: runs-on: [self-hosted, Linux, clean-room] timeout-minutes: 10 - needs: [workspace-test-shard] - if: always() + needs: [workspace-test-shard, scope] + # docs-only PR: scope skipped the shards, so there is nothing to fan in. A skipped job satisfies + # the required check; draft, full, merge_group and push run exactly as before. + if: ${{ always() && needs.scope.outputs.skip != '1' }} steps: - name: Every shard passed env: @@ -586,7 +624,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 @@ -599,9 +637,12 @@ jobs: MG_CITE: ${{ needs.mg-reuse.outputs.cite }} ADV: ${{ needs.x86-main-advisories.result }} DRAFT: ${{ github.event.pull_request.draft }} + SCOPE_SKIP: ${{ needs.scope.outputs.skip }} + SCOPE_WHY: ${{ needs.scope.outputs.reason }} run: | set -euo pipefail [ "${DRAFT:-}" != true ] || { echo "ci / gate: draft PR, full CI is not owed until ready_for_review"; exit 0; } + [ "${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 @@ -624,7 +665,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 @@ -640,9 +681,12 @@ jobs: ADV: ${{ needs.x86-main-advisories.result }} MST: ${{ needs.mg-reuse-self-test.result }} DRAFT: ${{ github.event.pull_request.draft }} + SCOPE_SKIP: ${{ needs.scope.outputs.skip }} + SCOPE_WHY: ${{ needs.scope.outputs.reason }} run: | set -euo pipefail [ "${DRAFT:-}" != true ] || { echo "gate: draft PR, full CI is not owed until ready_for_review"; exit 0; } + [ "${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/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 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}' diff --git a/scripts/ci_mg_reuse.sh b/scripts/ci_mg_reuse.sh index 91b34d6702..a7aa3c9d46 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' && github.event.pull_request.draft != true }}" + wire x86-main 'needs: [mg-reuse, scope]' + wire x86-main "if: \${{ !cancelled() && needs.mg-reuse.outputs.x86 != '1' && github.event.pull_request.draft != true && 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 'advisory_gate.sh"' 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' && github.event.pull_request.draft != true \}\}/ 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' && github.event.pull_request.draft != true && 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..81f50cc80b --- /dev/null +++ b/scripts/ci_scope.sh @@ -0,0 +1,140 @@ +#!/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; 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/images/[^.][^[:space:]]*\.(png|jpg|jpeg|gif|svg|webp)$' + +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 + [[ $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" +} + +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 "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 "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 + 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/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="${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() { 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'; } + 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 | 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 + 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 + has_scope "$(jl "$n" needs)" && [[ "$(jl "$n" if)" == *"needs.scope.outputs.skip != '1'"* ]] || bad="$bad; $n does not honour scope" + done + 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 | 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)" + [[ "$(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. + 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 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\]/]/' + 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" +} + +case "${1:-}" in + classify) shift; classify "${1:-false}" ;; + --self-test) self_test ;; + *) echo "usage: $0 classify | --self-test" >&2; exit 2 ;; +esac