Skip to content

ci: INT-726 let scoped PRs satisfy their required checks - #476

Merged
piekstra merged 3 commits into
mainfrom
piekstra/INT-726-ci-scoped-pr-gates-signed
Aug 14, 2026
Merged

ci: INT-726 let scoped PRs satisfy their required checks#476
piekstra merged 3 commits into
mainfrom
piekstra/INT-726-ci-scoped-pr-gates-signed

Conversation

@piekstra

Copy link
Copy Markdown
Contributor

[INT-726]

Replaces #475 with signed commits — see "Why this replaces #475" at the bottom. Same diff, already reviewed and approved there.

Branch protection requires build-test-cfl, build-test-jtk, lint-cfl, lint-jtk, identity-check-cfl, identity-check-jtk. All six were gated at the job level on paths-filter output, so a PR touching one tool skipped the other tool's jobs — and GitHub does not accept a skipped check as a satisfied required check.

Every single-tool PR is therefore unmergeable without an admin bypass. #464 and #474 both hit it; a jtk-only change hits it in the opposite direction.

The cost isn't the inconvenience. Branch protection works because merging over an unmet gate is deliberate and visible. When routine work can't merge without a bypass, the bypass stops being remarkable — and a genuine unmet gate looks like the twenty benign ones before it.

Change

Work stays in <job>-run, which keeps its job-level condition and may skip freely, with no per-step guards. The protected check name lives on a wrapper that always runs and reports on the -run job's behalf via a local composite action:

build-test-cfl:
  needs: [detect-changes, build-test-cfl-run]
  if: always()
  steps:
    - uses: actions/checkout@v4
    - uses: ./.github/actions/required-check
      with:
        job: build-test-cfl-run
        result: ${{ needs.build-test-cfl-run.result }}
        gate-result: ${{ needs.detect-changes.result }}

detect-changes is in needs deliberately. Without it, a detect-changes failure empties the outputs, every -run condition goes false, all six skip, and the wrappers would pass every required check with nothing verified — trading "skipped check blocks a good PR" for "skipped check waves through a broken one".

Branch protection settings are untouched; the required names are unchanged, just produced by the wrappers.

Verification

Decision table exercised directly by running the gate script over every combination:

GATE      RESULT     exit
success   success    0     ran and passed
success   skipped    0     no relevant changes; nothing to verify
success   failure    1
success   cancelled  1
failure   skipped    1     <- detect-changes failure cannot wave checks through
skipped   skipped    1
cancelled skipped    1

actionlint clean. On #475 all six required checks reported SUCCESS, confirming the wrapper mechanism works end to end.

Limit: ci.yml is in all three paths filters, so this PR marks every tool relevant and cannot exercise the skip path itself. The next tool-scoped PR is the real proof.

Unrelated flake surfaced

#475 hit TestClient_Do/DELETE_request failing in build-test-shared"transport connection broken: CloseIdleConnections called". Not from this change (the diff touches only .github/), passes 5/5 locally under -race, and shared/client last changed in #348. It passed on re-run. Worth noting that build-test-shared only runs when shared/**, go.work, Makefile or ci.yml changes, so this flake is normally invisible on tool-scoped PRs.

Why this replaces #475

main also requires signed commits, and my commits on #475 were unsigned — a second, independent blocker that the admin bypass on #474 had masked. These are the same three commits, cherry-picked and signed (verified=true). #475 will be closed.

Branch protection requires the six per-tool jobs, but they were gated at the
job level on paths-filter output, so a PR touching one tool skipped the other
tool's jobs. GitHub does not accept a skipped check as a satisfied required
check, so every single-tool PR was unmergeable without an administrator
bypass. #464 and #474 both hit it.

The cost is not the inconvenience. Branch protection works because merging
over an unmet gate is deliberate and visible; when ordinary work cannot merge
without a bypass, the bypass stops being remarkable and a real unmet gate
looks like the routine ones before it.

The six required jobs now run on every PR and gate their steps instead. A job
with nothing to do reports success rather than skipping, which is the honest
result -- the check ran and found no work -- and the required checks are
satisfied without changing repository settings.

build-test-shared and lint-shared keep their job-level condition; neither is
a required check.

[INT-726]
Review round. Gating every step duplicated the condition six times with
nothing enforcing that a step added later carried it, and since
actions/checkout was itself gated, an unguarded step on an irrelevant PR
would have run against an unchecked-out workspace and failed for a reason
that had nothing to do with the change.

The work now stays in <job>-run, which keeps its job-level condition and may
skip freely with no per-step guards at all. The protected check name lives on
a wrapper that always runs and reports the outcome of the job that did the
work, treating skipped as a pass because there was nothing to verify. The
guard exists once per job rather than once per step, so adding a step to a
-run job needs no thought.

Branch protection is untouched: the required names are unchanged, they are
just produced by the wrappers now.

[INT-726]
Review round, and the first finding was a hole this change introduced.

A -run job is skipped whenever its condition is false, and the condition
reads detect-changes outputs. If detect-changes fails, those outputs are
empty, every condition evaluates false, and all six jobs skip -- so a wrapper
treating skipped as a pass reported every required check green with nothing
verified at all. That is the failure this PR exists to prevent, reintroduced
one level up.

Wrappers now take detect-changes in needs and fail unless it succeeded, so a
skip only passes when it genuinely means there was no work.

The case statement was also repeated once per wrapper and would grow with
each tool the repo adds. It moves into a local composite action, so a wrapper
is now four lines and the logic exists once. The decision table is verified
directly: success and skipped pass only when change detection succeeded;
failure, cancellation, and any non-success gate all fail.

[INT-726]

@monit-reviewer monit-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: 578f101d76a4
Profile: claude-monit-reviewer - Posting as: monit-reviewer

Summary

Reviewer Findings
policies:conventions 0
automation:ci-release 0
structure:repo-health 1
structure:repo-health (1 finding)

Minor - .github/workflows/ci.yml:38

The gate decision table (7 cases: success/skipped/failure/cancelled combinations of a -run job's result crossed with detect-changes' result) and the actionlint pass the PR cites as verification were both run manually and are not captured anywhere in CI. Nothing in this repo's workflows runs actionlint, and the decision table exists only as prose in the PR body, not as a script/test under version control. This is exactly the class of bug this PR fixes (a workflow-YAML gating mistake that silently breaks required-check enforcement) and it will now recur invisibly: a future edit to required-check/action.yml or to any of the six near-identical wrapper blocks can regress the gate logic (e.g. drop the gate-result check, mis-wire needs, or flip a case) with no CI signal, only manual review to catch it. Add an actionlint step to CI (or a pre-commit/make lint target already run in CI) so workflow syntax is checked automatically, and commit the decision-table cases as a small bats/shell test that exercises .github/actions/required-check/action.yml's script directly so a regression fails the build instead of requiring another manual re-verification.

Reviewer Coverage

  • policies:conventions — complete (broad); skipped: none; constraints: none
  • automation:ci-release — complete (broad); skipped: none; constraints: none
  • structure:repo-health — complete (broad); skipped: none; constraints: ci.yml is touched by every tools' paths filter, so this PR cannot exercise the skip path it introduces; the next tool-scoped PR is the real end-to-end proof, per the PR's own stated limit.
Inspected files (2)
  • .github/actions/required-check/action.yml
  • .github/workflows/ci.yml

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 3m 49s | ~$2.40 (est.) | claude-sonnet-5 | cr 0.10.286
Field Value
Model claude-sonnet-5
Reviewers policies:conventions, automation:ci-release, structure:repo-health
Engine claude_cli · claude-sonnet-5
Reviewed by cr · monit-reviewer
Duration 3m 49s wall · 6m 01s compute
Cost ~$2.40 (est.)
Tokens 64 in / 15.7k out

Per-workstream usage

  • orchestrator-selection — claude-sonnet-5
    • In: 6
    • Out: 2.3k
    • Cache read: 101.3k
    • Cache create: 79.4k
    • Cost: ~$0.36 (est.)
    • Duration: 33s
  • policies:conventions — claude-sonnet-5
    • In: 24
    • Out: 3.8k
    • Cache read: 788.8k
    • Cache create: 85.1k
    • Cost: ~$0.61 (est.)
    • Duration: 1m 11s
  • automation:ci-release — claude-sonnet-5
    • In: 14
    • Out: 5.0k
    • Cache read: 411.0k
    • Cache create: 85.5k
    • Cost: ~$0.52 (est.)
    • Duration: 1m 13s
  • structure:repo-health — claude-sonnet-5
    • In: 16
    • Out: 4.1k
    • Cache read: 488.3k
    • Cache create: 86.2k
    • Cost: ~$0.53 (est.)
    • Duration: 2m 48s
  • orchestrator-rollup — claude-sonnet-5
    • In: 4
    • Out: 464
    • Cache read: 31.8k
    • Cache create: 95.5k
    • Cost: ~$0.37 (est.)
    • Duration: 13s

Comment thread .github/workflows/ci.yml
@@ -38,7 +38,7 @@ jobs:
- 'Makefile'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

File-level note: .github/workflows/ci.yml

The gate decision table (7 cases: success/skipped/failure/cancelled combinations of a -run job's result crossed with detect-changes' result) and the actionlint pass the PR cites as verification were both run manually and are not captured anywhere in CI. Nothing in this repo's workflows runs actionlint, and the decision table exists only as prose in the PR body, not as a script/test under version control. This is exactly the class of bug this PR fixes (a workflow-YAML gating mistake that silently breaks required-check enforcement) and it will now recur invisibly: a future edit to required-check/action.yml or to any of the six near-identical wrapper blocks can regress the gate logic (e.g. drop the gate-result check, mis-wire needs, or flip a case) with no CI signal, only manual review to catch it. Add an actionlint step to CI (or a pre-commit/make lint target already run in CI) so workflow syntax is checked automatically, and commit the decision-table cases as a small bats/shell test that exercises .github/actions/required-check/action.yml's script directly so a regression fails the build instead of requiring another manual re-verification.

Reply inline to this comment.

@piekstra
piekstra marked this pull request as ready for review August 14, 2026 11:34
@piekstra
piekstra merged commit 51de27e into main Aug 14, 2026
16 checks passed
@piekstra
piekstra deleted the piekstra/INT-726-ci-scoped-pr-gates-signed branch August 14, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants