Skip to content

fix(pr-validation): scope, cancellation and the commit-list cap - #932

Merged
bedatty merged 5 commits into
developfrom
fix/gate-scope-cancellation-commit-cap
Oct 19, 2026
Merged

bedatty merged 5 commits into
developfrom
fix/gate-scope-cancellation-commit-cap

Conversation

@bedatty

@bedatty bedatty commented Oct 19, 2026

Copy link
Copy Markdown
Contributor
Lerian

GitHub Actions Shared Workflows


Description

The three High findings from Radagast's review of #926. One commit each.

1. The Environment gate ran outside its own scope

Neither the approval job nor the preflight carried an in-scope condition — they gated on has-breaking-changes alone.

Before #918 that was survivable, because out-of-scope normalisation forced the detection to false. #918 started preserving the caller's real result — correctly, so the acknowledgement could still be reported — and removed the only thing keeping Environment work out of scope.

A breaking pull request excluded by a branch rule, a skip label or an ignored author therefore waited on an Environment it had no business touching, or failed because that Environment could not be read, and the verdict then discarded the answer and returned success.

Scope now gates the approval job, the preflight, the CODEOWNERS lookup that exists to create the Environment, and the reuse lookup. The acknowledgement report keeps running outside scope, which is the part #918 got right.

2. A cancellation passed enforcement with nothing replacing it

#912 treated every cancelled approval as supersession. The reasoning was sound — a run can be replaced without the head moving, since editing the description queues a new run on the same commit — but too wide. A cancellation with nothing replacing it reported success for a revision nobody approved, while result=failure and approved=false sat in the outputs saying the opposite.

Supersession is now established rather than assumed: either the head moved on, or a newer run exists for this branch to carry the decision. Neither leaves it unresolved, which does not pass.

Scenario Before Now
cancelled, newer run exists superseded ✅ superseded ✅
cancelled, head moved superseded ✅ superseded ✅
cancelled, nothing replacing it superseded → passed ❌ unresolved → blocks ✅

3. A verdict from a truncated commit list

The commits endpoint returns at most 250 commits, pagination or not. The resolver paginated and trusted what came back.

A break living past the cap read as "no breaking change". With approval reuse it is worse: an approved break inside the window plus an omitted one outside leaves the fingerprint unchanged, so an approval is honoured for a change it never covered.

The returned count is now compared against the pull request's declared total, and a mismatch is refused rather than answered. The caller fails closed on it.

Refusing rather than reconstructing history is deliberate, and it is a trade: pull requests above 250 commits can no longer be gated automatically. That is the honest answer rather than a confident wrong one — but if you would rather have complete-history detection, say so and I will take that route instead.

Type of Change

  • fix: Bug fix in a workflow (incorrect behavior, broken step, wrong condition)

Breaking Changes

None for callers — no input, output or job name changes.

Three behaviour changes, all corrections: out-of-scope pull requests no longer wait on the Environment; an unexplained cancellation no longer passes; a pull request above 250 commits now fails the gate instead of being waved through.

Testing

  • YAML syntax validated locally
  • Triggered a real workflow run on a caller repository using @this-branch or the beta tag
  • Verified all existing inputs still work with default values
  • Confirmed no secrets or tokens are printed in logs
  • Checked that unrelated workflows are not affected

Verdict logic exercised against a stubbed gh for every cancellation path, plus approval, rejection and moved-head to confirm no regression. The commit cap has two new cases — a truncated list is refused, a complete one is answered — with the refusal verified by mutation.

Three existing tests failed and were updated, which is the part worth reading: they pinned cancelled → superseded unconditionally. They were encoding the bug, not guarding against it.

Suites: 95 wiring, 22 + 32 + 22 + 60 shell. actionlint, composite-schema, shellcheck, typos clean.

Related Issues

Radagast's High on #926.

Neither the approval job nor the preflight carried an in-scope
condition; they gated on has-breaking-changes alone. Before #918 that
was survivable because out-of-scope normalisation forced the detection
to false. #918 started preserving the caller's real result — correctly,
so the acknowledgement could still be reported — and removed the only
thing keeping Environment work out of scope.

A breaking pull request excluded by a branch rule, a skip label or an
ignored author therefore waited on an Environment it had no business
touching, or failed because that Environment could not be read, and the
verdict then discarded the answer and returned success.

Scope now gates everything the Environment touches: the approval job,
the preflight, the CODEOWNERS lookup that exists to create it, and the
reuse lookup. The acknowledgement report keeps running outside scope,
which is the thing #918 was right about.
Radagast's High on #926.

#912 treated every cancelled approval as supersession. The reasoning was
sound — a run can be replaced without the head moving, because editing
the description queues a new run on the same commit — but it was too
wide. A cancellation with nothing replacing it reported success for a
revision nobody approved, while result=failure and approved=false sat in
the outputs saying the opposite.

Supersession is now established, not assumed: either the head moved on,
or a newer run exists for this branch to carry the decision. Neither
leaves the cancellation unresolved, which does not pass.

Exercised against a stubbed gh: cancelled with no newer run resolves to
unresolved; cancelled with a newer run and cancelled with a moved head
both resolve to superseded; approval, rejection and moved-head paths are
unchanged.
Radagast's High on #926, and the one I would not have found.

The commits endpoint returns at most 250 commits, pagination or not.
The resolver paginated and trusted the result, so a break living past
the cap read as "no breaking change" — and with approval reuse it is
worse: an approved break inside the window plus an omitted one outside
leaves the fingerprint unchanged, so an approval is honoured for a
change it never covered.

The returned count is now compared against the pull request's declared
total, and a mismatch is refused rather than answered. The caller fails
closed on it: detection-succeeded goes false and the verdict blocks.

Refusing rather than reconstructing the history is deliberate. A gate
that cannot see the whole change should say so; guessing from a subset
is how it ends up approving one break while blind to another. The cost
is that pull requests above 250 commits cannot be gated automatically,
which is the honest answer rather than a confident wrong one.

Two cases added. The suite also needed `|| capped_status=$?` on the
assignment, since `set -e` takes the script down on a substitution that
exits non-zero — the same trap flagged on #915 and sidestepped in `run`,
walked into again in a new block.
@bedatty
bedatty requested a review from a team as a code owner October 19, 2026 14:50
@lerian-studio

lerian-studio commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

🔍 Lint Analysis

Check Files Scanned Status
YAML Lint 1 file(s) ✅ success
Action Lint 1 file(s) ✅ success
Pinned Actions 1 file(s) ✅ success
Markdown Link Check 1 file(s) ✅ success
Spelling Check 5 file(s) ✅ success
Shell Check 1 file(s) ✅ success
README Check 1 file(s) ✅ success
Composite Schema no changes ⏭️ skipped
Deployment Matrix no changes ⏭️ skipped

🔍 View full scan logs

@lerian-studio lerian-studio added size/S PR changes 50–199 lines workflow Changes to one or more reusable workflow files validate Changes to PR validation composite actions (src/validate/) scripts Changes to shell scripts (src/**/*.sh) labels Oct 19, 2026
@lerian-studio

lerian-studio commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

🔍 PR Validation Summary

✅ PR Mergeable — no blocking failures

Check Status Blocking
Source Branch ✅ success yes
PR Title ✅ success yes
PR Description ✅ success yes
Breaking Change Guard ✅ success yes
Commit Signatures ✅ success yes
PR Size ✅ success no
Auto Labels ✅ success no
PR Metadata ✅ success no

🔍 View workflow run

@lerian-studio

lerian-studio commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

🛡️ CodeQL Analysis Results

Languages analyzed: actions

✅ No security issues found.


🔍 View full scan logs | 🛡️ Security tab

@lerian-studio

This comment has been minimized.

@lerian-studio lerian-studio added the review-ready Required checks passed — CodeRabbit is cleared to review label Oct 19, 2026
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

The approval workflow now gates approval-related steps by pull-request scope and checks for newer runs before classifying a cancelled approval as superseded. The commit resolver verifies that the returned commit list matches the pull request’s declared commit count.

Changes

Approval workflow

Layer / File(s) Summary
Scope-gated approval steps
.github/workflows/breaking-change-approval.yml, src/validate/breaking-change-guard/test-workflow.py
Prior-approval lookup, reviewer derivation, Environment preflight, and the approval job now require an in-scope pull request. Tests check approval steps in scope and the acknowledgement notice out of scope.
Cancellation supersession check
.github/workflows/breaking-change-approval.yml, src/validate/breaking-change-guard/test-workflow.py
For a cancelled approval, the verdict step checks up to 50 runs for a newer run on the head branch. A newer run produces a superseded verdict without updating the pull request. Tests cover cancellation with and without a replacement run, a changed head, and a recorded rejection.

Commit enumeration

Layer / File(s) Summary
Commit-list completeness check
src/validate/breaking-change-commits/resolve.sh, src/validate/breaking-change-commits/test.sh
The resolver now checks the returned commit count against the pull request’s declared count before evaluating commit messages. Tests cover incomplete and matching counts.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: 🟡 Moderate · up to 76ede

A cancelled breaking-change approval can be treated as superseded by another pull request’s run, allowing validation to pass without the current PR’s replacement approval. Out-of-scope runs also make an unnecessary Environment approval lookup. Resolve these before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (2 skipped: 2… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title is concise and accurately identifies the three main fixes: scope handling, cancellation handling, and the commit-list cap.
Description check Passed The description is complete and directly matches the changes. It includes the change summary, type, breaking-change assessment, testing details, and validation results. The caller workflow run was not…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is complete and directly matches the changes. It includes the change summary, type, breaking-change assessment, testing details, and validation results. The caller workflow run was not performed and the Related Issues section is not populated, but both are clearly represented or non-critical.

Full details: Docstring Coverage

Explanation

Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (2 skipped: 2 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

This comment has been minimized.

coderabbitai[bot]

This comment was marked as resolved.

bedatty added a commit that referenced this pull request Oct 19, 2026
`actions/runs?branch=` returns runs of every workflow on the branch, and
the siblings from the same push start later with higher ids — so any
cancelled approval found a "newer run" and passed. Measured on
test/final-bang: a run of Self — Repository Routines sat above the gate's
own run id, which restores the exact false success #932 set out to remove.

The check now resolves the current run's `workflow_id` and queries that
workflow's runs. The test stub answers only the scoped URL, so a
regression to the broad query reads as "no newer run" and fails the
supersession cases instead of passing them silently.

Also documents the 250-commit refusal in the composite README.
@lerian-studio lerian-studio added size/M PR changes 200–499 lines documentation Improvements or additions to documentation and removed size/S PR changes 50–199 lines labels Oct 19, 2026
`actions/runs?branch=` returns runs of every workflow on the branch, and
the siblings from the same push start later with higher ids — so any
cancelled approval found a "newer run" and passed. Measured on
test/final-bang: a run of Self — Repository Routines sat above the gate's
own run id, which restores the exact false success #932 set out to remove.

The check now resolves the current run's `workflow_id` and queries that
workflow's runs. The test stub answers only the scoped URL, so a
regression to the broad query reads as "no newer run" and fails the
supersession cases instead of passing them silently.

Also documents the 250-commit refusal in the composite README.
@bedatty
bedatty force-pushed the fix/gate-scope-cancellation-commit-cap branch from 8f8632f to 76edec7 Compare October 19, 2026 16:19
@lerian-studio

This comment has been minimized.

@coderabbitai

This comment has been minimized.

coderabbitai[bot]

This comment was marked as resolved.

A head branch can carry more than one pull request — same branch,
different bases — so scoping the query to the gate's own workflow still
let another pull request's run stand in for this one's and pass a
cancellation as superseded. The filter now also matches the run's
`.pull_requests`, falling back to the branch scope when the field comes
back empty, which is what runs from forks look like.

The `gh` stub now executes the real `--jq` against a real payload. The
branch-wide query survived a full round of review because the stub
returned the count directly and never ran the filter — the query is the
thing under test, so it has to actually run. Verified by removing the
filter: test_a_newer_run_of_another_pull_request_is_not_supersession
fails.
@lerian-studio

Copy link
Copy Markdown
Contributor

@coderabbitai review

@bedatty
bedatty merged commit 08cce0f into develop Oct 19, 2026
43 checks passed
@github-actions
github-actions Bot deleted the fix/gate-scope-cancellation-commit-cap branch October 19, 2026 16:55
@bedatty bedatty mentioned this pull request Oct 19, 2026
7 of 8 tasks
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Pull request is closed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation review-ready Required checks passed — CodeRabbit is cleared to review scripts Changes to shell scripts (src/**/*.sh) size/M PR changes 200–499 lines validate Changes to PR validation composite actions (src/validate/) workflow Changes to one or more reusable workflow files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants