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:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user