From 5669a0e9e7640393746e9a13153642ee049b0441 Mon Sep 17 00:00:00 2001 From: sax3l Date: Tue, 15 Sep 2026 04:50:53 +0200 Subject: [PATCH] =?UTF-8?q?fix(cd):=20gate=20f=C3=A5r=20retry=20p=C3=A5=20?= =?UTF-8?q?'no=20signatures=20found'=20=E2=80=94=208-sekunders-racet=20mel?= =?UTF-8?q?lan=20release.yml:s=20push=20och=20cosign-sign=20m=C3=A4t=20liv?= =?UTF-8?q?e=202026-09-15=20(pull=2002:36:45,=20verify-fail=2002:37:03,=20?= =?UTF-8?q?sign=20fast=2002:37:11);=20h=C3=A5rd=20fail=20kvar=20f=C3=B6r?= =?UTF-8?q?=20=C3=A4kta=20verify-fel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/cd.yml | 58 +++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml index 11cea66..2a4c914 100644 --- a/.gitea/workflows/cd.yml +++ b/.gitea/workflows/cd.yml @@ -112,31 +112,55 @@ jobs: # the artifact release.yml produces instead. Bounded: a genuinely # broken/skipped release run fails this job loudly rather than # hanging forever. + # + # The loop retries TWO distinct not-ready states (measured live on + # run 2265dbb 2026-09-15: deploy pulled the image at 02:36:45 and + # failed verify at 02:37:03 with "no signatures found" while the + # release run attached the signature at 02:37:11 — an 8-second race + # that the pull-only retry missed): + # 1. tag not pushed yet (release still building) + # 2. tag pushed but signature not attached yet (release between + # push and cosign sign) attempt=0 max_attempts=40 - until docker pull "$IMAGE_REF" >/tmp/pull.log 2>&1; do + digest="" + while true; do attempt=$((attempt + 1)) if [ "$attempt" -ge "$max_attempts" ]; then - echo "::error::no signed image found at $IMAGE_REF after $max_attempts attempts (10 min) — release.yml has not produced an artifact for sha-$SHORT. Refusing to deploy an unverified rebuild." - cat /tmp/pull.log + echo "::error::no signed image found at $IMAGE_REF after $max_attempts attempts (10 min) — release.yml has not produced a signed artifact for sha-$SHORT. Refusing to deploy an unverified rebuild." + cat /tmp/pull.log 2>/dev/null || true + cat /tmp/verify.log 2>/dev/null || true exit 1 fi - echo "image not yet available at $IMAGE_REF (attempt $attempt/$max_attempts) — waiting for release.yml..." - sleep 15 + if ! docker pull "$IMAGE_REF" >/tmp/pull.log 2>&1; then + echo "image not yet available at $IMAGE_REF (attempt $attempt/$max_attempts) — waiting for release.yml..." + sleep 15 + continue + fi + + # Read back the digest the registry actually stored for this tag — + # same filter release.yml itself uses (RepoDigests can hold multiple + # entries; never trust index 0) — and verify THAT digest, never the + # mutable tag alone, so a tag reassigned after signing can't slip past. + repo_digest="$(docker inspect --format='{{json .RepoDigests}}' "$IMAGE_REF" \ + | node -e "const rd=JSON.parse(require('fs').readFileSync(0,'utf8')); const prefix=process.env.IMAGE_REPO+'@'; const m=rd.find(r=>r.startsWith(prefix)); if(!m){console.error('no RepoDigests entry starting with '+prefix+' among: '+JSON.stringify(rd));process.exit(1);} console.log(m);")" + digest="${repo_digest##*@}" + echo "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "::error::unexpected digest shape: $digest"; exit 1; } + + echo "verifying cosign signature on ${IMAGE_REPO}@${digest} ..." + if cosign verify --key cosign.pub "${IMAGE_REPO}@${digest}" >/tmp/verify.log 2>&1; then + break + fi + if grep -q "no signatures found" /tmp/verify.log; then + echo "image present but signature not attached yet (attempt $attempt/$max_attempts) — waiting for release.yml's cosign sign..." + sleep 15 + continue + fi + echo "::error::cosign verify FAILED for ${IMAGE_REPO}@${digest} (not a not-ready state — refusing to deploy)" + cat /tmp/verify.log + exit 1 done - # Read back the digest the registry actually stored for this tag — - # same filter release.yml itself uses (RepoDigests can hold multiple - # entries; never trust index 0) — and verify THAT digest, never the - # mutable tag alone, so a tag reassigned after signing can't slip past. - repo_digest="$(docker inspect --format='{{json .RepoDigests}}' "$IMAGE_REF" \ - | node -e "const rd=JSON.parse(require('fs').readFileSync(0,'utf8')); const prefix=process.env.IMAGE_REPO+'@'; const m=rd.find(r=>r.startsWith(prefix)); if(!m){console.error('no RepoDigests entry starting with '+prefix+' among: '+JSON.stringify(rd));process.exit(1);} console.log(m);")" - digest="${repo_digest##*@}" - echo "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "::error::unexpected digest shape: $digest"; exit 1; } - - echo "verifying cosign signature on ${IMAGE_REPO}@${digest} ..." - cosign verify --key cosign.pub "${IMAGE_REPO}@${digest}" - echo "digest=$digest" >> "$GITHUB_OUTPUT" echo "SIGNED ARTIFACT VERIFIED: ${IMAGE_REPO}@${digest} — authorizing Coolify redeploy of sha-$SHORT" -- 2.54.0