Repository navigation
fix(gate): cancel PR runs only for new commits - #71
Merged
Merged
Conversation
A newer run on the same commit (for editing the description, marking the pull request ready, reopening it) cancelled the run in progress, whose gate (`if: always()`) then failed on the jobs the cancellation stopped. As the newest check run of a name on a commit counts, that failure blocked the merge (cloud-registry-ui#242). Let only a new head commit (action `synchronize`) cancel the runs of a pull request in progress; runs for other actions wait instead, and a waiting run that is replaced starts no job. A cancelled run then only ever tests an old commit, whose failed gate blocks nothing. The gate keeps `always()` and fails closed: `!cancelled()` would skip it in cancelled runs, and GitHub counts a skipped required check as passed. Apply this to this repository's CI and the example in the gate's README, document why, and test that both use the settings. Closes #70 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FSoqMpmDCfbACLj3uy6xV
Reviewer's GuideConfigurationRestrict workflow cancellation to first-attempt
DocumentationDocument the concurrency configuration and explain how cancelled runs affect the fail-closed gate and required checks.
TestsExtend the gate test script to verify the gate condition and workflow-level cancellation settings in CI and the README example.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
uniqueg
marked this pull request as ready for review
November 10, 2026 17:06
Linked issuesChecked at commit fdbdb10 #72 fix(gate): cancel PR runs only for new commits · 4 of 5 met · 1 to check by hand
|
There was a problem hiding this comment.
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path=".github/workflows/ci.yml" line_range="28" />
<code_context>
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
- cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.action == 'synchronize' }}
jobs:
</code_context>
<issue_to_address>
**Current head stays merge-blocked**
When an older pull-request `synchronize` run is rerun while CI for a newer head is active, the rerun keeps `github.event.action == 'synchronize'` and shares the PR’s `github.ref` concurrency group, so `cancel-in-progress` cancels the run testing the current head; its `always()` gate fails, leaving the current commit blocked until CI reports again.
Make the cancellation condition distinguish stale `synchronize` runs from the run testing the current head so a stale run cannot cancel it.
</issue_to_address>
### Comment 2
<location path=".github/workflows/ci.yml" line_range="230" />
<code_context>
runs-on: ubuntu-latest
timeout-minutes: 5
+ # Not `!cancelled()`: see "Cancelled runs" in actions/gate/README.md
if: always()
needs: [lint, test]
permissions: {}
</code_context>
<issue_to_address>
**Cancelled runs still fail**
The issue asks cancelled runs to leave no failed `CI passed`, but the gate still runs `always()` and fails when a needed job was cancelled. Restricting cancellation to `synchronize` prevents the same-commit case, but a cancelled run can still leave a failed check on the old commit.
</issue_to_address>
### Comment 3
<location path="tests/gate/test.sh" line_range="113" />
<code_context>
+ failures=$((failures + 1))
+ fi
+done
+cancel="$(sed -n 's/^ *cancel-in-progress: *//p' .github/workflows/ci.yml)"
+# The README's example is the same, folded (`>-`) to fit the line length
+cancel_readme="$(awk '
</code_context>
<issue_to_address>
**Misplaced cancellation setting passes**
When the expected expression is placed in a nested, unrelated concurrency block instead of the workflow-level block, the test reads `cancel-in-progress` without checking that it is under the workflow-level `concurrency` key. If the setting is moved into a job-level concurrency block while the workflow-level setting is removed, these extractors still find the expected expression and pass, even though the workflow no longer cancels runs as intended.
Scope both checks to the intended `concurrency` mapping, for example by parsing the YAML structure rather than matching the key anywhere in the file.
Also at `tests/gate/test.sh:115-119`.
</issue_to_address>Sourcery assessment
Approval pending. 3 findings to address first.
Blocking findings: .github/workflows/ci.yml:28, .github/workflows/ci.yml:230, tests/gate/test.sh:113
A rerun keeps the event of its run, so rerunning an older commit's `synchronize` run cancelled the run on the head commit, whose gate then failed. Cancel only in a run's first attempt; reruns wait instead. Scope the test of `cancel-in-progress` to the workflow-level `concurrency` block, and read the README's folded example the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FSoqMpmDCfbACLj3uy6xV
This was referenced Nov 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A newer run on the same commit (for editing the description, marking the pull request ready, reopening it; e.g. Sourcery editing the body) cancelled the CI run in progress. Its gate (
passed, "CI passed",if: always()) then failed on the jobs the cancellation stopped, and as the newest check run of a name on a commit counts, that failure blocked the merge (cloud-registry-ui#242: run 42668413208 cancelled by 42668617719 on b183c64; the same happened again today on bb178d3, runs 42816742704/42816969540).Fix: only a new head commit cancels the runs of a pull request in progress:
run_attempt == 1: a rerun keeps its run's event, so rerunning an older commit'ssynchronizerun would otherwise cancel the run on the head commit (Sourcery finding). Runs for other actions, and reruns, wait for the run in progress instead, and a waiting run that is replaced never starts a job (no check run at all). A cancelled run then only ever tests an old commit, whose failed gate blocks nothing. The gate keepsif: always()and stays fail-closed: a needed job that failed or was cancelled (timeout, fail-fast, by hand) still fails it.Not chosen:
if: ${{ !cancelled() }}on the gate (suggested in the issue). It skips the gate in cancelled runs, and GitHub counts a skipped required check as passed, so a run cancelled by hand (or replaced by a newer one that hasn't reported yet) would allow the merge without tests. The first version of this branch did that; the adversarial review rejected it.Changes:
.github/workflows/ci.yml: the newcancel-in-progress; the gate keepsalways().actions/gate/README.md: the usage example now includes theconcurrencyblock; new section "Cancelled runs" (whyalways()and not!cancelled(), runs cancelled by hand or by rerunning an older commit's run, the cost: after a non-push event the result comes after up to two runs); the Tests section.tests/gate/test.sh: checks that the gate job ofci.ymland of the README's example runsalways()(job-levelif:only, with parser self-tests) and that both use the newcancel-in-progress.No file that callers run changes (
actions/gate/action.yml,check.share untouched), so per RELEASING.md this needs no release; it appears in the notes of the next one.Closes #72
Part of #70; the callers follow in #73.
Verification
pre-commit run --all-files(all hooks; markdownlint-cli2 v0.24.0 run separately via npx because nodeenv fails locally): passed.bash tests/gate/test.sh,tests/pr-title,tests/labels,tests/publish-release,tests/release: all passed.${{ !cancelled() }}orcancel-in-progressto${{ github.event_name == 'pull_request' }}inci.ymlmakestests/gate/test.shfail.concurrency), 1 answered (a failed gate on an old commit is intended and blocks nothing); all resolved.Rollout
The callers set
cancel-in-progressin their ownci.yml, so a pin bump changes nothing for them: each needs a one-line change ofcancel-in-progressto the expression above (their gates already runalways()). All pinactions/gate@743e82d… # v1.0.0:cloud-registry and protes (
tests/test_workflows.py) and trs-filer (tests/test_config.py) assert the gate'sif: always(), which stays valid; their docs/development.md describe the gate. This PR doesn't change any of them.Declined review findings
match, own fixture file, job-levelif:only) because a grep can't tell a job-levelif:from a step's (adversarial finding, round 1).cancelled()): moot, the gate keepsalways().Open questions
None.
🤖 Generated with Claude Code
https://claude.ai/code/session_011FSoqMpmDCfbACLj3uy6xV