Skip to content

ci(build): the release images exist before the release can be merged - #1329

Merged
ExtraToast merged 1 commit into
mainfrom
worktree-ci+release-images-before-merge
Sep 18, 2026
Merged

ExtraToast merged 1 commit into
mainfrom
worktree-ci+release-images-before-merge

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Closes #1291. Part of #1289.

The version bump lands on main and the images are built minutes later, so Flux applies a version whose images do not exist yet and the new pod sits in ImagePullBackOff while the old one serves. That happens on every release.

It is not hypothetical right now: #1066 is open as chore(main): release 1.8.0, and ghcr.io/esa-blueshell/api:v1.8.0 does not exist. Merge that today and production is pointed at a tag nobody has built.

What this achieves

A push to the release branch builds and publishes the full set of images under the release tag, so they are on ghcr before the release pull request can be merged. Nothing is deployed and no :latest moves — this only makes the artefacts exist ahead of the commit that names them.

How

build.yml gains release-please--branches--main as a push trigger, and one step in detect-changes resolves the tag for the whole run: the workflow input when set, otherwise the version from .release-please-manifest.json — which release-please has already bumped on that branch — and empty on an ordinary main push. Every later job reads that resolved value, so none of them needs to know which trigger fired.

The rest is unchanged. A release still rebuilds the complete set rather than the changed subset, images are still published by digest and assembled into a manifest list, and main still tags :sha-<short> only.

release.yml still calls this workflow once the release is actually cut. That republishes the same tag over the same layers, which is cheap, and it means the release tag exists even if the branch build never ran.

Not in scope

The digest pin and the required check that verifies it are #1293, and the app identity that will own the release branch is #1290. Until those land the deployed version is still a tag edited by hand, so this publishes images without deploying anything.

Worth a reviewer's attention

  • promote is deliberately gated on the workflow input, not the resolved tag. A release-branch push publishes the tag early, but until that branch merges there is no release, and moving :latest would deploy one that does not exist. So the branch build publishes images and moves nothing; only the real release promotes.
  • The release branch can now trigger a job holding packages: write. Today anyone with push access can write to that branch. A single app identity owns the release branch, and nothing else may write to it #1290 restricts it to a single app identity, which is what closes this properly — worth knowing that the two tickets are related in more than ordering.
  • Resolving the tag from the manifest rather than the branch name means a renamed release branch stops producing tags rather than producing wrong ones. The branch name appears in one place, matching the one already in release.yml.
  • Fetching that branch reports a forced update — release-please regenerates it whenever main moves. That is harmless here, since each push rebuilds, but it is the behaviour The deployed version is a digest, written by the pipeline and checked before merge #1293's pin check has to survive.

Verification

The tag resolver was exercised for all three triggers against the real tree: a main push resolves to empty, a release-branch push to v1.8.0, an explicit input passes through unchanged. The v1.8.0 came from the live release-please--branches--main manifest, not from a fixture.

build.yml parses, and the manifest-check job now covers it.

What cannot be shown until this is on main: the last acceptance criterion, that the tag resolves on ghcr from an open release pull request. A push trigger only fires from the default branch, so the next push to #1066's branch is what confirms it end to end.

The version bump landed on main and the images were built minutes later, so
Flux applied a version whose images did not exist yet and the new pod waited
in ImagePullBackOff while the old one served.

A push to the release branch now builds and publishes the full set under the
release tag, taken from the manifest release-please has already bumped there.
One step resolves the tag for the whole run, so no later job needs to know
which trigger fired.

promote stays gated on the workflow input rather than the resolved tag: the
branch build publishes the tag early, but until that branch merges there is no
release, and moving :latest would deploy one that does not exist.
@github-actions

Copy link
Copy Markdown
Contributor

Changes

Bucket Files Added Removed
ci · build & config 1 ████████ 35 ░░ 10
total 1 +35 −10

No production or test lines added.

Coverage

Statements Branches Functions Lines
api unit 40.40% 40.50% 32.68% 41.97%
main
api integration 68.49% 51.85% 68.29% 76.67%
main
frontend unit 67.64% 58.80% 60.46% 68.82%
main
frontend e2e 60.60% 37.81% 58.04% 64.12%
main

No baseline is cached from main yet, so there is nothing to compare against.

Patch coverage: this pull request changes no line that coverage measures.

@ExtraToast
ExtraToast merged commit 3668301 into main Sep 18, 2026
39 checks passed
@ExtraToast
ExtraToast deleted the worktree-ci+release-images-before-merge branch September 18, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The release images exist before the release can be merged

1 participant