Skip to content

feat(promote-release): retag docker moving tags + promote paired release on human promotion#194

Draft
sydorovdmytro wants to merge 4 commits into
mainfrom
dsydorov/devops-1083-promote-release
Draft

feat(promote-release): retag docker moving tags + promote paired release on human promotion#194
sydorovdmytro wants to merge 4 commits into
mainfrom
dsydorov/devops-1083-promote-release

Conversation

@sydorovdmytro

Copy link
Copy Markdown
Contributor

Summary

  • New composite action promote-release: on a human editing a release from pre-release to full (release: types: [released] — verified live that this only fires on a real human edit, never on a GITHUB_TOKEN/bot-authored release publish), retags the moving docker tags (:latest, :{major}, :{major}.{minor}, plus any suffix variant like -fips) onto the already-published, already-signed version tag via docker buildx imagetools create (digest-preserving copy, no rebuild, no re-signing needed).
  • Optionally promotes a paired release in another repo (oss-repo input: unsets prerelease, sets latest) — used so a single click on the Pro release also promotes the customer-facing OSS release.
  • Image list is a JSON input, not hardcoded, so this isn't vcluster-pro-specific — a future loft-enterprise caller passes its own image set.
  • Depends on the caller no longer auto-publishing moving tags at build time (see companion vcluster-pro PR) — draft until that lands and both are rehearsed together in a throwaway repo.

Test plan

  • make test-promote-release — 13 bats cases (happy path incl. suffix variants, non-stable no-op, missing paired release, empty oss-repo, dry-run, validation failures)
  • zizmor clean, shellcheck clean
  • End-to-end rehearsal against real GHCR images + a throwaway release (tracked in DEVOPS-1083)

Internal Reference

References DEVOPS-1083

…ase on human release-promotion

DEVOPS-1083. Wires on: release: types: [released] (verified live: fires
only on a human editing prerelease->false, never on a GITHUB_TOKEN/bot
release publish) to a digest-preserving docker buildx imagetools retag of
:latest/:X/:X.Y (and suffix variants), then optionally un-prereleases +
latests a paired release in another repo. Image list and oss-repo are
caller-supplied inputs so this isn't vcluster-pro-specific.
…r GHCR login

Matches the existing vcluster-pro release.yaml pattern (DOCKER_USERNAME
secret paired with the GH_ACCESS_TOKEN PAT), instead of assuming the
release-promoting human's own GitHub login has GHCR write access.
The hand-written table drifted from auto-doc's exact formatting
(alphabetical ordering, quoted default values), which check-docs caught.
…x bats negation

- Add .github/workflows/test-promote-release.yaml - every other tested
  action has one; this one was missing, so the bats suite never actually
  ran in CI (only actionlint/zizmor/check-docs did).
- Replace real-looking test fixtures (v0.37.1, ghcr.io/loft-sh/vcluster-pro,
  loft-sh/vcluster) with obviously-fake ones (v9.9.9, example-org/...) so
  nobody mistakes test data for a real artifact.
- Fix two bare '! grep' assertions (shellcheck SC2314): a bash negation
  outside 'run' doesn't reliably fail a bats test. Use 'run !' instead.
suffix=$(jq -r '.suffix // ""' <<<"${entry}")

src="${image}:${VERSION}${suffix}"
for moving in latest "${MAJOR}" "${MAJOR}.${MINOR}"; do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking:latest, :{major}, and :{major}.{minor} are advanced unconditionally, with no check that VERSION is the newest published stable release. Because the action fires on any human pre-release→full edit, promoting a backport/patch — flipping an older v0.36.2 to full after v0.37.0 is already :latest — repoints :latest and :{major} to the older image, silently serving every :latest/:{major} puller a downgrade (:{major}.{minor} is fine, since it's scoped to the older line). The same hazard applies to gh release edit --latest at line 90 for the OSS repo.

Guard the advance on version ordering — only move :latest/:{major} when VERSION is newer than what they currently point at (e.g. compare against docker buildx imagetools inspect of the current :{major} tag, or gh release view --json isLatest for the OSS repo) — or add a documented skip-moving-tags input that callers set for backport promotions. This is the one concern I'd resolve before this ships beyond the vcluster-pro forward-release path.

Comment on lines +59 to +63
# Validate every entry before making any changes, so a config typo can't
# leave the images partially retagged.
IMAGE_COUNT=$(jq -r 'length' <<<"${INPUT_IMAGES}")
for ((i = 0; i < IMAGE_COUNT; i++)); do
image=$(jq -r ".[$i].image // empty" <<<"${INPUT_IMAGES}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider — The comment says validating up front means "a config typo can't leave the images partially retagged," but the loop only checks that the JSON image field is present — not that the source manifest <image>:<version><suffix> actually exists. If a suffix variant (e.g. -fips) wasn't built for this version, validation passes, then docker buildx imagetools create fails mid-loop after earlier entries are already retagged — the exact partial state the comment promises to prevent. Either extend the pre-flight loop with a docker buildx imagetools inspect "${image}:${VERSION}${suffix}" existence check per entry (skipped under dry-run), or soften the comment to match what's actually guaranteed.

if [[ -n "${OSS_REPO}" ]]; then
if gh release view "${VERSION}" --repo "${OSS_REPO}" >/dev/null 2>&1; then
echo "Promoting ${OSS_REPO}@${VERSION}: unset prerelease, set latest"
run gh release edit "${VERSION}" --repo "${OSS_REPO}" --prerelease=false --latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

considergh release edit is a hard-failing call that runs after all docker retags have completed. If it fails (transient API error, token missing contents:write on oss-repo, network blip), set -e exits the whole action non-zero even though the GHCR moving tags are already correctly set — so the run looks like a total failure and an operator can't tell the docker state without inspecting GHCR. Mirror the soft-skip already used on line 92: emit a ::warning:: with the manual recovery command and keep the advisory paired-release step non-blocking.

Suggested change
run gh release edit "${VERSION}" --repo "${OSS_REPO}" --prerelease=false --latest
if ! run gh release edit "${VERSION}" --repo "${OSS_REPO}" --prerelease=false --latest; then
echo "::warning::gh release edit failed for ${OSS_REPO}@${VERSION}; docker retags are already complete. Promote the OSS release manually: gh release edit ${VERSION} --repo ${OSS_REPO} --prerelease=false --latest"
fi

Comment thread .github/actions/promote-release/test/action.bats
Comment thread .github/actions/promote-release/test/action.bats
Comment on lines +127 to +130
@test "docker push failure -> non-zero exit" {
export DOCKER_MOCK_FAIL=1
run "$SCRIPT"
[ "$status" -ne 0 ]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider — This test asserts only a non-zero exit. Because DOCKER_MOCK_FAIL=1 fails the very first call, it can't distinguish fail-fast from fail-after-N-retags, and no test exercises a missing source manifest (<image>:<version><suffix> absent) — the scenario that produces the partial-retag state the action.sh:59 comment claims to prevent. Assert the call count so a first-call abort is pinned:

Suggested change
@test "docker push failure -> non-zero exit" {
export DOCKER_MOCK_FAIL=1
run "$SCRIPT"
[ "$status" -ne 0 ]
@test "docker push failure -> non-zero exit" {
export DOCKER_MOCK_FAIL=1
run "$SCRIPT"
[ "$status" -ne 0 ]
# aborts on the first docker call (set -e), leaving no further retags:
[ "$(wc -l < "$DOCKER_MOCK_CALLS")" -eq 1 ]
}

This becomes blocking if a future change makes the retag loop continue past a failed docker call (e.g. adding || true or a per-image retry), since no test would then catch a silent partial promotion.

contents: read
steps:
- name: Promote release
uses: loft-sh/github-actions/.github/actions/promote-release@<sha> # promote-release/v1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — This usage snippet pins with @<sha> # promote-release/v1, but every sibling action README (vcluster-release, semver-validation, subtree-mirror, release-branch-freeze) and this PR's own top-level README entry pin callers with the floating @promote-release/v1 tag. CLAUDE.md's release process confirms callers pin the coordination tag, not a SHA. Match that here so the two READMEs agree:

Suggested change
uses: loft-sh/github-actions/.github/actions/promote-release@<sha> # promote-release/v1
uses: loft-sh/github-actions/.github/actions/promote-release@promote-release/v1

@loft-bot

Copy link
Copy Markdown

Correction to my review summary above. Two of the three items I listed under Blocking concerns — the test/action.bats:67 and :91 run ! grep assertions — are withdrawn as false positives. This file sets bats_require_minimum_version 1.5.0, under which run ! <cmd> is bats's built-in expect-failure assertion, not a no-op. I confirmed on bats 1.11.0 that run ! grep -q '^EDIT ' fails the test when an EDIT line is present, so both assertions correctly enforce that no gh release edit runs on the no-matching-release and dry-run paths. I've replied to those two threads and resolved them.

The remaining blocking item stands: src/action.sh:78 — the moving tags (:latest/:{major}) can move backwards on a backport/patch promotion. The consider/nit items (partial-retag comment at :59–63, non-blocking gh release edit at :90, the docker-failure-test strengthening at :130, and the README pin nit at README.md:52) are unaffected.

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