From f08fce4ab6efd841f4038fbf804ceabc4d1070c7 Mon Sep 17 00:00:00 2001 From: agent Date: Mon, 19 Oct 2026 14:25:29 +0200 Subject: [PATCH] ci: refuse to re-publish an existing image tag (zegit#262) A dispatch for 12.11.1-ci7 without sign_only rebuilt and re-pushed the tag. The registry dropped the old manifest, so flux-config's 12.11.1-ci7@sha256:86d7e3b4 pin now 404s on the next pull. That is the zegit#143 threat shape, done by accident. Like zegit-arc-runner, a build now refuses a tag the registry already holds, and sign_only refuses one it does not. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/build-image-gitea.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-image-gitea.yml b/.github/workflows/build-image-gitea.yml index 3f3a069..208ee85 100644 --- a/.github/workflows/build-image-gitea.yml +++ b/.github/workflows/build-image-gitea.yml @@ -78,13 +78,31 @@ jobs: # A master push VALIDATES that the Dockerfile still builds on both arches # but publishes nothing; only an explicit dispatch names a tag and pushes. # Guessing a tag from a branch push would silently mint versions. + # + # Tags are immutable (zegit#262): consumers pin tag@digest, and the + # registry drops the old manifest when a tag moves, so re-publishing a + # tag breaks every pin on it. A build refuses an existing tag; sign_only + # refuses a tag that does not exist. - id: meta + env: + EVENT: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + SIGN_ONLY: ${{ inputs.sign_only }} run: | set -euo pipefail - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "$EVENT" = "workflow_dispatch" ]; then + pull=$(curl -fsS "https://${REGISTRY}/v2/token?scope=repository:${IMAGE_PATH}:pull&service=container_registry" | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])') + tags=$(curl -fsS -H "Authorization: Bearer ${pull}" "https://${REGISTRY}/v2/${IMAGE_PATH}/tags/list?n=10000" | python3 -c 'import json,sys; print("\n".join(json.load(sys.stdin).get("tags") or []))') + if printf '%s\n' "$tags" | grep -qxF "$INPUT_TAG"; then + if [ "$SIGN_ONLY" != "true" ]; then + echo "::error::${IMAGE_PATH}:${INPUT_TAG} already exists; tags are immutable, so nothing is overwritten (to sign it, dispatch with sign_only)"; exit 1 + fi + elif [ "$SIGN_ONLY" = "true" ]; then + echo "::error::${IMAGE_PATH}:${INPUT_TAG} is not published; nothing to sign"; exit 1 + fi echo "push=true" >> "$GITHUB_OUTPUT" - echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" - echo "sign_only=${{ inputs.sign_only }}" >> "$GITHUB_OUTPUT" + echo "tag=${INPUT_TAG}" >> "$GITHUB_OUTPUT" + echo "sign_only=${SIGN_ONLY}" >> "$GITHUB_OUTPUT" else echo "push=false" >> "$GITHUB_OUTPUT" echo "tag=ci-validation" >> "$GITHUB_OUTPUT" -- 2.54.0