ci(promote-base-latest): re-point base-latest by digest, not need_build

The gate keyed off need_build=='true', assuming need_build==false meant
base-latest was already current. A dry-run dispatch (promote_latest=false)
that pre-builds base-<hash> falsifies that: the later tag run sees
need_build==false and skipped promotion, leaving base-latest one base
behind (observed 2026-06-27, v1.2.3 dry-run-first release).

Gate now runs on every tag release / promote dispatch; the no-op
optimization moved into the step as a crane digest compare so it re-tags
only when base-latest actually differs from the released base-<hash>.
Workflow-only change; base hash unaffected (no base rebuild).
This commit is contained in:
pi
2026-06-27 20:57:03 +02:00
parent 2985d9ade8
commit b7197e88b0
2 changed files with 51 additions and 11 deletions
+34 -11
View File
@@ -565,16 +565,19 @@ jobs:
needs: needs:
- base-decide - base-decide
- build-variant - build-variant
# Skip on cache-hit base builds: when need_build=false, base-latest # Run on every tag release (and on promote_latest=true dispatches).
# already points at the same digest as base-<hash>, so the retag is # The job-level gate deliberately does NOT key off need_build anymore:
# a tautology and any transient failure of it is purely cosmetic. # the actual no-op optimization moved INTO the step as a digest compare
# Manual workflow_dispatch with promote_latest=true overrides this # (see below). Keying the gate on need_build was wrong because a prior
# gate as an escape hatch (e.g., if base-latest got hand-deleted). # dry-run dispatch (promote_latest=false) can pre-build+push base-<hash>,
# making need_build=false on the subsequent tag run even though
# base-latest is still stale — the old gate then skipped promotion and
# left base-latest pointing at the PREVIOUS base. (Observed 2026-06-27,
# v1.2.3: dry-run-first release left base-latest one base behind.)
if: | if: |
always() && always() &&
needs.build-variant.result == 'success' && needs.build-variant.result == 'success' &&
(inputs.promote_latest == 'true' || (inputs.promote_latest == 'true' || github.ref_type == 'tag')
(github.ref_type == 'tag' && needs.base-decide.outputs.need_build == 'true'))
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
@@ -596,11 +599,31 @@ jobs:
crane auth login docker.io \ crane auth login docker.io \
-u ${{ vars.DOCKERHUB_USERNAME }} \ -u ${{ vars.DOCKERHUB_USERNAME }} \
-p "${{ secrets.DOCKERHUB_TOKEN }}" -p "${{ secrets.DOCKERHUB_TOKEN }}"
- name: Re-tag base-<hash> as base-latest - name: Re-tag base-<hash> as base-latest (only if stale)
env:
BASE_HASH_REF: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
BASE_LATEST_REF: ${{ env.IMAGE }}:base-latest
run: | run: |
crane copy \ set -euo pipefail
${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }} \ # Correctness invariant: after a release, base-latest must resolve to
${{ env.IMAGE }}:base-latest # the SAME digest as the base-<hash> the just-built variants were
# FROM. Compare digests rather than trusting need_build — a prior
# dry-run dispatch can pre-build base-<hash>, so need_build=false on
# the tag run does NOT imply base-latest is already current. When the
# digests already match (genuine cache-hit release) this is a no-op,
# so we skip the crane copy entirely — preserving the original
# "don't do a tautological retag" intent and avoiding any cosmetic
# transient-failure exposure on releases that change nothing.
want=$(crane digest "${BASE_HASH_REF}")
have=$(crane digest "${BASE_LATEST_REF}" 2>/dev/null || echo "")
echo "base-<hash> digest: ${want}"
echo "base-latest digest: ${have:-<absent>}"
if [ "${want}" = "${have}" ]; then
echo "base-latest already current; nothing to promote."
else
echo "Promoting base-latest -> ${BASE_HASH_REF}"
crane copy "${BASE_HASH_REF}" "${BASE_LATEST_REF}"
fi
# ── Phase 6: update Hub description (only on real release runs) ──── # ── Phase 6: update Hub description (only on real release runs) ────
update-description: update-description:
+17
View File
@@ -13,6 +13,23 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
## Unreleased ## Unreleased
### Fixed (CI)
- **`promote-base-latest` now re-points `base-latest` reliably after a
dry-run-first release.** The job's gate previously required
`need_build == 'true'`, on the assumption that `need_build == false`
implied `base-latest` was already current. That assumption breaks when a
`workflow_dispatch` dry-run (`promote_latest=false`) pre-builds and pushes
`base-<hash>` first: the subsequent tag run then sees `need_build == false`
(probe hit) and **skipped** promotion, leaving `base-latest` pointing at the
*previous* base. (Observed 2026-06-27 releasing v1.2.3 via dry-run-then-tag
`base-latest` ended up one base behind, lacking the mempalace self-heal.)
Now the gate runs on every tag release (or `promote_latest=true` dispatch),
and the no-op optimization moved **into** the step as a `crane digest`
compare: it re-tags only when `base-latest` actually differs from the
released `base-<hash>`, so genuine cache-hit releases stay a no-op while
stale aliases get corrected. No image-content change; base hash unaffected.
--- ---
## v1.2.3 — 2026-06-27 ## v1.2.3 — 2026-06-27