From afe3f7f57f0decae1a4580ae35934b03d2b1eef5 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Thu, 6 Aug 2026 09:10:17 -0500 Subject: [PATCH] =?UTF-8?q?Retry=20the=20ghcr=20push=20once=20=E2=80=94=20?= =?UTF-8?q?a=20transient=20403=20should=20not=20cost=20a=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ghcr refused the 1.9.29 push with "denied: permission_denied: Error from intermediary with HTTP status code 403", then accepted the identical build on a manual re-run minutes later. Nothing about the token, the permissions or the workflow changed in between; the registry simply said no once. The timing is what makes it worth handling. The GitHub Release job has already published by the time this runs, so a failure here leaves a tag that exists with no image behind it — alpha and every self-hoster pulling :latest see a version that is announced and unpullable, which reads as a broken release rather than a hiccup at a registry. It also needs a human to notice and re-run, which is the part that does not scale. One retry, after a pause, and a second refusal still fails the release. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL --- .github/workflows/release.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81fd5f6..a0522c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,33 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + # ghcr refused this push on the 1.9.29 release with "denied: permission_denied: Error from + # intermediary with HTTP status code 403", then accepted the identical build on a manual + # re-run minutes later. Nothing about the token, the permissions or the workflow changed in + # between — the registry simply said no once. + # + # That is worth one retry rather than a failed release, and it is worst exactly here: the + # GitHub Release job has already published by this point, so a failure leaves a tag that + # exists with no image behind it. Anyone deploying from ghcr — alpha, and every self-hoster + # pulling :latest — sees a version that is announced and unpullable, which reads as a broken + # release rather than a hiccup at a registry. - uses: docker/build-push-action@v6 + id: push + continue-on-error: true + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.ver.outputs.tags }} + + - name: Pause before retrying the push + if: steps.push.outcome == 'failure' + run: sleep 45 + + # No continue-on-error: a second refusal is a real failure and must fail the release. + - name: Retry the push + if: steps.push.outcome == 'failure' + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64