Skip to content

fix(ci): an independent guard reports rather than being masked by the guard before it - #4915

Merged
rbuergi merged 3 commits into
mainfrom
fix/4784-guard-steps-report-rather-than-mask
Sep 21, 2026
Merged

rbuergi merged 3 commits into
mainfrom
fix/4784-guard-steps-report-rather-than-mask

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

The skip-trapdoor is made by step ORDERING, and nothing in the file looks wrong

node-repo-validate.yml's validate job runs ~36 independent guards as consecutive steps.
GitHub's implicit step condition is success(), so the first failing guard skips every guard behind
it — and a skipped step publishes no failure.

Measured on MeshWeaver.SocialMedia#210, run 35431670104, job 105867277548, 2026-09-19 08:22Z:
the vendored-resolver drift check failed with 32 code line(s) differ and 16 steps reported
skipped
behind it, among them

step what stopped being enforced
Every PR-reachable secret in this repo is asserted by a preflight the shape that bit Reinsurance#128
Every manifest.lock is current (and carries a version) a stale lock reaching a publish
Every module's version matches its content a feature shipping to nobody (#878)
No mapping in this repo's workflows writes a key twice the duplicate-key guard
No pin comment names a commit this repo no longer pins abbreviated-sha / pin drift
This repo's no-op set agrees with the platform's no-op parity

validate / Validate node repos is a required context in all five satellites, and a required
context that reported skipped counts as satisfied under both classic protection and rulesets.
Because platform-ref defaults to main and the drift check fetches the canonical live, every
satellite is drifted from the instant a canonical change merges — so for the length of each re-copy
wave, every pull request in that repository was unguarded by all six, with one red about an unrelated
file as the only symptom.

This is the shape AGENTS.md legislates against, except the trapdoor is success()-by-default rather
than a continue-on-error: or an if:.

What changed

  • Every one of the 36 guard steps carries !cancelled() (&&-ed with the five conditions that
    were already there), which drops the implicit success() so the step runs and reports its own
    verdict. A failure still fails the job.
  • .github/scripts/check-guard-step-masking.py keeps it that way. For each declared
    (workflow, job): every step after the prerequisite prefix must carry !cancelled() or
    always(); the prerequisites — the checkout, its history fetch, setup-python — must be a
    prefix, because a prerequisite in the middle is the same defect wearing a legitimate name; and a
    prerequisite naming a step that was renamed or removed is stale and reds, so the guard cannot
    answer green having checked nothing. 13 self-test cases, both arms. Wired into core's own guard
    list in dotnet-test.yml (self-test first, then --root .).

A guard's own fetch is deliberately not a prerequisite. Its consumers now run and fail naming
the file they could not open — a second red rather than a silent skip — and the fetch's ::error::
is the root.

Control

$ git show origin/main:.github/workflows/node-repo-validate.yml > $CTL/.github/workflows/…
$ python3 .github/scripts/check-guard-step-masking.py --root $CTL
check-guard-step-masking: 1 job(s), 36 independent guard step(s) checked, 36 violation(s)   → exit 1

$ python3 .github/scripts/check-guard-step-masking.py --root .
check-guard-step-masking: 1 job(s), 36 independent guard step(s) checked, 0 violation(s)    → exit 0

$ python3 .github/scripts/check-guard-step-masking.py --self-test
self-test: every case fired on its defect and stayed silent on its fix   (13 cases)          → exit 0

The neighbouring workflow guards still pass on the edited tree: check-workflow-timeouts 90 jobs /
0 violations, check-workflow-yaml-keys 37 files / 0, check-reusable-workflow-runners 24 jobs / 0,
check-workflow-shell 0 live findings.

Not in this change

#4786 (fork PRs and the ARC runners) is the adjacent structural issue and is a runner-policy
decision, not a defect fix; it is left open. Option 2 of #4784's acceptance — one job per guard
family — was not taken: it would turn one job into ~10, each re-checking out the repository, against
a fleet whose stated goal is zero Actions spend.

Merged with #4949, which landed the FIRST half of this fix on main

While this PR was open, #4949 fixed #4784 on main with if: ${{ !cancelled() }} on every
non-prologue step and its own check-guard-step-masking.py. That is the first half. What this PR
still adds is the second half
, the one Copilot raised on this PR: bare !cancelled() also stops the
prerequisites from masking, so a failed checkout lets every guard run against an empty workspace,
which is a wall of secondary reds and, for any guard that passes on an empty tree, a vacuous pass.
Here the last prerequisite publishes ok=true and every guard requires
steps.ready.outputs.ok == 'true', so a prerequisite failing still stops everything with its own red,
and a guard failing stops nothing but itself.

Resolution of the merge: every conflict hunk was the same step under the two spellings, taken in the
gated form; the guard steps main added since (#4785, in both jobs) got the same gate; main's duplicate
wiring of the guard in workflow-shell was dropped for this PR's gated pair; the script is this PR's
(a superset of main's rules, 19 self-test cases, 0 violations over 100 guard steps on the merged
tree); the dated What's New this PR minted is dropped for main's existing entry, which gains one
paragraph on the prerequisite half (policy whatsnew-cadence).

Fixes #4784

Pairs-with: none — no public type or member leaves src/; the diff is two workflow files, a new CI
script, a doc page and a What's New entry.

🤖 Generated with Claude Code

… guard before it

`node-repo-validate.yml`'s `validate` job is ~36 independent guards in one step list, and
GitHub's implicit step condition is `success()` — so the FIRST failing guard skips every guard
behind it, and a `skipped` step publishes no failure.

Measured on MeshWeaver.SocialMedia#210 (run 35431670104, job 105867277548, 2026-09-19 08:22Z):
the vendored-resolver drift check failed with `32 code line(s) differ` and 16 steps reported
`skipped` behind it, including `check-pr-secret-preflight.py`, the manifest-lock currency check,
the module-version check, the duplicate-key guard, the pin-drift guard and no-op parity.

`validate / Validate node repos` is a REQUIRED context in all five satellites, and a required
context that reported `skipped` counts as SATISFIED under both classic protection and rulesets.
Because `platform-ref` defaults to `main`, every satellite is drifted from the instant a canonical
change merges — so for the length of each re-copy wave every pull request in that repository was
unguarded by all six, with one red about an unrelated file as the only symptom.

The trapdoor is made by step ORDERING, not by a `continue-on-error:` or an `if:`, so nothing in the
workflow looks wrong on inspection — which is why it needs a gate and not a comment.

- every one of the 36 guard steps now carries `!cancelled()` (`&&`-ed with the five conditions
  already there), dropping the implicit `success()` so the step runs and reports; a failure still
  fails the job.
- `.github/scripts/check-guard-step-masking.py` holds it: every step after the declared
  prerequisite PREFIX must carry `!cancelled()` or `always()`; the prerequisites (checkout, its
  history fetch, setup-python) must BE a prefix, because one in the middle is the same defect
  wearing a legitimate name; and a prerequisite naming a step that was renamed is STALE and reds.
  13 self-test cases, both arms. Wired into core's own guard list in dotnet-test.yml.

Control: the gate run against `origin/main`'s copy of the workflow reports
`36 independent guard step(s) checked, 36 violation(s)` and exits 1; against this branch's copy,
`36 ... checked, 0 violation(s)` and exits 0.

A guard's own FETCH is deliberately not a prerequisite: its consumers now run and fail naming the
file they could not open — a second red rather than a silent skip — and the fetch's `::error::`
is the root.

Fixes #4784

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 19, 2026 18:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The new checker can pass vacuously, and both the checker and prerequisite failure paths can still produce masked or misleading validation results.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 3 Medium severity

Open (4)
What changed in this PR

Strengthens the shared CI validation lane so independent guards continue reporting after failures.

Changes:

  • Adds !cancelled() to validation guard steps.
  • Introduces structural validation and self-tests for guard-step masking.
  • Documents the skip-trapdoor and its operational impact.
File Description
.github/​scripts/​check-guard-step-masking.py New guard invariant checker
.github/​workflows/​node-repo-validate.yml Makes validation steps failure-independent
.github/​workflows/​dotnet-test.yml Runs the new checker in core CI
src/​MeshWeaver.Documentation/​Data/​Architecture/​ReadingCiSignals.md Documents skipped-step behavior
src/​MeshWeaver.Documentation/​Data/​WhatsNew/​2026-09-19-one-failing-check-used-to-silence-sixteen-others.md Adds the fix release note

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +153 to +157
for index in range(len(prerequisites), len(steps)):
steps_checked += 1
condition = steps[index].get("if")
text = "" if condition is None else str(condition)
if not any(token in text for token in SURVIVES_A_FAILURE):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and it is the same class of defect as the one this file exists to catch — a check that cannot fail. Fixed.

check_tree now refuses a job whose step count is at or below its declared prerequisite count, naming both numbers and both readings ("either the guards were removed, which is the finding, or they moved and this declaration is stale"), and it continues so no later assertion pretends to have run.

Self-test case prefix-only-is-not-a-pass drives _PREFIX with nothing after it and is confirmed to FIRE:

self-test ok   prefix-only-is-not-a-pass      expected=fire got=fire

A related hole you did not name but which the same reasoning reaches: the readiness step could stop publishing what the guards read, which would skip every one of them, silently and green. readiness-publishes-nothing covers that too.

Comment on lines +95 to +98
def _matches(step_id: str, declared: str) -> bool:
if declared.startswith("uses:"):
return step_id.startswith(declared)
return step_id == declared

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and thank you — _step_id stripping the @ref is exactly what made the prefix comparison look harmless. Fixed: _matches is now exact equality for both shapes, with the reason written into its docstring rather than left to be re-derived.

def _matches(step_id: str, declared: str) -> bool:
    """Exact, both shapes. `_step_id` already strips an action's `@ref`, so a PREFIX comparison would
    let `actions/checkout-foo` satisfy a declared `uses:actions/checkout` — a renamed prerequisite
    passing the structural check instead of being reported stale."""
    return step_id == declared

Self-test case look-alike-action-is-not-the-prerequisite uses actions/checkout-foo@v7 as step 0 and is confirmed to FIRE:

self-test ok   look-alike-action-is-not-the-prerequisite expected=fire got=fire

Comment on lines +2150 to +2153
- name: "An independent guard reports rather than being masked by an earlier one — the gate can fail (self-test)"
run: python3 .github/scripts/check-guard-step-masking.py --self-test
- name: "No guard in the shared validate lane is masked by the guard before it"
run: python3 .github/scripts/check-guard-step-masking.py --root .

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and it is worse than the two steps you named: workflow-shell is 62 steps — two prerequisites and 60 independent guards, every one of them on the implicit success(). It is the job that gates main-cd.yml, the module lanes and every script a satellite fetches, so one red there silently withdrew the rest of CI's self-enforcement for that run. Adding the condition to only my two steps would have left the other 58 in the defect.

So the fix is the same treatment, applied to that job as well, and — the part that stops it coming back — dotnet-test.yml / workflow-shell is now a declared subject of the guard itself, beside the validate lane. All 60 guards carry the condition; the two prerequisites do not.

Control, on origin/main's copy of dotnet-test.yml with this branch's node-repo-validate.yml:

check-guard-step-masking: 2 job(s), 93 guard step(s) checked, 116 violation(s)   → exit 1

and on this branch:

check-guard-step-masking: 2 job(s), 96 guard step(s) checked, 0 violation(s)     → exit 0

The self-test also gained shell-job-checked-too, which leaves the validate lane correct and breaks only the shell job — otherwise every other case keeps dotnet-test.yml valid and none of them would notice if that subject stopped being checked.

# previous imported commit. The guard is fetched at the lane's own scripts ref for the same
# reason as the other centralized policy below: a caller cannot silently retain an old rule.
- name: Fetch the content-CI publish-path guard at the lane's scripts ref
if: ${{ !cancelled() }} # an independent guard REPORTS; it is never masked by an earlier one (#4784)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the finding I am most glad you made, because it is the half I got wrong rather than an omission: !cancelled() drops the implicit success() for the prerequisites too, so a failed checkout let all 36 guards run against an empty workspace. Mostly that is a wall of secondary reds, but for any guard that happens to pass on an empty tree it is a vacuous pass — the thing this file exists to prevent.

Fixed by making the prerequisite line explicit rather than positional. The last prerequisite publishes one output:

- name: The workspace and the tools are present — the ONE prerequisite every guard shares
  id: ready
  run: echo "ok=true" >> "$GITHUB_OUTPUT"

and every guard requires it:

if: ${{ !cancelled() && steps.ready.outputs.ok == 'true' }}

The two failure modes are then separate, which is the property that was missing:

  • a prerequisite fails ⇒ ready is skipped, its output is empty, every guard is skipped, and the prerequisite's own red is the verdict. Nothing runs against an empty workspace.
  • a guard fails ⇒ ready is untouched, so every other guard still reports.

I preferred this to steps.<id>.outcome == 'success' on each prerequisite because it is one expression rather than three repeated 96 times, and because it names the concept ("the prerequisite line") in the workflow instead of encoding it in a list a reader has to reconstruct.

The guard enforces both halves now — a condition carrying !cancelled() but not the readiness gate is a violation with its own message — and cancelled-without-readiness is the case that fires on exactly the shape I originally shipped:

self-test ok   cancelled-without-readiness    expected=fire got=fire

A guard's own fetch is still deliberately not a prerequisite: its consumers run and fail naming the file they could not open, which is a second red rather than a silent skip, and the fetch's ::error:: is the root.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

443 tests  ±0   443 ✅ ±0   53s ⏱️ -2s
  3 suites ±0     0 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit 940d268. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±0    1 suites  ±0   3m 0s ⏱️ + 1m 0s
300 tests ±0  300 ✅ ±0  0 💤 ±0  0 ❌ ±0 
302 runs  ±0  302 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 940d268. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 553 tests  ±0   1 553 ✅ ±0   3m 11s ⏱️ -1s
    2 suites ±0       0 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit 940d268. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

716 tests  ±0   524 ✅ ±0   5m 41s ⏱️ +7s
  3 suites ±0   192 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit 940d268. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files  ±0      3 suites  ±0   5m 55s ⏱️ -2s
1 922 tests ±0  1 922 ✅ ±0  0 💤 ±0  0 ❌ ±0 
1 923 runs  ±0  1 923 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 940d268. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    5 files  ±0      5 suites  ±0   14m 1s ⏱️ +20s
3 825 tests ±0  3 823 ✅ ±0  2 💤 ±0  0 ❌ ±0 
3 829 runs  ±0  3 827 ✅ ±0  2 💤 ±0  0 ❌ ±0 

Results for commit 940d268. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Test Results

   17 files  ±0     17 suites  ±0   32m 43s ⏱️ + 1m 21s
8 759 tests ±0  8 565 ✅ ±0  194 💤 ±0  0 ❌ ±0 
8 766 runs  ±0  8 572 ✅ ±0  194 💤 ±0  0 ❌ ±0 

Results for commit 940d268. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

…d job had the same defect

Four Copilot findings, all correct, and two of them are the half I got wrong rather than omissions.

1. `!cancelled()` drops the implicit `success()` for the PREREQUISITES too, so a failed checkout let
   all 36 guards run against an empty workspace — a wall of secondary reds and, for any guard that
   passes on an empty tree, a vacuous pass. The prerequisite line is now explicit rather than
   positional: the last one publishes `ok=true`, and every guard carries
   `!cancelled() && steps.ready.outputs.ok == 'true'`. A prerequisite failing skips every guard and
   its own red is the verdict; a guard failing skips nothing. Preferred over three
   `steps.<id>.outcome` terms repeated 96 times, and it names the concept in the workflow.

2. `dotnet-test.yml`'s `workflow-shell` is 62 steps — TWO prerequisites and SIXTY independent guards,
   every one on the implicit `success()`. It is the job that gates main-cd.yml, the module lanes and
   every script a satellite fetches, so one red there silently withdrew the rest of CI's
   self-enforcement for that run. Given the same treatment, and DECLARED AS A SUBJECT of the guard so
   it cannot drift back.

3. The checker passed vacuously when a job held only its prerequisites: `range(len(prerequisites),
   len(steps))` is empty, so every per-step assertion was skipped and it exited 0 over a job whose 36
   guards had been deleted. Now refused, naming both readings. Same reasoning reaches a readiness step
   that stopped publishing `ok=true` — also refused, because that would skip every guard silently.

4. `_matches` compared `uses:` prefixes while `_step_id` already strips the `@ref`, so
   `actions/checkout-foo` satisfied a declared `uses:actions/checkout`. Exact equality now.

Self-test: 19 cases, every one fired on its defect and stayed silent on its fix. The new ones are
`prefix-only-is-not-a-pass`, `readiness-publishes-nothing`, `cancelled-without-readiness`,
`look-alike-action-is-not-the-prerequisite`, `double-quoted-readiness-accepted` and
`shell-job-checked-too` — the last leaves the validate lane correct and breaks ONLY the shell job,
because every other case keeps `dotnet-test.yml` valid and none of them would notice if that subject
stopped being checked.

Controls:
  origin/main's dotnet-test.yml + this branch's node-repo-validate.yml
    → 2 job(s), 93 guard step(s) checked, 116 violation(s)   exit 1
  this branch
    → 2 job(s), 96 guard step(s) checked, 0 violation(s)     exit 0
  check-workflow-{timeouts,yaml-keys,shell} and check-reusable-workflow-runners: unchanged and green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 19, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 19, 2026
@meshweaver-cloud

Copy link
Copy Markdown
Contributor

Merge-queue steward: left dequeued — a job other than a test shard failed — a build or gate failure is never a flake.
Group build: https://github.com/Systemorph/MeshWeaver/actions/runs/35465637254

  • failed job: CI's own shell

Not a catalogued flake. Fix the failure, or — with run URLs, an issue and an assertion-message pattern — add it to .github/known-flakes.json (see Doc/Architecture/MergeQueue). Re-queue with gh pr merge <n> --auto once the head is green; label queue-rejected marks this PR as needing a person.

@meshweaver-cloud meshweaver-cloud Bot added the queue-rejected The merge queue rejected this PR on an uncatalogued failure; a person owns it now label Sep 19, 2026
@rbuergi
rbuergi added this pull request to the merge queue Sep 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 20, 2026
@meshweaver-cloud

Copy link
Copy Markdown
Contributor

Merge-queue steward: left dequeued — a job other than a test shard failed — a build or gate failure is never a flake.
Group build: https://github.com/Systemorph/MeshWeaver/actions/runs/35493144653

  • failed job: CI's own shell

Not a catalogued flake. Fix the failure, or — with run URLs, an issue and an assertion-message pattern — add it to .github/known-flakes.json (see Doc/Architecture/MergeQueue). Re-queue with gh pr merge <n> --auto once the head is green; label queue-rejected marks this PR as needing a person.

@rbuergi rbuergi added the release-stream Required for the 3.0.0-ci release stream label Sep 20, 2026
@rbuergi
rbuergi enabled auto-merge September 20, 2026 08:34
@rbuergi

rbuergi commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

This is DIRTY on a real content collision, not on drift

Merging origin/main here gives two conflicts, and the first one is the interesting one:

CONFLICT (add/add): Merge conflict in .github/scripts/check-guard-step-masking.py
CONFLICT (content): Merge conflict in .github/workflows/node-repo-validate.yml

add/add means main already has a file at that path. It arrived today via #4949
(22fdbd37ef"review(#4949): the control could itself be skipped, and the exemption matched by
prefix"
). So two pull requests built a guard at the same path, and #4949 merged first.

They are NOT the same guard, so this is not simply superseded

subject, from its own docstring
main (#4949), 7,080 B "Refuse a skip-trapdoor made by STEP ORDERING in a reusable validation lane" — has a _is_prologue concept
this branch, 21,696 B "in a one-job lane full of independent guards, a failing guard must [report rather than mask the ones after it]" — no prologue concept

478 lines differ. I checked whether this branch's version subsumes main's: it does not — it has no
prologue/step-ordering notion at all. Overlapping subject (guard steps in a one-job lane), two
different defects. So neither "take main's" nor "take the branch's" is lossless, and one file name
cannot hold both.

Why I stopped here

Resolving it means deciding how the two guards relate — one file covering both defects, or two files
with distinct names plus the fleet-wide rename of whichever call sites move. That is a design call
about your own guard, and #4949's name (check-guard-step-masking.py) arguably describes this
branch's content better than its own, which is the sort of thing worth getting right once rather than
picking to clear a conflict.

The rest of the merge is clean apart from node-repo-validate.yml, which is an ordinary content
conflict in the lane both PRs edit.

(No changes pushed. Reporting only, so the conflict is understood before it is resolved —
[[a-shared-red-spawns-simultaneous-fixers]] in practice.)

…4949 landed the first half of the same fix

Main already carries `check-guard-step-masking.py` and `!cancelled()` on every non-prologue step
(#4949, merged 2026-09-20). This branch keeps the second half that #4949 does not have: the
`ready` prerequisite output every guard requires, so a failed checkout, history fetch or tool
setup still masks the guards (Copilot on #4915) instead of letting them run against an empty
workspace.

Resolution: every conflict hunk was the same step carrying the two spellings, resolved to the
readiness-gated form; the three steps main added since (#4785, the node-test launcher guard) and
the #4785 self-test in `workflow-shell` got the same gate; main's duplicate wiring of the guard in
`workflow-shell` was dropped in favour of this branch's gated pair; the script is this branch's
(a superset of main's rules, 19 self-test cases). The dated What's New this branch minted is
dropped for main's existing entry, which gains one paragraph on the prerequisite half (policy
`whatsnew-cadence`), and the doc page's guard counts follow the merged tree (39 and 61).

Verified on the merged tree: check-guard-step-masking 2 jobs / 100 steps / 0 violations,
self-test 19/19; check-workflow-timeouts 90 / 0; check-workflow-yaml-keys 37 / 0;
check-reusable-workflow-runners 24 / 0; check-workflow-shell 0 live; permission-pairing 5 / 0;
pr-secret-preflight 2 / 2 / 0; actionlint clean on both workflows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rbuergi
rbuergi added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit 4e7641d Sep 21, 2026
41 of 62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

queue-rejected The merge queue rejected this PR on an uncatalogued failure; a person owns it now release-stream Required for the 3.0.0-ci release stream

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A mid-job guard failure in node-repo-validate silently SKIPS 16 later guards — a skip-trapdoor made by step ordering

2 participants