feat: add merge queue versioning workflows - #122
Open
dblane-digicatapult wants to merge 7 commits into
Open
dblane-digicatapult wants to merge 7 commits into
dblane-digicatapult wants to merge 7 commits into
Conversation
Repos on a GitHub merge queue cannot use the existing version gate. That gate requires an absolute version number on the pull request branch, so two approved PRs necessarily compute the same next version and the second to reach the queue fails check-version and is evicted. Recovering it means bumping again, which dismisses its approval and drops it from the queue, which is the loop the merge queue was meant to remove. Add an alternative model where the PR carries only the intent to bump and the number is computed on trunk after merge: - require-version-label.yml gates a PR on carrying exactly one v: label. It is order independent, so every queued PR satisfies it at once. No permissions needed, labels come from the event payload. - apply-version-npm.yml / apply-version-poetry.yml compute and commit the version on trunk after merge, using the same arithmetic as the existing synchronise-*-version workflows. The version commit is pushed with the GitHub App token rather than GITHUB_TOKEN. build-docker, generate-sbom and release-github all read the version from the checked out tree, so the released commit must be the one carrying the new version. GITHUB_TOKEN pushes do not trigger workflows, so the version commit would never be built. A merge therefore produces two runs on trunk: one applies the version, the second releases it. The commit prefix guard keeps those two from triggering each other. A merge queue can land several PRs in one push, so the bump is resolved across every commit in the push and the strongest label wins. Also fix two workflows that silently misbehave under merge_group: - migration-checks-npm/poetry resolved no base sha on merge_group, so the immutability lint, ordering lint and seeded-upgrade all no-opped for every queued PR. - tests-npm posted its coverage comment unconditionally, which has no PR to attach to on merge_group and would stall the queue when coverage is required. The existing synchronise-*-version workflows are untouched, so repos not adopting a merge queue are unaffected.
This was referenced Aug 1, 2026
gh pr list --state merged --limit 1 was wrong in two independent ways. It orders by created date rather than merged date, so a long-lived PR merged today loses to a newer one merged last week. And it credits only one PR, so any release containing several silently attributes the whole release to one arbitrary change. That is every batched merge queue merge, and any release covering more than one push. Resolve the PRs from the commits since the previous release tag instead. This is also what makes release notes correct under merge queue versioning, where the released commit is a bot version commit with no PR of its own. Requires full history, so checkout now uses fetch-depth: 0. The floating latest tag is excluded from tag detection or the range would always be empty. Notes are written to a file and passed via body_path rather than through a GITHUB_OUTPUT heredoc, which also removes the risk of a PR body containing the delimiter and corrupting the release.
The tests job name interpolated matrix.branch, so the resulting check name contained the branch: 'Run tests - test:init - my-feature-branch'. Under a merge queue that becomes the ephemeral gh-readonly-queue/main/pr-N-<sha> ref, so the check name is unique per pull request and per merge attempt and can never be listed as a required status check. The queue would wait indefinitely for a name that never appears again. Use 'current' and 'main' instead, which is all that is needed to tell the two coverage matrix legs apart. This breaks no existing consumer: the name already varied per pull request, so no repository can currently have these checks in its required list.
6 tasks
The merge queue deletes its ephemeral gh-readonly-queue/... ref as soon as the entry merges, so code scanning has no ref to attach the analysis to and the upload fails with 'ref ... not found in this repository'. Observed on a real queue run: static-checks-npm and static-checks-poetry both failed on merge_group for this reason alone. That matters because lint and test must be a required check for the queue to be worth having. A queue that merges failing CI is worse than no queue. With this unfixed, making static-checks required would block every merge for a reason unrelated to the code. The scan itself still runs on merge_group, so a real finding still fails the job. Only the upload is skipped, and the same commit is uploaded from its pull request run and again on push to trunk, so no coverage is lost.
The comments had grown into essays repeating the same reasoning in seven files. Trimmed each to the non-obvious fact plus a pointer, and documented the merge_group behaviour once in a README table instead.
build-docker's check-version call had no fail_on_same_version, so it defaulted to true: fail when the package version already matches the latest release. For repos with no separate check-version job (testbed-portal among them) that is the de facto pull request version gate, so it cannot simply be turned off. Under merge queue versioning the branch deliberately carries no version bump, so in steady state the version equals the latest tag and this would fail every pull request. Queue repos set the input to false and rely on require-version-label instead. Default is true, so no existing caller changes behaviour. Worth noting the canary did not catch this: hello-world's package.json is 2.0.0 against a latest release tag of v1.1.3, so it was already ahead and the check passed by accident.
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.
Pull Request
Checklist
PR Type
Linked tickets
High level description
Adds an opt-in versioning model for repositories on a GitHub merge queue, where a pull request carries only the intent to bump (a
v:label) and the version number is computed on trunk after merge. Also fixes five existing workflows that break, or silently misbehave, undermerge_group.Additive throughout. The
synchronise-*-versionworkflows are untouched, so repositories not adopting a queue are unaffected.Detailed description
Why the current gate cannot survive a queue. Requiring
package.jsonto be ahead of the last release puts an absolute number on the branch, so two approved pull requests compute the same one. The queue merges the first and publishes that tag; the second now equals the latest tag, failscheck-version, and is evicted. Recovering it needs another bump, which dismisses its approval. That is the loop the queue was adopted to remove.New workflows
require-version-label.ymlv:label. Order independent, so every queued PR satisfies it at once. No token, no permissions.apply-version-npm.yml/apply-version-poetry.ymlsynchronise-pr-version-*, different location. Resolves the bump across every commit in the push, so batched queue merges work.Why the App token, and why a merge produces two runs.
build-docker,generate-sbomandrelease-githuball read the version from the checked-out tree, so the released commit must be the one carrying the new version.GITHUB_TOKENpushes do not trigger workflows, so a version commit pushed with it would never be built and nothing would be published. Using the App token means the version commit triggers a second run, and that run releases. A sharedchore(release):prefix guards both sides against triggering each other.This is the same App the
synchronise-*workflows already use, so adopters need no new app and no new secrets. It does need adding to the trunk ruleset bypass.Bug fixes
migration-checks-npm/poetryresolvemerge_groupmerge_grouppayload has neitherpull_requestnorbefore, so the base sha resolved empty and the immutability lint, ordering lint and seeded-upgrade silently no-opped for every queued PR. Lost coverage, not just a compatibility nicety.tests-npmcoverage comment guarded topull_requestmerge_group; would fail the job and stall the queue for any repo withcoverage: true.tests-npm/tests-poetryjob namesgh-readonly-queue/...ref, unique per PR and per merge attempt. Such a name can never be a required status check, so the queue would wait forever.static-checks-npm/poetrySARIF upload skipped onmerge_grouprelease-githubrelease notesgh pr list --limit 1, which orders by created date, not merged date, so a long-lived PR merged today loses to a newer one merged last week. Also credits only one PR, misattributing any batched merge. Now resolved from the commits since the previous release tag.build-dockergainsfail_on_same_version(defaulttrue)check-versioncall defaulted to failing when the version matches the latest release. For repos with no separatecheck-versionjob that is the PR version gate, so it cannot simply be disabled. Queue repos setfalse; default is unchanged for everyone else.Documentation
README gains a "Choosing a versioning model" section presenting the two models as alternatives, and a "Merge queue compatibility" table recording the
merge_groupbehaviour once rather than in each file. Three new examples docs.Describe alternatives you've considered
Pushing the version commit with
GITHUB_TOKEN. Preferred initially because it cannot loop. Rejected: such pushes never trigger a run, so the release keeps running on the pre-bump commit and silently publishes nothing. Making it work needs areforversioninput plumbed throughbuild-docker,generate-sbomandrelease-github, a much larger and non-additive change.Deriving the version from git tags and dropping it from
package.json. Cleanest conceptually, but changes anything readingpackage.jsonat build or deploy time.Replacing the per-PR bump outright. Rejected. Both models are kept and documented so repositories can adopt a queue at their own pace.
Disabling
build-docker's same-version failure rather than adding an input. Rejected: it is the de facto PR version gate for repos without a separatecheck-versionjob.Operational impact
Additive for every existing consumer. Behaviour changes only on code paths that are currently broken or unreachable on
pull_requestandpush.A repository adopting the new model must:
merge_groupto whichever workflow provides its required checks, or the queue waits indefinitely.fail_on_same_version: falseonbuild-dockerin its PR workflow.v:labels.Additional context
Design writeup: Merge Queue for testbed-portal.
Verified in a real merge queue on
hello-world(against a throwaway branch,mainuntouched):v:patchmerged together with no version on either branch. PR 31's queue branch was built on PR 30's commit, confirming stacking.migration-checkswas refused entry: "Pull request has failing required statuses".Embedded shell logic (label matching, version arithmetic, precedence, prefix guard, release-note tag detection and range) was executed against a case table including multi-label, no-label,
nullpayload, two-digit components, a force-movedlatesttag and near-miss labels such asv:majorish.Known gap:
apply-version-poetry.ymlis not exercised by the canary, which syncs onlypackage.json. A Poetry repo needs its own canary before adopting.