Skip to content

ci: regression guard for path-filtered required checks (#286) - #828

Merged
claude[bot] merged 2 commits into
masterfrom
claude/issue-286-path-filtered-required-check
Aug 27, 2026
Merged

ci: regression guard for path-filtered required checks (#286)#828
claude[bot] merged 2 commits into
masterfrom
claude/issue-286-path-filtered-required-check

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • design-approval.yml's idempotency check ran after its staleness check, and only recognized a flow as "already approved" when the design-approval issue's own stamp still matched the feature's current manifest stamp exactly.
  • gate-frames-first (scripts/check-frames-first.mjs) gates implementation purely on build.flows[<flow>].approved === true — it never reads manifest.stamp. So a flow's approval legitimately survives later, unrelated manifest edits that move the stamp (e.g. feat(frames-first): declare implementation.paths coverage for 19 features #717 / chore(frames-first): shrink uncovered residual 116→58, exclude app infra #734, which added implementation.paths coverage across 19 features and re-stamped them).
  • Net effect: re-firing the workflow (e.g. via relabel) on a stale approval-request issue for an already-approved flow produced a false "frames changed, re-review and approve again" — even though nothing further was blocking that flow.
  • Reordered: idempotency (flow.approved === true → close, no-op) now runs before staleness. Staleness still fully gates any flip to approved: true for a flow not yet approved.

Context — issue #347

Investigating #347 (design-approval: approve auth-experience / signup) found the flow was already approved by the owner via a direct commit to master (370dbe50, 2026-07-21) after design-approval.yml's Contents-API write path was rejected by the branch-protection ruleset at the time. design/frames/auth-experience/manifest.json already carries approved: true for signup (and the feature's other 3 flows) at a valid, up-to-date stamp — implementation is not blocked. Issue #347 itself couldn't self-close: its recorded stamp was a 12-char prefix from the original (failed) automated attempt, and the feature's stamp has since moved again via #717/#734's coverage-declaration edits. This fix lets the workflow reconcile that class of stale issue instead of permanently failing to close it.

Test plan

  • node --test scripts/__tests__/stamp-frames.test.mjs — 3/3 pass (unaffected; no manifest content changed)
  • node --test scripts/__tests__/check-frames-first.test.mjs — 19/19 pass (unaffected)
  • node scripts/stamp-frames.mjs --check — all 21 feature stamps still up to date
  • Extracted and node --check'd the modified actions/github-script block (workflow YAML has no JS/YAML syntax checker in CI)
  • Next real approval-request issue exercises the new order live (no local GH Actions runner available)

Generated with Claude Code

Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc


Generated by Claude Code

Issue #286: the branch ruleset's required status check "In-repo packages
resolve from source" was produced by workspace-deps-check.yml, which was
path-filtered to `**/package.json`. A path-filtered workflow never runs on
a PR that touches none of its paths, so the check run is never created --
and GitHub treats a required-but-never-reported context as unsatisfied.
Every PR that didn't touch a package.json sat permanently at "Expected --
waiting for status to be reported", with no red to click and nothing to
re-run. That's why every merge needed --admin, defeating the auto-merge
path CLAUDE.md documents.

VERIFIED FIRST, PER INSTRUCTIONS: this exact defect no longer reproduces
on master.
  - workspace-deps-check.yml's `on.pull_request` carries no `paths:` key
    today -- confirmed by reading the file at origin/master (2de4a1c).
  - The ruleset ("Protect Master", id 17974934) still requires the
    "In-repo packages resolve from source" context -- confirmed via
    `gh api repos/izzywdev/FuzeFront/rulesets/17974934` (branch-protection
    API 403'd for this token as anticipated, but the rulesets API worked).
  - Empirically confirmed on merged PR #818, which touches no package.json
    file: `In-repo packages resolve from source: success` is present in
    its check-runs, and the PR merged cleanly.
  - git blame shows the filter was already removed by #794 (2026-08-23,
    "ci: two required checks on master cannot deliver a correct verdict"),
    which chose exactly fix (a) from this issue's own writeup ("Always
    run, skip the work -- keeps the gate honest") over the path-filter-
    shim alternative, because the job is dependency-free and scans 49
    manifests in under a second, so running it unconditionally costs
    nothing. That reasoning is sound and is not being revisited here.

So there is no live code defect left in scope of #286 to fix -- #794
already applied the correct one of the two standard fixes and did not
weaken the required set. What #286 leaves genuinely open is that nothing
stops the exact same regression from recurring: the "no paths: filter"
invariant lived only in prose (two workflow header comments) with no CI
enforcement, for either of the two workflows whose comments state it
(workspace-deps-check.yml, gate-sealed-keys.yml).

This commit turns that prose into an enforced, dependency-free gate:
  - governance/required-check-triggers.json declares the (context,
    workflow file) pairs that must always run unconditionally on
    pull_request.
  - scripts/check-required-check-triggers.mjs scans each declared
    workflow for a paths:/paths-ignore: filter nested under its
    pull_request: trigger and fails if one is present. No YAML-parser
    dependency (same style as check-workspace-deps.mjs) -- a line-based
    indentation scan is enough for the one shape that matters, and it
    does not false-positive on an unrelated paths: filter under a
    sibling push: trigger (tested).
  - .github/workflows/gate-required-check-triggers.yml wires it in on
    every PR to master, itself with NO paths: filter -- it would be
    exactly the bug it exists to catch if it were path-filtered.

Mutation-tested: re-adding the historical `paths: ['**/package.json', ...]`
block to workspace-deps-check.yml's pull_request trigger is caught
(exit 1, exact FuzeFront#286 message); the restored file passes (exit 0).

Not done here, and why: promoting gate-required-check-triggers itself
into the branch ruleset's required set needs ruleset-write access this
session was not asked to exercise, and is a separate governance decision
(which checks are required) from the workflow-level fix #286 asked for.
Filed as a note for @izzywdev rather than actioned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc
@github-actions
github-actions Bot requested a review from izzywdev as a code owner August 27, 2026 08:04
@github-actions github-actions Bot added the auto-merge Enable squash auto-merge once CI passes label Aug 27, 2026
@claude claude Bot changed the title ci: add regression guard for path-filtered required checks (#286) ci: regression guard for path-filtered required checks (#286) Aug 27, 2026
@claude claude Bot closed this Aug 27, 2026
@claude claude Bot reopened this Aug 27, 2026
@github-actions
github-actions Bot enabled auto-merge (squash) August 27, 2026 08:12
izzywdev pushed a commit that referenced this pull request Aug 27, 2026
…ing 2 of 38

#828 (issue #286) adds governance/required-check-triggers.json — a gate that
fails if a required context's workflow ever gains a `paths:` filter on
`pull_request:`. It is what stops the deadlock in §2 from being reintroduced, and
its own header states the limitation:

  Adding a NEW required context to the ruleset? Add its workflow here in the same
  PR — the gate is only as complete as this list.

It ships listing 2 workflows. Step 3 of this runbook takes the required set to 38.
Applied without extending that list, the guard silently covers 2 of 38 and the
other 36 can be re-filtered by anyone with nothing to catch it — a green gate
measuring almost nothing, which is the shape of failure gate-vacuous-check exists
to prevent.

Adds step 3a: the full context -> producing-workflow mapping (seven workflows
cover all 38), plus a verification snippet that prints required / guarded /
UNGUARDED counts so the two files cannot drift silently.

Mapping verified against the workflow files rather than written from memory. Two
contexts are matrix expansions and needed checking by hand: `CodeQL Analysis
(javascript)` comes from security.yml's `codeql-analysis` job (matrix: language)
and `Lint & Test (24.x)` from ci.yml's `lint-and-test` (matrix: node-version).

Also notes that gate-openapi-conformance.yml and gate-route-ownership.yml join
the list once #844 lands, since unfiltering them is precisely the property this
guard protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc
@claude
claude Bot merged commit 942abae into master Aug 27, 2026
57 checks passed
@claude
claude Bot deleted the claude/issue-286-path-filtered-required-check branch August 27, 2026 15:51
claude Bot added a commit that referenced this pull request Aug 27, 2026
…te-identifier are not among them (#843)

* docs(governance): master requires 11 of 45 checks; gate-authz and gate-identifier are not among them

CLAUDE.md states the gate set IS the production guard, now that CI may approve
its own PRs: "gate-ds-conformance, gate-frames-first, gate-authz, gate-identifier,
and the full CI matrix ... A bot-authored, bot-approved PR that clears every gate
ships to prod."

The ruleset does not enforce that. Measured against the live ruleset: 11 of 45
checks are required. gate-authz and gate-identifier — both named above — are not
required. Neither is any backend, integration or e2e suite, nor CodeQL, Snyk,
Container Security Scan, gate-toolchain, gate-version, gate-pagination or
gate-vacuous-check.

auto-merge.yml arms `gh pr merge --auto --squash`, and GitHub's auto-merge waits
on REQUIRED contexts only. Master is deploy-on-push. So a PR merges and deploys
the moment those 11 pass, while the rest are still running or already red. On
2026-08-27 the bot armed auto-merge on #836 while `Build chat-service` was failing
on an unbuildable production image; that check is not required, so nothing would
have stopped the merge.

This commit is the applicable change, not a report of one: the rulesets API is
write-blocked through the agent proxy (403), so it needs repo-admin rights. The
desired set is data in governance/required-status-checks.json and the runbook
carries the exact PUT, the verification, and the rollback.

Audited every workflow trigger before choosing the set, because a required check
that does not run on every PR is never created and pins the PR at "Expected"
forever — this repo hit that on 2026-08-23 across four sampled PRs. Only
unfiltered checks are included (39 contexts). `Generate SBOM` is excluded as it
always concludes skipped.

Two findings worth more than the count:

- The most valuable gates CANNOT be required as they stand. `shipped code matches
  the frozen contract` — which would have caught config-service shipping two
  declared-but-unimplemented endpoints — is path-filtered, along with
  gate-frames-first, gate-route-ownership, image-reproducibility and helm-validate.
  Step 4 is to unfilter the trigger and filter inside the job, the correction
  workspace-deps-check.yml and gate-sealed-keys.yml already carry.

- Merge queue must NOT be enabled yet, though it is the right fix for the
  release-commit treadmill. Zero of 58 PR-triggered jobs have a `merge_group`
  trigger, so queue entries would get no checks at all and time out; and gates
  that ratchet on the PR base would compute over an empty diff under
  `merge_group`, reporting green vacuously. Trigger support and base-ref handling
  land first, proven by a gate still FAILING under a merge_group event.

Health-checked before proposing: 29 candidate contexts across all 20 open PRs
produced 0 failures, so widening stalls nothing in flight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc

* docs(governance): drop gate-code-review from the required set — it can hang, not fail

Correcting my own recommendation in the previous commit. gate-code-review is
`runs-on: fuzefront` — a SELF-HOSTED runner — with no `timeout-minutes`. Requiring
it would let one offline runner block every merge in the repo, permanently, with
no red anywhere to look at.

Not hypothetical. Measured 2026-08-27 while draining the PR queue: the job
completes in 12-14 seconds when the runner picks it up (five PRs did), but
NOTHING has completed since 09:08Z and 17 PRs sit queued for up to an hour —
including #840, the PR at the front of the merge queue, queued since 08:52Z. The
runner is not slow; it stopped taking work. Meanwhile in-progress GitHub-hosted
runs fell from 14 to 5, so this is not shared capacity.

The health check in the previous commit read gate-code-review as 5 success /
0 fail / 15 pending and I counted it safe. That was the wrong reading: those
pendings were not "slow", they were never going to complete. 0 failures is not
evidence a check can fail.

Hence the general rule now stated in the runbook, which is what I actually got
wrong: A REQUIRED CHECK MUST BE ABLE TO FAIL. A check that can only pass or hang
is worse than no check, because the deadlock it produces is indistinguishable
from CI still running, so nobody knows to go look. gate-code-review and the
path-filtered gates both fail that test, in different ways — one hangs waiting
for a runner, the others are never created at all.

Requireable again once it has a GitHub-hosted fallback, or a timeout that fails
rather than hangs. Required contexts 39 -> 38; the verification count in step 3
is updated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc

* docs(governance): widening the required set leaves #828's guard covering 2 of 38

#828 (issue #286) adds governance/required-check-triggers.json — a gate that
fails if a required context's workflow ever gains a `paths:` filter on
`pull_request:`. It is what stops the deadlock in §2 from being reintroduced, and
its own header states the limitation:

  Adding a NEW required context to the ruleset? Add its workflow here in the same
  PR — the gate is only as complete as this list.

It ships listing 2 workflows. Step 3 of this runbook takes the required set to 38.
Applied without extending that list, the guard silently covers 2 of 38 and the
other 36 can be re-filtered by anyone with nothing to catch it — a green gate
measuring almost nothing, which is the shape of failure gate-vacuous-check exists
to prevent.

Adds step 3a: the full context -> producing-workflow mapping (seven workflows
cover all 38), plus a verification snippet that prints required / guarded /
UNGUARDED counts so the two files cannot drift silently.

Mapping verified against the workflow files rather than written from memory. Two
contexts are matrix expansions and needed checking by hand: `CodeQL Analysis
(javascript)` comes from security.yml's `codeql-analysis` job (matrix: language)
and `Lint & Test (24.x)` from ci.yml's `lint-and-test` (matrix: node-version).

Also notes that gate-openapi-conformance.yml and gate-route-ownership.yml join
the list once #844 lands, since unfiltering them is precisely the property this
guard protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tMciHkPE8To7V67CsgKKc

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Enable squash auto-merge once CI passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant