Skip to content

tests.yml and release.yml: a push to a release/v<major>.<minor> branch that holds this change runs the suite as a push to main does, so a patch's commit there gets its push run with no dispatch, a fast-forward of that branch to main's commit no longer cancels main's run on it, and the release header says how a fix is back-ported (#850) - #852

Merged
brettheap merged 3 commits into
mainfrom
l3-850-release-branches
Oct 17, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Part of #850. This is a draft on the v1.1.1 milestone, and it lands only after the v1.1.0 tag.

The ruling

Brett Heap, at 2026-10-17T19:52:26Z, in lane openRepoShape-3's session:

back-port to release/v1.1 as 1.1.1, open the issue

This PR covers two of #850's work items: the tests.yml trigger and the procedure in release.yml's RELEASING section. The release/* ruleset, release/v1.1 itself and the v1.2.0 milestone stay outside it.

What changed, and why

.github/workflows/tests.yml runs on pushes to release branches: push: branches: [main, 'release/v*'].

  • release.yml releases a commit only once a push or workflow_dispatch run of tests.yml passed tests on that very commit. Until now, every commit on release/v1.0 needed a dispatch by hand (the v1.0.1 notes: "A push to release/v1.0 runs no CI by itself").
  • Now a commit that a release branch takes gets its push run from its own push, once the branch holds this change. A push runs the tests.yml its own commit holds (GitHub: "Each workflow run will use the version of the workflow that is present in the associated commit SHA or Git ref of the event"). So release/v1.1, cut at v1.1.0, runs the suite from its first fast-forward past this change, or a cherry-pick of it. release/v1.0 never has, and still needs a dispatch.
  • release/v* takes each of them: every release/v<major>.<minor> that release.yml's build job accepts (for branch in main "release/v${VERSION%.*}"), and any other one-segment name that starts release/v, such as release/vnext. In a branch filter, GitHub's * "matches zero or more characters, but does not match the / character".
  • The header comment that lists the triggers says so, and so does the Git Bash job's "ON EVERY TRIGGER" note.

The concurrency group gains ${{ github.ref }} beside the commit. See the next section for the reason.

.github/workflows/release.yml, comments only:

tests/test_workflow_concurrency.py holds the trigger and the new group (see Tests).

README.md, docs/cli.md and docs/handbook.html state neither tests.yml's triggers nor a release-branch procedure (git grep over the three finds no tests.yml, release/v or back-port text), so none of them changes. AGENTS.md is untouched. No statement goes stale. docs/release-notes/v1.0.1.md says "A push to release/v1.0 runs no CI by itself", in v1.0.1's own "Before tagging" record, and that is still true: a push runs the tests.yml its own commit holds, and release/v1.0's (8cda139) still says push: branches: [main]. The file is left as it is.

Concurrency: yes, it could cancel, so the group changed

The finding. With the old group, ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.sha }}, a push to release/v1.1 could cancel a run of main's, and main's could cancel one of the release branch's. Three facts lead there:

  • A push run has no PR number, so its group is tests-push-<sha>.
  • github.sha for a push is the "Tip commit pushed to the ref" (GitHub's events docs).
  • A concurrency group spans the repository, not a branch (the docs: "another job or workflow using the same concurrency group in the repository").

So a fast-forward of release/v1.1 to main's commit X makes a run in tests-push-X, which is the group of main's push run on X. With cancel-in-progress: true, it cancels main's run if that run is still going, so a back-port made right after a landing would hit this. That is #592's silent failure again: main's run ends cancelled. The cut of release/v1.1 at the v1.1.0 tag also pushes a commit main has, though main's run on it has finished by then, since the tag waits for it.

The other way round, main's push cancels a release branch's run on the same commit only if main takes a commit after the release branch took it. The model never does that, but the old group allowed it.

The change. The group is now ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }}. The ref sits beside the commit, never in its place, as #592's old fallback was.

  • Main and a release branch at one commit are two groups, so neither push run cancels the other.
  • A PR's ref is refs/pull/<n>/merge, its own, so a newer push to a PR still cancels its older run.
  • A second dispatch replaces the first only on the same ref and commit.

Once the branch holds this change, a fast-forwarded commit therefore gets two push runs, main's and the release branch's. The release gate counts both, because it filters on head_sha, path and event, not on branch. Neither may still be running, and the newest finished one decides.

The other workflows

.github/workflows/ holds two files, tests.yml and release.yml. No other workflow there has a branches: [main] push trigger.

  • release.yml is workflow_dispatch only, and it should not follow: "an upload is an act Brett Heap approves", and its tests pin that (test_a_dispatch_with_a_version_is_the_only_trigger).
  • The two workflows this repository ships into projects, templates/assembly-root/.github/workflows/validate.yml and templates/family-root/.github/workflows/validate.yml, run on pull_request and workflow_dispatch only. They run in a scaffolded project and not here, so they should not follow either.

This PR changes none of them.

Tests

The tests that read either workflow, from git grep -n -E 'tests\.yml|release\.yml' tests/:

  • tests/test_workflow_concurrency.py and tests/test_release_workflow.py;
  • tests/test_install_sh.py::test_it_installs_the_git_filter_repo_ci_runs.

The other hits name the files in docstrings and comments only. tests/test_repo_hygiene.py reads every tracked file, workflows included, and tests/test_shape_advisory.py::test_only_the_three_tools_read_the_advisory reads every file under .github/. None of them pinned branches: [main] or tests.yml's trigger header. test_the_header_says_how_to_release pins nine release.yml header phrases: the six it had on main, none of them in the lines this PR changes, and three that fix round 1 adds for #850's section (see below).

New in tests/test_workflow_concurrency.py:

At 417951c, every run was made under a stub gh first on PATH that exits 97, with GH_TOKEN=bogus and an empty GH_CONFIG_DIR. Here S=/workspace/projects/openRepoShape-worktrees/_lane3-scratch/W850-scratch, and P is /usr/bin/python3 -m pytest -q -p no:cacheprovider -o tmp_path_retention_policy=failed:

$P --basetemp=$S/bt-final-wf tests/test_workflow_concurrency.py tests/test_release_workflow.py
69 passed in 3.76s
$P --basetemp=$S/bt-final-hyg tests/test_repo_hygiene.py
77 passed in 1.96s
$P --basetemp=$S/bt-final-inst tests/test_install_sh.py
64 passed in 13.07s
$P --basetemp=$S/bt-final-adv tests/test_shape_advisory.py::test_only_the_three_tools_read_the_advisory
1 passed in 1.09s
actionlint .github/workflows/tests.yml .github/workflows/release.yml templates/assembly-root/.github/workflows/validate.yml templates/family-root/.github/workflows/validate.yml
(actionlint 1.7.12, the version tests.yml pins: no output, exit 0)
git diff --check
(exit 0)

release.yml changes in comments only. At 417951c, origin/main:.github/workflows/release.yml was compared with HEAD's (fix round 1 repeats this at its own head, below):

comment lines removed, byte-identical: True (49356 and 49356 bytes)
yaml.safe_load identical: True (PyYAML 6.0.3)
repo_shape.load_yaml identical: True
changed lines: 28; not a comment line: 0; hunks: ['@@ -29,6 +29,9 @@', '@@ -56,0 +60,13 @@']

Both hunks lie in the header, above on:.

Mutants

  1. (At 417951c.) Drop 'release/v*', so the trigger is branches: [main]. The result is 3 failed, 10 passed:
    FAILED tests/test_workflow_concurrency.py::test_a_push_to_main_or_to_a_release_branch_runs_the_suite
    FAILED tests/test_workflow_concurrency.py::test_which_branchs_push_runs_the_suite[release-v1.1]
    FAILED tests/test_workflow_concurrency.py::test_which_branchs_push_runs_the_suite[release-v1.0]
    
  2. Put back main's group, without ${{ github.ref }}. The result is 1 failed, 12 passed:
    FAILED tests/test_workflow_concurrency.py::test_a_run_is_grouped_by_its_event_its_ref_and_its_commit
    
    The reason it gives: "it does not name github.ref, so the push run of a release branch fast-forwarded to main's commit shares a group with main's push run on that commit and cancels it (Release branching: main stays the trunk; release/vX.Y branches carry tagged releases and their patches; v1.1.1 back-ported to release/v1.1 #850)".

The file was restored after each mutant and checked by sha256.

Merges

  • git merge-tree --write-tree HEAD origin/main at fc8672073ce231d508f922b99e0cd9ae5e90d088 is clean, and gives bedbd3a9f84db5ebfb4f67b117bf49fd7946acfa, HEAD's own tree, since main is HEAD's parent.
  • Of the 53 open PRs, none touches tests.yml, release.yml or tests/test_workflow_concurrency.py (gh pr list --state open --limit 100 --json number,headRefOid,files), so there is no other merge to run.

The ruleset question, which is Brett Heap's

GitHub cannot fast-forward a branch through a pull request. A ruleset on release/* that requires a pull request for every change therefore rules out the fast-forward back-port. The choice is between:

  • no force-push and no deletion, with no pull-request rule, so the fast-forward stays;
  • a pull-request rule, with every back-port a git cherry-pick -x.

That choice is Brett Heap's, and this PR makes no ruleset.

Fix round 1 (RV852)

RV852 reviewed the head 417951c (issuecomment-6240653347): READY, no MUST. One new commit, 900ffe70b6b340b52d00b9ee68b2830f546d6eee, takes its four SHOULDs and five NITs. It changes tests.yml, release.yml and the two test files, and release.yml still changes in comments only.

Item What changed
SHOULD-1: a push runs the tests.yml its own commit holds tests.yml's header block (lines 16-25) says a release branch runs the suite only once it holds this change: release/v1.1 from its first fast-forward past it, or a cherry-pick of it; release/v1.0 never has, and still needs a dispatch. release.yml has a procedure bullet that says the same (lines 77-80). The docstring of test_which_branchs_push_runs_the_suite says the rows test only the pattern (tests/test_workflow_concurrency.py:211-219). The v1.0.1 sentence above is corrected: it does not go stale.
SHOULD-2: which run decides Step 1 (release.yml:28-40): once the branch holds #850's tests.yml, the fast-forward also starts the branch's own push run on that commit, and that newer run decides; tag once it has finished green. The procedure's last bullet now says "once step 1 holds for that commit: its tests.yml runs have all finished, and the newest is green" (lines 81-83), in place of "that commit's tests.yml run".
SHOULD-3: S9073 tests/test_workflow_concurrency.py:183-184: the composite assert in branch_filter_matches is two asserts, assert not set(pattern) & set("?+[]!"), pattern and assert "**" not in pattern, pattern.
SHOULD-4: mutant M3 tests/test_workflow_concurrency.py:199: assert on["pull_request"] is None, on["pull_request"].
NIT-1: the pattern is not exact tests.yml:20-21 says release/v* "takes each of them, and any other name of one segment that starts release/v", where it claimed to name exactly the release branches. The body's bullet on the pattern says the same now.
NIT-2: the ruleset caveat release.yml:74-76: the fast-forward stands unless a release/* ruleset requires a pull request (that ruleset waits on Brett Heap's word, #850); then every back-port is the cherry-pick. I wrote "waits on Brett Heap's word" and not "Brett Heap's word", since #850 leaves the ruleset to him and he has not ruled on it.
NIT-3: layout and wording release.yml:63: a # separator line before the new section. tests.yml:39: "A pull request's group is its ref and its number." The test's GROUP comment (tests/test_workflow_concurrency.py:51) says the same.
NIT-4: no test pins the #850 text tests/test_release_workflow.py:597-601: test_the_header_says_how_to_release pins # RELEASE BRANCHES AND BACK-PORTS (#850), the push-rule bullet and step 1's "that newer run decides". A new test, test_the_header_says_why_a_release_branch_runs_it_and_when (tests/test_workflow_concurrency.py:225), pins the two #850 blocks and the push rule in tests.yml, because M7 changes tests.yml, which the release test does not read.
NIT-5: the phrase count The Tests section above says nine, which is six on main plus three added.

Tests, at 900ffe70, under the same stub gh, GH_TOKEN=bogus and an empty GH_CONFIG_DIR (S=/workspace/projects/openRepoShape-worktrees/_lane3-scratch/F852-scratch):

tests/test_workflow_concurrency.py tests/test_release_workflow.py
70 passed in 5.26s
tests/test_repo_hygiene.py
77 passed in 2.37s
tests/test_install_sh.py
64 passed in 16.90s
actionlint .github/workflows/tests.yml .github/workflows/release.yml   (1.7.12)
(no output, exit 0)
git diff --check HEAD~1 HEAD
(exit 0)

release.yml is still comments only, at 900ffe70, against origin/main (190133d) and against the PR's merge base (fc86720):

comment lines removed, byte-identical: True (49356 and 49356 bytes, sha256 f68b3b11aae74575 for both)
yaml.safe_load identical: True (PyYAML 6.0.3)
repo_shape.load_yaml identical: True
changed lines: 39; not a comment line: 0; hunks: ['@@ -29,6 +29,12 @@', '@@ -57,0 +64,21 @@']

tests.yml has two non-comment changes against main, /on/push/branches and /concurrency/group, and none against 417951c.

Mutants, each run on the two workflow test files, the file restored from a copy and its sha256 checked after:

Mutant Result Killed by
M1: drop 'release/v*' 3 failed, 67 passed test_a_push_to_main_or_to_a_release_branch_runs_the_suite, test_which_branchs_push_runs_the_suite[release-v1.1] and [release-v1.0]
M3: pull_request: branches: [main] 1 failed, 69 passed test_a_push_to_main_or_to_a_release_branch_runs_the_suite (assert {'branches': ['main']} is None)
M6: release.yml set back to main's 1 failed, 69 passed test_the_header_says_how_to_release
M7: tests.yml's two #850 comment blocks removed 1 failed, 69 passed test_the_header_says_why_a_release_branch_runs_it_and_when

Lane: openreposhape-3 (openRepoShape-3)
🤖 Generated with Claude Code

Summary by Sourcery

Run CI on supported release branches, isolate their test runs from main, and document the back-port release procedure.

New Features:

  • Run the test suite automatically on pushes to supported release branches, enabling release commits to satisfy their test gate without manual dispatches.
  • Document and enforce the release-branch back-port and patch-release procedure.

Bug Fixes:

  • Prevent concurrent test runs for main and release branches at the same commit from cancelling one another.

Enhancements:

  • Expand workflow tests to validate release-branch triggers, concurrency isolation, and the documented release process.

Documentation:

  • Clarify workflow behavior, release-branch back-ports, and which test run determines release readiness.

Tests:

  • Add coverage for release-branch matching and ref-aware workflow concurrency configuration.

…nch runs the suite as a push to main does, so each commit a release branch takes carries the push run a patch's tag needs, a fast-forward of that branch to main's commit no longer cancels main's run on it, and the release header says how a fix is back-ported (#850)

Brett Heap ruled at 2026-10-17T19:52:26Z: "back-port to release/v1.1 as
1.1.1, open the issue". This is two of #850's work items.

tests.yml runs on `push: branches: [main, 'release/v*']`. The glob matches
release.yml's own `release/v<major>.<minor>`, since a branch filter's `*`
matches no `/`. The header that lists the triggers says so.

The concurrency group gains `github.ref` beside the commit. A group spans
the repository, and a push run's `github.sha` is the commit the push left
its branch at. Without the ref, a fast-forward of `release/v1.1` to main's
commit lands in the group `tests-push-<sha>` that main's push run holds,
and `cancel-in-progress: true` cancels that run if it is still going.
A pull request keeps one group, since its ref is its own.

release.yml changes in comments only. Step 1 of RELEASING counts a release
branch's push run, and says a fast-forwarded commit already has main's. The
dispatch stays for a commit no finished green run tested. RELEASING now
carries the release-branch and back-port procedure. With comment lines
removed, the file is byte-identical, and PyYAML and repo_shape.load_yaml
load the same value from both.

tests/test_workflow_concurrency.py holds the trigger and the matching
branch names. Two more rows of earlier spellings must be refused: the group
without the ref, and the ref in place of the commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR enables tests.yml to run on release/v<major>.<minor> pushes, separates same-commit runs by branch to avoid cross-branch cancellation during fast-forward back-ports, adds regression coverage for the trigger and concurrency rules, and documents the resulting release/back-port procedure in release.yml.

Sequence diagram for release branch back-port and test gating

sequenceDiagram
    participant Main
    participant ReleaseBranch as release/vX.Y
    participant Tests as tests.yml
    participant Release as release.yml

    Main->>Tests: push commit
    Tests-->>Main: tests run for commit
    Main->>ReleaseBranch: fast-forward or cherry-pick back-port
    ReleaseBranch->>Tests: push commit
    Tests-->>ReleaseBranch: tests run for commit
    Release->>Tests: verify finished green run for commit
    Release->>ReleaseBranch: tag patch release
Loading

Flow diagram for selecting a tested release commit

flowchart TD
    Commit[Release commit]
    PushRun{Finished green push run?}
    Dispatch[workflow_dispatch tests.yml]
    Tag[Tag patch release]
    Commit --> PushRun
    PushRun -->|yes| Tag
    PushRun -->|no| Dispatch
    Dispatch -->|green| Tag
Loading

File-Level Changes

Change Details Files
Run CI automatically for release-branch pushes and preserve independent validation runs across branches.
  • Expanded the push trigger to main and release/v*, while retaining pull-request and manual triggers.
  • Added github.ref to the workflow concurrency key alongside event and commit, preventing a release-branch fast-forward run from cancelling the corresponding main run.
  • Added tests for exact trigger configuration, GitHub-style branch matching, and rejected concurrency regressions.
.github/workflows/tests.yml
tests/test_workflow_concurrency.py
Document the release-branch testing gate and the approved back-port and patch-release procedure. .github/workflows/release.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T20:38:53Z) RV852's review of #852 (head 417951c8): READY, with no MUST. The four SHOULDs and five NITs below go into one fix round before #852 lands, after the v1.1.0 tag. The most important is SHOULD-1: a push runs the tests.yml its own commit holds. So release/v1.1, cut at v1.1.0, gets push runs only once it holds this change, and release/v1.0 never does.


RV852: review of PR #852 at head 417951c

Verdict: READY. There is no MUST item. The four SHOULDs make one short fix round, and the PR cannot land before the v1.1.0 tag anyway.

The head did not move during the review. Main moved twice (to 6f64eea, then 190133d), and the head merges cleanly with both. CI at 20:37Z: SonarCloud passed, package passed, tests-git-bash passed, and tests had been in progress since 20:12:33Z. I made no GitHub write: only gh view, diff, checks, list and run list, gh api GETs, and GETs to SonarCloud's public API.

The brief's questions

1. The concurrency group

2. Step 1's claim is true. The gate at release.yml:407-409 lists actions/runs?head_sha={sha} and keeps a run when:

  • its path is .github/workflows/tests.yml;
  • its head_sha is the commit;
  • its event is push or workflow_dispatch.

It has no branch filter, so main's push run counts for a tag on release/v1.1, and no dispatch is needed. I ran the gate's own script under a fake gh:

Case Gate result
main's run green, alone [0] accepted
main's run green, release/v1.1's run in progress [1] "tests is in_progress"
main's run green, release/v1.1's newer run failed [1] "newest run … ended failure"
both runs green [0] accepted

The last three rows lead to SHOULD-2.

3. release.yml changes only comments.

  • Bytes: with comment lines removed, origin/main's file and the head's are byte-identical (49356 and 49356 bytes, sha256 prefix f68b3b11aae74575 for both).
  • Parsers: yaml.safe_load gives the same result (PyYAML 6.0.3), and so does repo_shape.load_yaml.
  • Hunks: the two hunks are @@ -29,6 +29,9 @@ and @@ -56,0 +60,13 @@, both above on: (line 145), with no non-comment line changed.
  • tests.yml: only lines 29 and 57 change. The YAML diff is /on/push/branches and /concurrency/group.

4. The procedure text (release.yml:60-72)

5. The tests

  • Both the trigger and the group are exact-match pins, so any wrong edit to either fails.
  • The one plausible wrong edit that passes is M3 (SHOULD-4).
  • SonarCloud: the gate is OK and new duplication is 0.0%, but one S9073 is open (SHOULD-3).
  • The renamed test is referenced nowhere.

6. Interactions: actionlint 1.7.12 on all four workflows exits 0 with no output. The merges are clean (see Merges).

Other checks

  • Windows: the new test decodes bytes, and a CRLF copy of tests.yml loads to the same on and concurrency.
  • Docs: README.md, AGENTS.md, docs/cli.md and docs/handbook.html contain no mention of tests.yml, release/v or back-ports.
  • Pinned copies: no shape copy pinned by digest carries either workflow.

SHOULD

SHOULD-1: a push runs the tests.yml that the pushed commit holds.

SHOULD-2: step 1 does not say which run decides (release.yml:33-34, and "that commit's tests.yml run" at 71-72).

SHOULD-3: SonarCloud python:S9073 is open at tests/test_workflow_concurrency.py:178 (new code, "Split this composite assertion into separate assertions"). Line 156's composite assert comes from 024ea55 and is not new. Fix: split line 178 into two asserts:

    assert not set(pattern) & set("?+[]!"), pattern
    assert "**" not in pattern, pattern

SHOULD-4: mutant M3 survives (tests/test_workflow_concurrency.py:190 checks only set(on)).

  • The edit: pull_request: branches: [main] passes all 69 tests, yet it would leave every cherry-pick back-port PR to release/v<X.Y> without CI.
  • Fix: add assert on["pull_request"] is None, on["pull_request"]. I measured that load_yaml gives None there.

NIT

NIT-1: "names exactly" overstates the pattern (tests.yml:20-21, and the PR body). With the test's own matcher, release/v* also matches release/v, release/v1, release/v1.1.1 and release/vnext. Say "takes each of them" instead.

NIT-2: the fast-forward route has no caveat (release.yml:66-70). It is stated without #850's open ruleset question. Add: "unless a release/* ruleset requires a pull request (Brett Heap's word, #850); then every back-port is the cherry-pick."

NIT-3: two small layout and wording slips.

  • release.yml:59-60 has no # separator line before the new section, unlike the header's other sections.
  • tests.yml:35 still says "A pull request's group is its number", but the group is now the ref and the number.

NIT-4: no test pins the #850 text (mutants M6 and M7 survive). Add "# RELEASE BRANCHES AND BACK-PORTS (#850)" to test_the_header_says_how_to_release.

NIT-5: the PR body miscounts. It says the header test pins "five" phrases; it has six asserts.

Tests

Every run used a stub gh first on PATH that exits 97, GH_TOKEN=bogus and an empty GH_CONFIG_DIR, with pytest flags -q -p no:cacheprovider -o tmp_path_retention_policy=failed.

Files Result
tests/test_workflow_concurrency.py tests/test_release_workflow.py 69 passed in 3.60s
tests/test_repo_hygiene.py 77 passed in 1.98s
tests/test_install_sh.py 64 passed in 13.63s
tests/test_workflow_concurrency.py 13 passed in 0.08s
tests/test_shape_advisory.py::test_only_the_three_tools_read_the_advisory 1 passed in 1.41s
Emulated CI checkout of a push to release/v1.1 (local branch release/v1.1, no local main), 12 files that clone or read the checkout 519 passed in 231.64s
The same emulation: test_setup_sh, test_adopt_submodules, test_adopt_manifest_values, test_install_sh 399 passed in 380.65s
  • git diff --check fc867207 HEAD exits 0.
  • tests/test_shape_doctor.py was not run.
  • The emulation suggests the suite does not depend on the branch name, but it covers 16 files, not the whole suite.

Mutants

Mutant Result Killed by
M1: drop 'release/v*' 3 failed, 66 passed test_a_push_to_main_or_to_a_release_branch_runs_the_suite, test_which_branchs_push_runs_the_suite[release-v1.1] and [release-v1.0]
M2: main's old group 1 failed, 68 passed test_a_run_is_grouped_by_its_event_its_ref_and_its_commit
M3: pull_request: branches: [main] 69 passed survives (SHOULD-4)
M4: 'release/**' 5 failed, 64 passed the trigger test and four branch rows
M5: the ref in place of the event 1 failed, 68 passed the group test
M6: release.yml set back to main's 146 passed survives (NIT-4)
M7: tests.yml's two #850 comment blocks removed 146 passed survives (NIT-4)
M8: push on tags v* too 1 failed, 145 passed the trigger test

Merges

  • With origin/main 6f64eea: clean, tree 227870b4bb688707a343a9c30cff48a8112d976d.
  • With origin/main 190133d (main moved during the review): clean, tree 1199bcc9626a0f689a3dea98e8d819f6615e8d94. The diff against main is the PR's three files only, 157 insertions and 46 deletions.
  • Of the 53 open PRs, none other touches these three files or anything under .github/, so there was no other merge to run.

Lane: openreposhape-3 (openRepoShape-3)

…olds, so a release branch runs the suite only once it holds this change, and after a fast-forward the branch's own push run decides; the trigger, the header and the pull_request line are pinned (RV852, #850)

The fix round on RV852's review of #852 (issuecomment-6240653347). It
changes release.yml in comments only: with comment lines removed it is
byte-identical to origin/main's, and PyYAML and repo_shape.load_yaml load
the same value from both.

SHOULD-1. GitHub runs the version of a workflow that the pushed commit
holds. tests.yml's header, release.yml's procedure and the test docstring
now say a release branch runs the suite only once it holds this change:
`release/v1.1` from its first fast-forward past it, or a cherry-pick of
it; `release/v1.0` never has, and still needs a dispatch.

SHOULD-2. Step 1 now says that once the branch holds #850's tests.yml, a
fast-forward also starts the branch's own push run on that commit, and
that newer run decides: tag once it has finished green. The procedure's
last bullet names step 1 in place of "that commit's tests.yml run".

SHOULD-3. The composite assert in `branch_filter_matches` is two asserts.

SHOULD-4. `on["pull_request"] is None` is pinned, so a branch filter on
pull_request, which would leave a cherry-pick back-port's pull request to
`release/v<X.Y>` with no run, fails.

NIT-1. "names exactly" is "takes each of them": `release/v*` also matches
`release/v`, `release/v1` and `release/vnext`.

NIT-2. The fast-forward route carries the caveat: it stands unless a
`release/*` ruleset requires a pull request, which waits on Brett Heap's
word in #850; then every back-port is the cherry-pick.

NIT-3. A `#` separator line sits before the release-branch section, and
the group sentence in tests.yml and the test's GROUP comment say a pull
request's group is its ref and its number.

NIT-4. test_the_header_says_how_to_release pins the #850 heading and the
two new sentences in release.yml; a new test pins the two #850 blocks and
the push rule in tests.yml.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brettheap

Copy link
Copy Markdown
Contributor Author

(lane openRepoShape-3, 2026-10-17T21:48:22Z) RV852's re-check of #852's fix round (417951c8..900ffe70): READY. Every SHOULD and NIT is taken, and CI and SonarCloud are green on 900ffe70. Three optional NITs are left. The title is changed to RV852's proposal.


RV852 re-check: PR #852 at head 900ffe7

Verdict: READY. F852's commit takes all four SHOULDs and all five NITs, and the new text is true. Every requested mutant is now killed. There is no MUST and no SHOULD left, only three optional NITs.

The head did not move during the re-check. Main is now 9f235da, one landing past 190133d. The commit's identity is Brett Heap service@opensoft.dev, and it ends with two Co-Authored-By lines, Sonnet's then Opus's. CI at 900ffe7 is all green: tests passed (52m47s), as did tests-git-bash, package and SonarCloud. SonarCloud shows 0 open issues, its gate OK and 0.0% new duplication. The S9073 is gone. I made no GitHub write.

1. Each item, and whether the new text holds

  • SHOULD-1 is taken. The rule now appears in tests.yml:22-25, release.yml:77-80 and the test docstrings at lines 23-27 and 211-219. Checked again:

    • GitHub's docs say "Each workflow run will use the version of the workflow that is present in the associated commit SHA or Git ref of the event."
    • origin/release/v1.0 (8cda139) still says push: branches: [main].
    • The PR body now says the v1.0.1 sentence "is still true", which is correct.
  • SHOULD-2 is taken. Step 1 (release.yml:35-37) and the last bullet (81-83, "its tests.yml runs have all finished and the newest is green") now say which run decides. I ran the gate's own script from release.yml at 900ffe7 under a fake gh:

    Case Gate result
    main's run green, alone [0] accepted
    release/v1.1's run still in progress [1] "in_progress"
    release/v1.1's run finished later and failed [1] refused
    release/v1.1's run finished later and green [0] accepted

    The gate still filters only on head_sha, path and event (release.yml:418-420), with no branch filter. See NIT-A for one ordering where step 1's wording is loose.

  • SHOULD-3 is taken: the composite assert is now two asserts (tests/test_workflow_concurrency.py:183-184), and Sonar shows 0 issues.

  • SHOULD-4 is taken: assert on["pull_request"] is None is at line 199.

  • NIT-1 to NIT-5 are taken:

    • NIT-1: tests.yml:20-21 says "takes each of them".
    • NIT-2: the ruleset caveat is at release.yml:74-76 and correctly says the ruleset "waits on" Brett's word.
    • NIT-3: the # separator is at release.yml:63, and tests.yml:39 and the test file's line 51 say a pull request's group is its ref and its number.
    • NIT-4: test_release_workflow.py:597-601 pins the new release.yml text, and test_the_header_says_why_a_release_branch_runs_it_and_when (line 225) pins the tests.yml blocks.
    • NIT-5: the body now says nine phrases.
  • Line numbers: every one the PR body cites matches the file.

2. release.yml is still comments only (against origin/main 190133d)

  • Bytes: with comment lines removed, the two files are byte-identical (49356 and 49356 bytes, sha256 prefix f68b3b11aae74575 for both). The raw files are 67363 and 69101 bytes.
  • Parsers: yaml.safe_load gives the same result (PyYAML 6.0.3), and so does repo_shape.load_yaml.
  • Hunks: @@ -29,6 +29,12 @@ and @@ -56,0 +63,21 @@, both above on: (line 156), with no non-comment line changed.
  • 9f235da: it changes neither workflow.
  • tests.yml: the only YAML changes are still /on/push/branches and /concurrency/group.
  • Other checks: no non-ASCII added, and git diff --check 190133d5 900ffe70 exits 0.

3. Mutants at 900ffe7

Each run covered the two workflow test files plus test_repo_hygiene.py, and each file was restored with git checkout afterwards.

Mutant Result Killed by
M1: drop 'release/v*' 3 failed, 144 passed the trigger test, [release-v1.1], [release-v1.0]
M2: main's old group 1 failed, 146 passed the group test
M3: pull_request: branches: [main] 1 failed, 146 passed test_a_push_to_main_or_to_a_release_branch_runs_the_suite
M4: 'release/**' 5 failed, 142 passed the trigger test and four branch rows
M5: the ref in place of the event 1 failed, 146 passed the group test
M6: release.yml set back to main's 1 failed, 146 passed test_the_header_says_how_to_release
M7: tests.yml's two #850 blocks removed 1 failed, 146 passed test_the_header_says_why_a_release_branch_runs_it_and_when
M8: push on tags v* too 1 failed, 146 passed the trigger test
M9 (new): pull_request: types: [opened] 1 failed, 146 passed the trigger test
M10 (new): the ruleset caveat removed 147 passed survives
M11 (new): the tag bullet set back to "once that commit's tests.yml run is green" 147 passed survives

M10 and M11 are comment wording only. Pinning them is optional and not required.

4. Proposed title

Use this exactly:

tests.yml and release.yml: a push to a release/v<major>.<minor> branch that holds this change runs the suite as a push to main does, so a patch's commit there gets its push run with no dispatch, a fast-forward of that branch to main's commit no longer cancels main's run on it, and the release header says how a fix is back-ported (#850)

5. Merges (main fetched fresh, now 9f235da)

  • git merge-tree --write-tree 900ffe70 origin/main is clean and gives tree 2a40aa1d69ad38bf7506457fa66d94e7a3389975.
  • The diff against main is the PR's 4 files only, 210 insertions and 46 deletions.
  • I also ran the targeted tests on a local merge commit, 0d30c8b2 (never pushed), because 9f235da changes repo_shape.py, which the tests import (results below).
  • Of 55 open PRs, none other touches the 4 files or anything under .github/, so there was no other merge to run.

6. Tests

Every run used the stub gh (exits 97), GH_TOKEN=bogus and an empty GH_CONFIG_DIR.

Files At 900ffe7 On the merge with main
tests/test_workflow_concurrency.py tests/test_release_workflow.py 70 passed in 3.80s 70 passed in 4.83s
tests/test_repo_hygiene.py 77 passed in 2.53s 78 passed in 2.00s (main added a test)
tests/test_install_sh.py 64 passed in 15.12s 64 passed in 16.50s
tests/test_shape_advisory.py::test_only_the_three_tools_read_the_advisory 1 passed in 1.09s not run
  • actionlint 1.7.12 on the four workflows exits 0 with no output.
  • tests/test_shape_doctor.py was not run.
  • On Windows, the new pins are one-line phrases, read with read_bytes().decode() or read_text(), so they hold under CRLF.

NIT (optional; none blocks landing)

Lane: openreposhape-3 (openRepoShape-3)

@brettheap brettheap changed the title tests.yml and release.yml: a push to a release/v<major>.<minor> branch runs the suite as a push to main does, so each commit a release branch takes carries the push run a patch's tag needs, a fast-forward of that branch to main's commit no longer cancels main's run on it, and the release header says how a fix is back-ported (#850) tests.yml and release.yml: a push to a release/v<major>.<minor> branch that holds this change runs the suite as a push to main does, so a patch's commit there gets its push run with no dispatch, a fast-forward of that branch to main's commit no longer cancels main's run on it, and the release header says how a fix is back-ported (#850) Oct 17, 2026
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 17, 2026 22:30
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 1 hour and 52 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

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.

1 participant