Skip to content

feat: adopt merge queue versioning - #29

Open
dblane-digicatapult wants to merge 11 commits into
mainfrom
merge-queue-adoption
Open

dblane-digicatapult wants to merge 11 commits into
mainfrom
merge-queue-adoption

Conversation

@dblane-digicatapult

@dblane-digicatapult dblane-digicatapult commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Checklist

  • Have you read Digital Catapult's Code of Conduct?
  • I have performed a self-review of my own code.
  • I have made corresponding changes to the documentation.
  • My changes generate no new warnings.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.

This repo is the test. It is the canary for validating reusable workflows end to end.

PR Type

  • Feature

Linked tickets

High level description

Canary adoption of the merge queue versioning model from digicatapult/shared-workflows#122. Moves this repository from bumping the version on the pull request branch to applying it on main after merge.

Detailed description

Removed

  • synchronise-pr-version-npm.yml and synchronise-trunk-version-npm.yml callers. These commit a version onto the branch, which dismisses approval and evicts the PR from a queue. Both reusable workflows remain in shared-workflows for repositories not adopting a queue.
  • The check-version job in test.yml. It asserts package.json is ahead of the last release, which two approved PRs cannot both satisfy.

The two models compete over the same v: labels, so a repository uses one or the other.

Added

  • version-label.yml, gating a PR on exactly one v: label. Its own file because it must trigger on labeled and unlabeled, which in test.yml would re-run the whole suite on every label change.
  • apply-version.yml, computing and committing the version on main after merge.
  • renovate.json, holding the merge queue Renovate overrides inline.
  • A Merge queue section in README.md carrying the rationale, so the workflow files stay terse.

Changed

  • test.yml: adds merge_group, and sets fail_on_same_version: false on both build-docker jobs. Without that second change every PR would fail, because the branch deliberately carries no version bump and build-docker's check-version defaults to failing when the version matches the latest release.
  • release.yml: every job guarded so the release runs on the version commit rather than the merge commit, since build-docker, generate-sbom and release-github all read the version from the checked-out tree.

What a merge now looks like

  1. The queue merges the PR. apply-version computes the bump from its label and pushes a chore(release): commit. release.yml is skipped.
  2. That version commit triggers a second run. apply-version is skipped by its guard. release.yml runs and releases.

Renovate

Four overrides, inline rather than in a shared preset so an untested preset does not land across every repository at once:

Setting Why
bumpVersion: null The base preset has Renovate write the version onto its own branch, reintroducing the collision this change removes.
addLabels: ["v:patch"] The base preset applies no v: labels, so every Renovate PR would fail the gate and never merge.
platformAutomerge: true Renovate supports merge queues only when true, since GitHub does the enqueueing. Already the default; explicit because it is load bearing. Needs "Allow auto-merge" on the repo.
rebaseWhen: "automerging" The base preset's behind-base-branch rebases every open PR whenever main moves, wasted CI under a queue and evicts anything enqueued. automerging limits that to PRs actually automerging.

Renovate's branch automerge (automergeType: branch) can never work behind a merge queue. This repo uses pr, so it is unaffected.

Describe alternatives you've considered

Keeping both the per-PR bump callers and the new ones, selected by a variable. Rejected: both react to the same v: labels, so the repo would bump twice. Coexistence belongs in shared-workflows, where both reusables remain, not in a single caller.

Putting the Renovate overrides straight into a shared preset. Rejected for now: proving them here first avoids landing an untested preset org-wide.

rebaseWhen: "conflicted". An earlier revision of this PR used it, taken from a GitHub discussion rather than the documentation. Renovate documents auto as rebasing when automerge is enabled, so conflicted risks an automerging PR sitting behind its base and never merging. automerging cannot deadlock.

Operational impact

Prerequisites that need repository admin, and this PR should not merge without them:

  1. The GitHub App behind BOT_ID / BOT_KEY must be added to the bypass list of the organisation ruleset protecting main. It now pushes there rather than to feature branches. A repo-level ruleset cannot grant this, because rulesets are cumulative.
  2. "Allow auto-merge" must be enabled. Currently off. Renovate needs it to route through the queue.
  3. A repo-level ruleset with the merge queue rule and required checks, with "Require branches to be up to date" off. Set the required check names only after #122 merges, since the test check names change.

Before merging: the six shared workflows pinned to @merge-queue-versioning (migration-checks-npm/poetry, static-checks-npm/poetry, tests-npm/poetry, plus build-docker) must be flipped to @main once #122 lands.

To revert: restore the two synchronise-* callers and the check-version job, and drop the workflow changes. Nothing in shared-workflows needs reverting.

Additional context

Verified in a real merge queue on a throwaway branch, so main was never touched:

  • Two PRs both labelled v:patch merged together, package.json unchanged at 2.0.0 throughout. PR 31's queue branch was built on PR 30's commit, confirming stacking.
  • A PR failing migration-checks was refused entry to the queue.
  • Full suite green on the queue branch, 38 jobs: run 30699160605.

Known gaps:

  • The Renovate overrides are inline, so no other repository benefits until promoted to renovate-config. That promotion must also cover cargo and nuget, which carry the same bumpVersion: patch and are omitted here only because this repo has neither.
  • generate-sbom on PRs now tags Dependency-Track with the current released version rather than the upcoming one, since the branch is not bumped. It uses fail_on_same_version: false so nothing fails, but the tag is less useful.
  • This repo is a weak canary for version comparisons: its package.json is 2.0.0 against a latest release tag of v1.1.3, so it is already ahead. That masked the build-docker same-version failure, which was found by inspection rather than by CI here.

Canary for digicatapult/shared-workflows#122. Switches this repo from bumping
the version on the pull request branch to applying it on main after merge, which
is what makes a GitHub merge queue usable here.

- Remove the synchronise-pr-version / synchronise-trunk-version callers. They
  commit a version onto the PR branch, which dismisses approval and evicts the
  PR from the queue. Both reusable workflows remain available in
  shared-workflows for repos not adopting a queue.
- Remove the check-version job from test.yml. It asserts package.json is ahead
  of the last release, which two approved PRs cannot both satisfy.
- Add version-label.yml, which gates a PR on carrying exactly one v: label. Own
  file because it needs labeled/unlabeled triggers, which would otherwise re-run
  the whole suite on every label change.
- Add apply-version.yml, which computes and commits the version on main after
  merge.
- Add the merge_group trigger to test.yml. A required check that never reports
  on the queue branch leaves the queue waiting indefinitely.
- Guard every release.yml job so the release runs on the version commit rather
  than the merge commit, since build-docker, generate-sbom and release-github
  all read the version from the checked out tree.

Changed shared workflows are pinned to @merge-queue-versioning and must be
flipped to @main once that PR merges. release.yml's migration-checks were
pinned to the stale @ENG-313-migration-checks and are repointed here too.
This was referenced Aug 1, 2026
@dblane-digicatapult dblane-digicatapult added the v:minor Change requires a semver minor version change label Aug 1, 2026
Without this every Renovate PR would fail the new require-version-label gate,
since the shared renovate config applies no v: labels, and Renovate's
bumpVersion would keep writing a version onto the PR branch, reintroducing the
exact collision the merge queue model removes.

The overlay is pinned to the merge-queue-preset branch of renovate-config and
must be flipped to the default branch once digicatapult/renovate-config#1191
merges.
Replaces the dependency on an unmerged renovate-config preset branch. Proving
the overrides here first avoids landing an untested preset org-wide; once the
model is demonstrated end to end they can be promoted to a shared preset.

Same four overrides, now self-contained: bumpVersion unset, v:patch added,
platformAutomerge on, rebaseWhen conflicted. Validated with
renovate-config-validator.
Picks up the fix that skips the GHAS SARIF upload on merge_group. Without it
static-checks fails in the queue because the ephemeral queue ref is deleted
before code scanning can attach the analysis, which would block every merge
once lint and test are required checks.
tests-poetry still carried the branch in its job name, so its check name was
'Run tests - tests/unit - gh-readonly-queue/mq-demo/pr-32-<sha>'. That is unique
per pull request and per merge attempt and can never be a required status check.
Visible side by side with the fixed tests-npm in the queue run:
https://github.com/digicatapult/hello-world/actions/runs/30699160605
Checked against the Renovate docs rather than assumption.

conflicted was taken from a GitHub discussion, not the documentation. Renovate
documents rebaseWhen=auto as rebasing when the repository requires up to date
branches OR when automerge is enabled, so disabling rebasing entirely risks an
automerging pull request sitting behind its base and never merging. That is a
worse failure than the wasted CI it saves.

automerging is purpose built for this: behind-base-branch behaviour for pull
requests that are actually automerging, never for the rest. It cannot deadlock,
and still removes the base preset's blanket rebasing from :rebaseStalePrs, which
under a queue rebases every open PR whenever trunk moves and evicts anything
already enqueued.

Also documents why platformAutomerge is load bearing: merge queues are only
supported when it is true, and automergeType=branch can never work behind one.
The comments had grown to explain the whole design in every file, and the
renovate.json description was five paragraphs. Trimmed each to the non-obvious
fact plus a pointer, and moved the rationale into a Merge queue section of the
README where it can be read in one place.
build-docker's check-version defaults to failing when the version matches the
latest release. Under merge queue versioning the branch carries no bump, so in
steady state that would fail every PR. require-version-label is the gate now.

release.yml keeps the default, since it runs on the bumped version commit.

This repo would not have surfaced it either way: package.json is 2.0.0 against a
latest release tag of v1.1.3, so the check was passing by accident.
jobs:
apply-version:
if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }}
uses: digicatapult/shared-workflows/.github/workflows/apply-version-npm.yml@merge-queue-versioning
static-checks-npm:
uses: digicatapult/shared-workflows/.github/workflows/static-checks-npm.yml@main
if: ${{ startsWith(github.event.head_commit.message, 'chore(release):') }}
uses: digicatapult/shared-workflows/.github/workflows/static-checks-npm.yml@merge-queue-versioning
tests-npm:
uses: digicatapult/shared-workflows/.github/workflows/tests-npm.yml@main
if: ${{ startsWith(github.event.head_commit.message, 'chore(release):') }}
uses: digicatapult/shared-workflows/.github/workflows/tests-npm.yml@merge-queue-versioning
uses: digicatapult/shared-workflows/.github/workflows/migration-checks-npm.yml@ENG-313-migration-checks
if: ${{ startsWith(github.event.head_commit.message, 'chore(release):') }}
uses: digicatapult/shared-workflows/.github/workflows/migration-checks-npm.yml@merge-queue-versioning
permissions:
uses: digicatapult/shared-workflows/.github/workflows/migration-checks-poetry.yml@ENG-313-migration-checks
if: ${{ startsWith(github.event.head_commit.message, 'chore(release):') }}
uses: digicatapult/shared-workflows/.github/workflows/migration-checks-poetry.yml@merge-queue-versioning
permissions:

migration-checks-npm:
uses: digicatapult/shared-workflows/.github/workflows/migration-checks-npm.yml@main
uses: digicatapult/shared-workflows/.github/workflows/migration-checks-npm.yml@merge-queue-versioning

migration-checks-poetry:
uses: digicatapult/shared-workflows/.github/workflows/migration-checks-poetry.yml@main
uses: digicatapult/shared-workflows/.github/workflows/migration-checks-poetry.yml@merge-queue-versioning

build-docker-npm:
uses: digicatapult/shared-workflows/.github/workflows/build-docker.yml@main
uses: digicatapult/shared-workflows/.github/workflows/build-docker.yml@merge-queue-versioning

build-docker-poetry:
uses: digicatapult/shared-workflows/.github/workflows/build-docker.yml@main
uses: digicatapult/shared-workflows/.github/workflows/build-docker.yml@merge-queue-versioning

jobs:
require-version-label:
uses: digicatapult/shared-workflows/.github/workflows/require-version-label.yml@merge-queue-versioning
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v:minor Change requires a semver minor version change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants