Skip to content

Give a pull request build a tag that moves - #113

Merged
indiebrain merged 2 commits into
masterfrom
2026-09-03--tag-pull-request-images-with-a-moving-tag
Sep 4, 2026
Merged

Give a pull request build a tag that moves#113
indiebrain merged 2 commits into
masterfrom
2026-09-03--tag-pull-request-images-with-a-moving-tag

Conversation

@indiebrain

Copy link
Copy Markdown

The problem

Every commit on an open branch publishes an image tagged with its own commit sha, and that tag never moves and never expires. Only the newest image of a branch is useful, so the rest are superseded the moment the next commit lands and then stay in the package permanently.

The rate makes it the dominant source of growth. Two to three pull requests are opened a day, but ten to eighteen builds are published a day, so roughly five in six published images are dead on arrival. Each build writes 11 versions: one index, five per-platform manifests, five attestation manifests.

The change

tags: |
  type=semver,pattern={{raw}}
  type=sha,prefix=,suffix=,format=long,enable=${{ github.event_name != 'pull_request' }}
  type=ref,event=pr
Event Before After
Pull request commit <40-char sha>, unique and permanent pr-123, moves to the newest image
Push to master <40-char sha> unchanged
Push of a v* tag v4.5.0 + <40-char sha> unchanged

When pr-123 moves, the index it pointed at keeps no tag and its platform children stop being reachable. That is exactly what the scheduled prune already deletes, so this routes the growth into cleanup that is already merged and has been observed running clean, instead of adding a rule that deletes tagged versions.

The guard is written as github.event_name != 'pull_request' rather than {{is_default_branch}} on purpose: the latter is false on a tag push, which would strip the commit-sha tag off release images.

What this gives up

A pull request image is addressable while the branch is the one being worked on, rather than forever. pr-123 gives you that branch's current image, not the one from three commits ago.

Measured against the last 56 closed pull requests in this repository: half close within about ten hours, 93% within seven days, and all within thirteen days. An image for an open pull request keeps being refreshed by each push, so it stays available for as long as the work is live.

Verification

make lint-workflows passes. The behavior this changes only appears once a build runs for a pull request, which this pull request is, so its own dockerhub-image job is the first exercise of it: it should publish pr-<this number> and no commit-sha tag.

Nothing else in the workflow depends on the pull request case. The "Verify the ghcr package is publicly pullable" step reads steps.meta.outputs.version, which becomes pr-<number> on a pull request, but that step is already guarded with if: github.event_name != 'pull_request'. DOCKER_METADATA_PR_HEAD_SHA stays, since it still selects the head sha for the org.opencontainers.image.revision label.

Not in this change

Finished pull requests still leave one image each, which wants a second cleanup step (delete-tags: 'pr-*' with its own older-than). That has to be a separate invocation of the action, because older-than restricts every rule in a single step and orphan cleanup wants a much shorter interval than finished pull requests do. It is worth landing only after this change has been seen working, since it has nothing to act on until pr-* tags exist.

Every commit on an open branch publishes an image tagged with its own
commit sha, and that tag never moves or expires. Only the newest of them
is useful, so roughly five of every six branch images are superseded the
moment the next commit lands and then stay in the package permanently.
At two to three pull requests a day producing ten to eighteen builds,
this is what fills the operator image package.

Tag a pull request build with `pr-<number>` instead, which moves to the
newest image on each push to that branch. The image it replaces keeps no
tag, its platform children stop being reachable, and the scheduled prune
removes it a day later, so the growth is collected by the cleanup that
already runs rather than by a new rule that deletes tagged versions.

Builds of master and of release tags are unchanged: both keep a unique
commit-sha tag, and a release keeps its semantic version tag as well. An
image built for a pull request is addressable while the branch is the
one being worked on rather than forever, which trades away the ability
to pull a superseded commit's image. Measured against the last 56 closed
pull requests, half close within ten hours and all within thirteen days.
@indiebrain
indiebrain requested a review from a team as a code owner September 4, 2026 03:15
@indiebrain
indiebrain merged commit f032fc3 into master Sep 4, 2026
10 checks passed
@indiebrain
indiebrain deleted the 2026-09-03--tag-pull-request-images-with-a-moving-tag branch September 4, 2026 11:32
@indiebrain indiebrain mentioned this pull request Sep 4, 2026
indiebrain added a commit that referenced this pull request Sep 4, 2026
Prepare the repository for release of version v4.6.0. Two files change:
`Makefile` sets the version the tag is cut from, and `CHANGELOG.md`
moves the `Unreleased` entries under a dated heading.

## Read this before upgrading

**A `RedisFailover` that has `auth.secretPath` set restarts once when
the operator is upgraded.** Its pod template gains a record of which
password it was built for, and that record is new. A failover with no
password is unaffected.

The Redis pods go together rather than one at a time, because a
restarted pod cannot replicate with one that has not restarted yet.
Expect a minute or two, a few tens of seconds of it without a master.

**This also reaches anyone tracking `latest`.** `docker/metadata-action`
runs with the default `flavor.latest=auto`, so a `v*` tag build moves
`latest`. Verified on the current release: `latest` and `v4.5.0` share
digest `sha256:013e238e59fe…`, and `latest` was written seven seconds
after `v4.5.0` in the same run and has not moved since. Publishing
v4.6.0 moves `latest` to it.

## This release ships

**Five fixes to behavior against a live cluster:**

- Removing the `haproxy` block from a `RedisFailover` now removes the
Deployment, Service and ConfigMap it created, instead of leaving a proxy
routing traffic from a configuration nothing would update again (#107).
- A Redis node that could not be inspected no longer counts as "not the
master", which could drive recovery into promoting a second master or
replacing a healthy one. A failed demotion no longer reports success
(#106).
- The HAProxy health check authenticates, so `auth.secretPath` is usable
at all. Without it every backend stayed `DOWN` and the master endpoint
had no backend (#98).
- The connected-replicas check authenticates, so the operator stops
killing a healthy master's replica connections on every reconcile under
`requirepass` (#102).
- A password change on a running `RedisFailover` is applied rather than
leaving Redis on the password it started with and the operator locked
out (#104).

**Supply chain and image:**

- Built with Go 1.25.14 with `golang.org/x/net` at v0.58.0, taking the
reachable vulnerability count to zero, and the base images pinned by
digest (#108).
- The image carries a Software Bill of Materials, and provenance moves
to `max` (#109).
- Published to `ghcr.io/powerhome` alongside Docker Hub (#93).

## What this does to the open issues

Referenced, not closed. Closing is left to a person once the claims are
true of a published artifact.

| Issue | State after this release |
| --- | --- |
| #99 (connected-replica check omits password) | Fixed by #102. |
| #101 (v4.5.0 image has fixable vulnerabilities) | Fixed by #108. The
issue reports against `v4.5.0`, which predates the fix, so it is only
true of an artifact once v4.6.0 is published. |
| #100 (no-quorum recovery can promote wrongly) | **Partially**
addressed by #106. `SetOldestAsMaster` still sorts candidates by pod
`CreationTimestamp` (`operator/redisfailover/service/heal.go:115`),
which #100's acceptance criteria rule out directly. No integration
coverage was added either: #106 records that the harness cannot induce
the selective inspection failure the issue's reproduction turns on. Do
not close it. |

## Changelog style

The entries collected for this release had grown to multi-paragraph
bodies running 88 lines. Every release entry from v4.5.0 back is a
single line naming the change and linking its pull request, in blocks of
five to eight lines.

These are reduced to one line each to match. The reasoning lives in the
linked pull requests, and the long form goes into the v4.6.0 GitHub
Release body, which is what line 8 of the changelog already points
readers to. The one thing a reader must not miss, the restart above, is
kept in the file as an upgrade note above the entries.

## A correction to the changelog

The changelog carried `## [v4.5.1] - 2026-08-18`. **No such release
exists**: no `v4.5.1` git tag, and no `v4.5.1` image on Docker Hub or in
the ghcr package. The heading was added by feature pull request #93
rather than by a release commit, and it is dated three days before #93
merged.

The heading is removed. This file lists releases, and a heading for a
version with no tag, no image and no release notes points a reader at
something they cannot go and get. The record that the claim was
retracted belongs in this repository's history, which carries it, rather
than in the file. The change it listed ships here.

## Not included

Workflow and continuous-integration changes (#95, #96, #110, #111, #112,
#113, #114) are left out, along with the `/etc/cni/net.d` setup-minikube
workaround that rode in on #93. They change no published artifact and no
operator behavior, and the GitHub Release auto-notes list every merged
pull request anyway.

## After this merges

Capture the squash commit rather than tagging `HEAD` — this repository
squash-merges, so the commit to tag does not exist until the merge and
is not the branch head:

```
sha=$(gh pr view 115 --json mergeCommit -q .mergeCommit.oid)
git fetch origin --tags
git show $sha:Makefile | head -1          # must read VERSION := v4.6.0
git tag -a v4.6.0 -m "v4.6.0" "$sha"
git push origin v4.6.0
```

Then, once the tag build is green:

```
gh release create v4.6.0 --title v4.6.0 --notes-file <the long-form notes>
```

Order matters: `gh release create` against a tag that does not exist yet
creates one from the default branch, which tags the wrong commit and
fires another build.

If the tag build fails, re-run the failed jobs. Do not delete and
re-push the tag: re-running is idempotent since the bases are
digest-pinned, while a deleted public `v*` tag also drops out of the
image prune workflow's `exclude-tags: 'v*'` protection.

## Verification

- `## Unreleased` is retained and empty, matching how v4.5.0, v4.4.1 and
v4.4.0 were cut.
- The v4.6.0 block is 24 lines against 5 to 8 for its neighbours, with
one `### Added`, one `### Changed`, one `### Fixed`. The previous
`Unreleased` section had two `Added` and two `Fixed` sections
interleaved.
- This will be the first semantic version image published to `ghcr.io`,
so the first real exercise of `exclude-tags: 'v*'` in the prune
workflow. Dry-run that workflow afterwards and confirm `v4.6.0` appears
in no deletion list.
- It is also the first tag build to carry a bill of materials, since
#109 landed after v4.5.0. Worth confirming rather than assuming:
`docker buildx imagetools inspect powerhome/redis-operator:v4.6.0
--format '{{ json .SBOM }}'`
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.

2 participants