Reserve the latest markers for the highest stable release

A maintenance cut of an older line (v5.1.36 after v6 GA, or a future
6.0.x patch after 6.1 ships) was allowed to move Docker/GHCR :latest and
the GitHub latest release marker onto itself, silently downgrading every
install that follows latest. Promote :MAJOR and :MAJOR.MINOR
unconditionally, but :latest and make_latest only when the tag is the
highest stable semver. workflow_dispatch gains force_latest as the
explicit rollback escape hatch.
This commit is contained in:
rcourtman 2026-07-09 09:37:40 +01:00
parent 93bccaff18
commit e240f162e9
2 changed files with 37 additions and 6 deletions

View file

@ -987,9 +987,21 @@ jobs:
-X PATCH -F draft=false -F make_latest=false
echo "[OK] Published as prerelease: ${TAG}"
else
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-X PATCH -F draft=false -F make_latest=true
echo "[OK] Published as latest: ${TAG}"
# 'latest' belongs to the highest stable semver overall. A
# maintenance cut of an older line (e.g. v5.1.36 after v6 GA)
# publishes without stealing the latest marker from the current
# line.
HIGHEST_STABLE=$(gh api --paginate "repos/${{ github.repository }}/tags" --jq '.[].name' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
if [ "$TAG" = "$HIGHEST_STABLE" ]; then
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-X PATCH -F draft=false -F make_latest=true
echo "[OK] Published as latest: ${TAG}"
else
gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-X PATCH -F draft=false -F make_latest=false
echo "[OK] Published WITHOUT latest marker: ${TAG} (highest stable is ${HIGHEST_STABLE})"
fi
fi
- name: Skip publish (draft only)

View file

@ -40,6 +40,11 @@ on:
required: true
type: boolean
default: false
force_latest:
description: 'Move :latest to this tag even if it is not the highest stable (rollback)'
required: false
type: boolean
default: false
permissions:
contents: read
@ -155,6 +160,7 @@ jobs:
TAG: ${{ steps.extract.outputs.tag }}
PRERELEASE: ${{ steps.extract.outputs.prerelease }}
OWNER: ${{ github.repository_owner }}
FORCE_LATEST: ${{ github.event_name == 'workflow_dispatch' && inputs.force_latest || false }}
run: |
set -euo pipefail
VERSION="${TAG#v}"
@ -172,14 +178,27 @@ jobs:
-t ghcr.io/${OWNER}/pulse:rc \
ghcr.io/${OWNER}/pulse:${TAG}
else
echo "Promoting stable tags for ${TAG}"
# :latest belongs to the highest stable semver overall. A
# maintenance cut of an older line (e.g. v5.1.36 after v6 GA, or a
# 6.0.x patch after 6.1 ships) must only move its own :MAJOR and
# :MAJOR.MINOR tags.
HIGHEST_STABLE=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
LATEST_ARGS=""
LATEST_ARGS_GHCR=""
if [ "$TAG" = "$HIGHEST_STABLE" ] || [ "${FORCE_LATEST}" = "true" ]; then
LATEST_ARGS="-t rcourtman/pulse:latest"
LATEST_ARGS_GHCR="-t ghcr.io/${OWNER}/pulse:latest"
echo "Promoting stable tags for ${TAG} (including :latest)"
else
echo "Promoting stable tags for ${TAG} WITHOUT :latest (highest stable is ${HIGHEST_STABLE})"
fi
docker buildx imagetools create \
-t rcourtman/pulse:latest \
${LATEST_ARGS} \
-t rcourtman/pulse:${MAJOR_MINOR} \
-t rcourtman/pulse:${MAJOR} \
rcourtman/pulse:${TAG}
docker buildx imagetools create \
-t ghcr.io/${OWNER}/pulse:latest \
${LATEST_ARGS_GHCR} \
-t ghcr.io/${OWNER}/pulse:${MAJOR_MINOR} \
-t ghcr.io/${OWNER}/pulse:${MAJOR} \
ghcr.io/${OWNER}/pulse:${TAG}