From 417951c82faa173f78ab595bb97d1cc804226525 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sat, 17 Oct 2026 20:10:50 +0000 Subject: [PATCH 1/2] tests.yml and release.yml: a push to a `release/v.` 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) 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.`, 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-` 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 --- .github/workflows/release.yml | 28 +++++-- .github/workflows/tests.yml | 57 +++++++++----- tests/test_workflow_concurrency.py | 118 ++++++++++++++++++++++++----- 3 files changed, 157 insertions(+), 46 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0bdedd7..84dd1674 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,15 @@ name: release # Nothing is uploaded before them: the build job refuses every dispatch # until both environments exist and are limited so. Then, for each release: # 1. The release commit passes `tests` ON ITSELF, in a push or dispatch -# run of tests.yml. On main, the push run that landed it does that -# when that run finished green; one that failed does not, nor one -# that a later landing cancelled (each landing cancelled the one -# before until #593). Otherwise, and always on a `release/v` -# branch, which no push run tests: BEFORE TAGGING, dispatch tests.yml -# on a branch that points at the release commit, and wait for it: +# run of tests.yml. The push run that landed it on main, or that its +# push to a `release/v` branch started (tests.yml runs on both +# since #850), does that when that run finished green; one that failed +# does not, nor one that a later landing cancelled (each landing +# cancelled the one before until #593). A commit that came to a +# release branch by fast-forward already has main's push run. +# Otherwise, for a commit that no finished green run tested: BEFORE +# TAGGING, dispatch tests.yml on a branch that points at the release +# commit, and wait for it: # git push origin :refs/heads/ # gh workflow run tests.yml --ref -R opensoft/openRepoShape # A `release/v` branch serves while it is still at that commit. @@ -54,6 +57,19 @@ name: release # PyPI lacks. # A version is uploaded once. A bad upload is fixed by the next patch, never # by moving a tag (decision 4). +# RELEASE BRANCHES AND BACK-PORTS (#850), on Brett Heap's ruling at +# 2026-10-17T19:52:26Z, "back-port to release/v1.1 as 1.1.1, open the issue": +# - main stays the trunk, and every change lands there first; a release +# is its annotated tag, never a branch; +# - `release/v` is cut at the tag `v.0`, and each patch +# `v.` is tagged on it; +# - a fix for a patch lands on main first, through its own review, CI and +# gate, and is then back-ported to `release/v`: by a fast-forward +# to main while main holds only the patch's work, and otherwise by a +# pull request to `release/v` of `git cherry-pick -x `, +# the squash commit main landed; +# - the patch is tagged on `release/v` once that commit's tests.yml +# run is green (step 1), then released by steps 2 to 5. # # DISPATCH ONLY. Nothing here runs on a push, a tag or a pull request: an # upload is an act Brett Heap approves. The version is the tag's (decision diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e97dc97f..b50c66e5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,38 +4,57 @@ name: tests # clones them over the filesystem. It never calls `gh`, never touches the # network, and must never create a real repository. # -# The FULL suite runs on LINUX ONLY, on every trigger: a push to `main`, a pull -# request and a manual run all use the one ubuntu job below. Brett Heap decided -# on 2026-10-15, for the whole estate, to drop the macOS and Windows test jobs. +# The FULL suite runs on LINUX ONLY, on every trigger: a push to `main` or to a +# release branch (`release/v.`), a pull request and a manual run +# all use the one ubuntu job below. Brett Heap decided on 2026-10-15, for the +# whole estate, to drop the macOS and Windows test jobs. # One exception, on Brett Heap's word at 2026-10-15T18:20:35Z, # "Option 1: keep a small Git Bash job for #596": the `tests-git-bash` job # below runs the launcher's and the installer's suites, and nothing else, on # Windows under Git Bash. # +# A RELEASE BRANCH'S PUSHES RUN IT TOO (#850). A patch is tagged on its +# `release/v.` branch, and `release.yml` releases a commit only +# once a push or dispatch run of this workflow passed `tests` on it, so every +# commit a release branch takes gets that run from its own push, with no +# dispatch by hand. `release/v*` names exactly those branches: in a branch +# filter `*` matches no `/`. +# # The `package` job at the end runs no pytest. It builds the openreposhape # wheel and installs it with pipx, so what it checks needs PyPI (the build # backend, git-filter-repo; #563). The other jobs take only test tools there. on: push: - branches: [main] + branches: [main, 'release/v*'] pull_request: workflow_dispatch: # A RUN IS CANCELLED ONLY BY A NEWER RUN IN ITS OWN GROUP: a newer push to the -# same pull request, or a second dispatch on the same commit. A pull request's -# group is its number. A `push` run on main has no number, and the old fallback -# `github.ref` put every one of them in the one group `tests-refs/heads/main`, -# so each landing cancelled the run on the main before it: on 2026-10-15 every -# push run from #561 (7d9bec9) to #580 (dd3f23a) ended `cancelled` and nine PRs -# landed with no Linux run over their combination (#592). `github.sha` gives a -# push run a group of its own commit, so no later landing cancels it and each -# landing keeps its own Linux result. `github.event_name` keeps a push run and -# a dispatch apart, so a dispatch on main, which runs on whatever commit main -# is at, never cancels that commit's push run; a second dispatch on the same -# commit still replaces the first. +# same pull request, or a second dispatch of the same ref at the same commit. +# A pull request's group is its number. A `push` run on main has no number, +# and the old fallback `github.ref` put every one of them in the one group +# `tests-refs/heads/main`, so each landing cancelled the run on the main +# before it: on 2026-10-15 every push run from #561 (7d9bec9) to #580 +# (dd3f23a) ended `cancelled` and nine PRs landed with no Linux run over their +# combination (#592). `github.sha` gives a push run a group of its own commit, +# so no later landing cancels it and each landing keeps its own Linux result. +# `github.event_name` keeps a push run and a dispatch apart, so a dispatch on +# main, which runs on whatever commit main is at, never cancels that commit's +# push run; a second dispatch of the same ref at the same commit still +# replaces the first. +# +# `github.ref` KEEPS TWO BRANCHES APART AT ONE COMMIT (#850). A group spans +# the repository, not a branch, 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 shares the group of main's push run on that +# commit and cancels it while it is still running, as #592's landings did. +# The ref sits beside the commit, never in its place as #592's fallback did. +# A pull request's ref, `refs/pull//merge`, is its own, so a pull +# request still has one group, and a newer push to it still cancels the older +# run. concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.sha }} + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true jobs: @@ -158,9 +177,9 @@ jobs: # Git's real `curl` ahead of the suites' fake. conftest's comment there # has the rest. # - # ON EVERY TRIGGER, a push to `main` included: a squash lands a tree no - # pull-request run tested (#122), and these two suites are the only run - # of the one-line install on Windows. + # ON EVERY TRIGGER, a push to `main` or to a release branch included: a + # squash lands a tree no pull-request run tested (#122), and these two + # suites are the only run of the one-line install on Windows. tests-git-bash: runs-on: windows-latest timeout-minutes: 15 diff --git a/tests/test_workflow_concurrency.py b/tests/test_workflow_concurrency.py index 78f2c4c8..10c5a44c 100644 --- a/tests/test_workflow_concurrency.py +++ b/tests/test_workflow_concurrency.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -"""`.github/workflows/tests.yml` never lets one landing cancel another's run. +"""`.github/workflows/tests.yml` runs on each push to main and to a release +branch, and never lets one landing cancel another's run. THE FAILURE THIS GUARDS IS SILENT. A `push` run on main has no pull request number, so a group that fell back to `github.ref` landed on `refs/heads/main` @@ -11,15 +12,25 @@ main's push run, about 30 minutes a landing. The fix groups a pull-request run by its number (a new push to the pull -request still cancels its older run) and every other run by its event and its -own commit. The event is part of the group because a `workflow_dispatch` run -starts on whatever commit main is at, which is also the commit of that commit's -push run: grouped by commit alone, the dispatch would cancel the push run it -sits beside (RV593 SHOULD-1). - -These tests read the workflow as TEXT, like the rest of this suite, and never -touch the network or `gh`. `actionlint` in the workflow itself checks the -expression's syntax; what it cannot check is which fallback was chosen. +request still cancels its older run) and every other run by its event, its +ref and its own commit. The event is part of the group because a +`workflow_dispatch` run starts on whatever commit main is at, which is also the +commit of that commit's push run: grouped by commit alone, the dispatch would +cancel the push run it sits beside (RV593 SHOULD-1). + +#850 ADDS THE RELEASE BRANCHES. A push to `release/v.` runs the +suite as a push to main does, so each commit a release branch takes has the +push run `release.yml` requires of a tagged commit, with no dispatch by hand. +The ref joined the group with it: a group spans the repository, and a +fast-forward of a release branch to main's commit pushes the very commit main's +push run is testing, so grouped by event and commit alone the release branch's +run would cancel main's. + +These tests read the concurrency block as TEXT, like the rest of this suite, +and never touch the network or `gh`. `actionlint` in the workflow itself checks +the expression's syntax; what it cannot check is which fallback was chosen. +The trigger is read with `repo_shape.load_yaml`, the fail-closed reader +`tests/test_release_workflow.py` reads both workflows with. """ from __future__ import annotations @@ -29,20 +40,25 @@ import pytest from conftest import REPO +from repo_shape import load_yaml WORKFLOW = REPO / ".github" / "workflows" / "tests.yml" #: A pull request is grouped by its number. Anything else is grouped by its -#: event and the commit it runs on, so a push run and a dispatch on the same -#: commit stay in two groups and neither cancels the other. -GROUP = ("${{ github.workflow }}-${{ github.event_name }}-" +#: event, its ref and the commit it runs on, so a push run and a dispatch on +#: the same commit stay in two groups, and so do the push runs of main and of +#: a release branch fast-forwarded to main's commit; neither cancels the other. +GROUP = ("${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-" "${{ github.event.pull_request.number || github.sha }}") #: Spellings the workflow once had, or could be given again, each with a #: phrase its refusal must carry. `github.ref` is what #592 fixed: every push #: run on main in one group. The `github.sha` one without the event is this #: file's own first fix, which stops a landing cancelling the last landing's -#: run but lets a dispatch cancel its commit's push run (RV593 SHOULD-1). +#: run but lets a dispatch cancel its commit's push run (RV593 SHOULD-1). The +#: one without the ref is the one #592 closed with, which lets a release +#: branch's push cancel main's run on the same commit (#850). The ref in place +#: of the commit would put every push run on main in one group again. EARLIER_GROUPS = { "ref": ("${{ github.workflow }}-" "${{ github.event.pull_request.number || github.ref }}", @@ -51,8 +67,21 @@ "${{ github.workflow }}-" "${{ github.event.pull_request.number || github.sha }}", "github.event_name"), + "sha-without-the-ref": ( + "${{ github.workflow }}-${{ github.event_name }}-" + "${{ github.event.pull_request.number || github.sha }}", + "fast-forwarded"), + "ref-without-the-commit": ( + "${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}", + "github.sha"), } +#: The branches whose pushes run the suite (#850): `main`, the trunk every +#: change lands on first, and each release branch, `release/v.`, +#: where a patch is tagged. `release.yml` releases a commit only once a push or +#: dispatch run of this workflow passed `tests` on it. +PUSH_BRANCHES = ["main", "release/v*"] + def workflow_lines() -> list[str]: """The workflow's lines, comments dropped, whatever the checkout's EOL.""" @@ -83,24 +112,35 @@ def group_problem(group: str | None) -> str | None: """Why `group` is not the expression this workflow must carry, or None.""" if group == GROUP: return None - if group is not None and "github.ref" in group: + if group is not None and re.search(r"\|\|\s*github\.ref\b", group): return ("it falls back to `github.ref`, which is `refs/heads/main` " "for every push run on main, so the next landing cancels the " "last one's run (#592)") + if group is not None and "github.sha" not in group: + return ("it does not name `github.sha`, so every push run on one " + "branch shares a group and the next landing cancels the last " + "one's run (#592)") if group is not None and "github.event_name" not in group: return ("it does not name `github.event_name`, so a dispatch on a " "commit shares a group with that commit's push run and " "cancels it (RV593 SHOULD-1)") + if group is not None and "github.ref" not in group: + return ("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 (#850)") return f"it is not {GROUP!r}" -def test_a_run_is_grouped_by_its_event_and_its_commit(): - """The group's fallback is `github.sha` and it names the event (#592). +def test_a_run_is_grouped_by_its_event_its_ref_and_its_commit(): + """The group's fallback is `github.sha` (#592), and it names the event + and the ref beside it (#850). - `github.ref` is `refs/heads/main` for every push run on main, so it puts - every landing in one group and the next landing cancels the last one. The - commit alone is not enough either: a dispatch runs on the commit main is - at, the same commit as its push run, and would cancel it. + `github.ref` is `refs/heads/main` for every push run on main, so in place + of the commit it puts every landing in one group and the next landing + cancels the last one. The commit alone is not enough either: a dispatch + runs on the commit main is at, the same commit as its push run, and would + cancel it; and a release branch fast-forwarded to main's commit pushes + that same commit, and its run would cancel main's. """ group = top_level_concurrency().get("group") problem = group_problem(group) @@ -129,3 +169,39 @@ def test_no_job_carries_a_concurrency_of_its_own(): assert nested == [], ( "a job in tests.yml sets `concurrency:` itself, which the group " f"checked above does not govern: {nested}") + + +def branch_filter_matches(pattern: str, branch: str) -> bool: + """GitHub's branch filter for the one wildcard this workflow uses: `*` + matches any characters but `/`. A pattern with any other special + character is refused here rather than guessed at.""" + assert not set(pattern) & set("?+[]!") and "**" not in pattern, pattern + return re.fullmatch("[^/]*".join(map(re.escape, pattern.split("*"))), + branch) is not None + + +def test_a_push_to_main_or_to_a_release_branch_runs_the_suite(): + """#850: the push trigger names `main` and `release/v*`, so a commit a + release branch takes gets the push run `release.yml`'s gate requires of + the tagged commit; before, each had to be dispatched by hand. A pull + request and a manual run still run it.""" + on = load_yaml(WORKFLOW)["on"] + assert on["push"] == {"branches": PUSH_BRANCHES}, on["push"] + assert set(on) == {"push", "pull_request", "workflow_dispatch"}, set(on) + + +@pytest.mark.parametrize("branch, runs", [ + ("main", True), + ("release/v1.1", True), + ("release/v1.0", True), + ("l3-850-release-branches", False), + ("docs/release-notes-v1.0.0", False), +], ids=["main", "release-v1.1", "release-v1.0", "a-pull-requests-branch", + "a-branch-named-for-a-release"]) +def test_which_branchs_push_runs_the_suite(branch, runs): + """`release/v1.1`, where v1.1.1 is tagged, and `release/v1.0`, where + v1.0.1 was, run on a push as main does; a pull request's own branch runs + in its pull request instead, whatever its name says.""" + patterns = load_yaml(WORKFLOW)["on"]["push"]["branches"] + assert any(branch_filter_matches(pattern, branch) + for pattern in patterns) is runs, (branch, patterns) From 900ffe70b6b340b52d00b9ee68b2830f546d6eee Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Sat, 17 Oct 2026 20:44:53 +0000 Subject: [PATCH 2/2] tests.yml and release.yml: a push runs the tests.yml its own commit holds, 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` 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 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 27 +++++++++++----- .github/workflows/tests.yml | 18 +++++++---- tests/test_release_workflow.py | 8 +++++ tests/test_workflow_concurrency.py | 51 +++++++++++++++++++++++------- 4 files changed, 78 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84dd1674..22d09b35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,11 +27,14 @@ name: release # until both environments exist and are limited so. Then, for each release: # 1. The release commit passes `tests` ON ITSELF, in a push or dispatch # run of tests.yml. The push run that landed it on main, or that its -# push to a `release/v` branch started (tests.yml runs on both -# since #850), does that when that run finished green; one that failed -# does not, nor one that a later landing cancelled (each landing -# cancelled the one before until #593). A commit that came to a -# release branch by fast-forward already has main's push run. +# push to a `release/v` branch started (once the branch holds +# #850's tests.yml, which runs on both), does that when that run +# finished green; one that failed does not, nor one that a later +# landing cancelled (each landing cancelled the one before until +# #593). A commit that came to a release branch by fast-forward +# already has main's push run. 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. # Otherwise, for a commit that no finished green run tested: BEFORE # TAGGING, dispatch tests.yml on a branch that points at the release # commit, and wait for it: @@ -57,6 +60,7 @@ name: release # PyPI lacks. # A version is uploaded once. A bad upload is fixed by the next patch, never # by moving a tag (decision 4). +# # RELEASE BRANCHES AND BACK-PORTS (#850), on Brett Heap's ruling at # 2026-10-17T19:52:26Z, "back-port to release/v1.1 as 1.1.1, open the issue": # - main stays the trunk, and every change lands there first; a release @@ -67,9 +71,16 @@ name: release # gate, and is then back-ported to `release/v`: by a fast-forward # to main while main holds only the patch's work, and otherwise by a # pull request to `release/v` of `git cherry-pick -x `, -# the squash commit main landed; -# - the patch is tagged on `release/v` once that commit's tests.yml -# run is green (step 1), then released by steps 2 to 5. +# the squash commit main landed. 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; +# - a push runs the tests.yml its own commit holds, so a release branch +# runs it only once it holds #850's 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 (step 1); +# - the patch is tagged on `release/v` once step 1 holds for that +# commit (its tests.yml runs have all finished and the newest is green), +# then released by steps 2 to 5. # # DISPATCH ONLY. Nothing here runs on a push, a tag or a pull request: an # upload is an act Brett Heap approves. The version is the tag's (decision diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b50c66e5..9c090853 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,10 +15,14 @@ name: tests # # A RELEASE BRANCH'S PUSHES RUN IT TOO (#850). A patch is tagged on its # `release/v.` branch, and `release.yml` releases a commit only -# once a push or dispatch run of this workflow passed `tests` on it, so every +# once a push or dispatch run of this workflow passed `tests` on it, so a # commit a release branch takes gets that run from its own push, with no -# dispatch by hand. `release/v*` names exactly those branches: in a branch -# filter `*` matches no `/`. +# dispatch by hand. `release/v*` takes each of them, and any other name of one +# segment that starts `release/v`: in a branch filter `*` matches no `/`. +# A push runs the tests.yml its own commit holds, so a release branch runs it +# 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. # # The `package` job at the end runs no pytest. It builds the openreposhape # wheel and installs it with pipx, so what it checks needs PyPI (the build @@ -32,10 +36,10 @@ on: # A RUN IS CANCELLED ONLY BY A NEWER RUN IN ITS OWN GROUP: a newer push to the # same pull request, or a second dispatch of the same ref at the same commit. -# A pull request's group is its number. A `push` run on main has no number, -# and the old fallback `github.ref` put every one of them in the one group -# `tests-refs/heads/main`, so each landing cancelled the run on the main -# before it: on 2026-10-15 every push run from #561 (7d9bec9) to #580 +# A pull request's group is its ref and its number. A `push` run on main has +# no number, and the old fallback `github.ref` put every one of them in the +# one group `tests-refs/heads/main`, so each landing cancelled the run on the +# main before it: on 2026-10-15 every push run from #561 (7d9bec9) to #580 # (dd3f23a) ended `cancelled` and nine PRs landed with no Linux run over their # combination (#592). `github.sha` gives a push run a group of its own commit, # so no later landing cancels it and each landing keeps its own Linux result. diff --git a/tests/test_release_workflow.py b/tests/test_release_workflow.py index 845a4d81..99fb2909 100644 --- a/tests/test_release_workflow.py +++ b/tests/test_release_workflow.py @@ -591,6 +591,14 @@ def test_the_header_says_how_to_release(workflow): assert "# ACCEPTED LIMITS, carried over from openDox-code's release.yml" \ in text assert "lane\n# sessions act as brettheap" in text + # #850's section, and the two sentences RV852 asked it to carry: a push + # runs the tests.yml its own commit holds, and the branch's newer push run + # decides after a fast-forward. + assert "# RELEASE BRANCHES AND BACK-PORTS (#850)" in text + assert "# - a push runs the tests.yml its own commit holds, so a release" \ + in text + assert "and that newer run decides. Tag once it has finished green." \ + in text # --- RV599, SHOULD 2: the commit is on main or its release branch ------------------- diff --git a/tests/test_workflow_concurrency.py b/tests/test_workflow_concurrency.py index 10c5a44c..edb28ed8 100644 --- a/tests/test_workflow_concurrency.py +++ b/tests/test_workflow_concurrency.py @@ -21,6 +21,10 @@ #850 ADDS THE RELEASE BRANCHES. A push to `release/v.` runs the suite as a push to main does, so each commit a release branch takes has the push run `release.yml` requires of a tagged commit, with no dispatch by hand. +That holds once the branch holds this change, not before: a push runs the +tests.yml its own commit holds, so `release/v1.1` runs the suite from its first +fast-forward past this change, or a cherry-pick of it, and `release/v1.0`, +whose tests.yml still says `branches: [main]`, never has and needs a dispatch. The ref joined the group with it: a group spans the repository, and a fast-forward of a release branch to main's commit pushes the very commit main's push run is testing, so grouped by event and commit alone the release branch's @@ -44,10 +48,11 @@ WORKFLOW = REPO / ".github" / "workflows" / "tests.yml" -#: A pull request is grouped by its number. Anything else is grouped by its -#: event, its ref and the commit it runs on, so a push run and a dispatch on -#: the same commit stay in two groups, and so do the push runs of main and of -#: a release branch fast-forwarded to main's commit; neither cancels the other. +#: A pull request is grouped by its ref and its number. Anything else is +#: grouped by its event, its ref and the commit it runs on, so a push run and +#: a dispatch on the same commit stay in two groups, and so do the push runs +#: of main and of a release branch fast-forwarded to main's commit; neither +#: cancels the other. GROUP = ("${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-" "${{ github.event.pull_request.number || github.sha }}") @@ -175,19 +180,23 @@ def branch_filter_matches(pattern: str, branch: str) -> bool: """GitHub's branch filter for the one wildcard this workflow uses: `*` matches any characters but `/`. A pattern with any other special character is refused here rather than guessed at.""" - assert not set(pattern) & set("?+[]!") and "**" not in pattern, pattern + assert not set(pattern) & set("?+[]!"), pattern + assert "**" not in pattern, pattern return re.fullmatch("[^/]*".join(map(re.escape, pattern.split("*"))), branch) is not None def test_a_push_to_main_or_to_a_release_branch_runs_the_suite(): """#850: the push trigger names `main` and `release/v*`, so a commit a - release branch takes gets the push run `release.yml`'s gate requires of - the tagged commit; before, each had to be dispatched by hand. A pull - request and a manual run still run it.""" + release branch takes, once the branch holds this change, gets the push + run `release.yml`'s gate requires of the tagged commit; before, each had + to be dispatched by hand. A pull request and a manual run still run it, + and `pull_request` takes no branch filter, so the pull request of a + cherry-pick back-port to `release/v` gets its run too.""" on = load_yaml(WORKFLOW)["on"] assert on["push"] == {"branches": PUSH_BRANCHES}, on["push"] assert set(on) == {"push", "pull_request", "workflow_dispatch"}, set(on) + assert on["pull_request"] is None, on["pull_request"] @pytest.mark.parametrize("branch, runs", [ @@ -199,9 +208,29 @@ def test_a_push_to_main_or_to_a_release_branch_runs_the_suite(): ], ids=["main", "release-v1.1", "release-v1.0", "a-pull-requests-branch", "a-branch-named-for-a-release"]) def test_which_branchs_push_runs_the_suite(branch, runs): - """`release/v1.1`, where v1.1.1 is tagged, and `release/v1.0`, where - v1.0.1 was, run on a push as main does; a pull request's own branch runs - in its pull request instead, whatever its name says.""" + """The push patterns take main, `release/v1.1`, where v1.1.1 is tagged, + and `release/v1.0`, where v1.0.1 was; a pull request's own branch runs + in its pull request instead, whatever its name says. + + These rows test only the pattern. A push runs the tests.yml its own + commit holds, so a branch whose tests.yml lacks this change runs nothing + on a push, whatever its name: `release/v1.0`'s still says + `branches: [main]`, and `release/v1.1`, cut at v1.1.0, runs the suite only + from its first fast-forward past this change, or a cherry-pick of it.""" patterns = load_yaml(WORKFLOW)["on"]["push"]["branches"] assert any(branch_filter_matches(pattern, branch) for pattern in patterns) is runs, (branch, patterns) + + +def test_the_header_says_why_a_release_branch_runs_it_and_when(): + """#850's two comment blocks stay in tests.yml's header, with the rule that + bounds the trigger: a push runs the tests.yml its own commit holds, so a + release branch runs the suite only once it holds this change. A header + that loses them leaves the trigger and the group as two edits with no + reason to be kept. Each phrase sits on one line, whatever the EOL.""" + text = WORKFLOW.read_bytes().decode("utf-8") + for phrase in ( + "# A RELEASE BRANCH'S PUSHES RUN IT TOO (#850)", + "# A push runs the tests.yml its own commit holds, so a release", + "# `github.ref` KEEPS TWO BRANCHES APART AT ONE COMMIT (#850)"): + assert phrase in text, phrase