From efa97f34624d3804a45c237329da74d00a3dba10 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Fri, 24 Apr 2026 09:22:23 +0100 Subject: [PATCH] ci(docker): add retry wrapper for build-push with visibility and hardened shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps docker/build-push-action with a local composite action that retries once on failure (upstream keeps retry out of the action per docker/build-push-action#1422). Adds ignore-error=true on cache-to so GHA cache export flakes don't fail an otherwise successful push. - Emit `::notice::` in the resolve step when attempt 2 recovers from a first-attempt failure, so silent retries are grep-able in run logs and trending registry/cache flakes stay visible. - Bind `retry-wait-seconds` via `env:` in the backoff step to match the env-binding convention used elsewhere in docker.yml (TAG_INPUT, DIGEST, TAGS) — no direct expression interpolation inside shell bodies. Preserves existing contract end-to-end: SHA pin, provenance=max, sbom=true, dual-registry push, `steps.build.outputs.digest` wiring to Cosign and the build-provenance attestations. --- .../docker-build-push-retry/action.yml | 105 ++++++++++++++++++ .github/workflows/docker.yml | 11 +- 2 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 .github/actions/docker-build-push-retry/action.yml diff --git a/.github/actions/docker-build-push-retry/action.yml b/.github/actions/docker-build-push-retry/action.yml new file mode 100644 index 0000000000..5d86c5c1ef --- /dev/null +++ b/.github/actions/docker-build-push-retry/action.yml @@ -0,0 +1,105 @@ +# Wraps docker/build-push-action with one automatic retry. Upstream explicitly +# keeps retry out of the action (docker/build-push-action#1422); a local +# composite keeps docker.yml readable and pins the same action SHA in one place. +name: Docker build-push (with retry) +description: >- + Runs docker/build-push-action twice on failure with a configurable backoff, + then exposes the digest from whichever attempt succeeded. + +inputs: + context: + description: Build context path + required: false + default: '.' + file: + description: Dockerfile path (relative to repo root) + required: true + platforms: + description: Comma-separated platforms list for buildx + required: true + push: + description: Whether to push (string 'true' or 'false') + required: true + tags: + description: Newline-separated image tags (from docker/metadata-action) + required: true + labels: + description: Labels string (from docker/metadata-action) + required: true + cache-from: + description: buildx cache-from value + required: true + cache-to: + description: buildx cache-to value (include ignore-error=true for GHA cache flakes) + required: true + retry-wait-seconds: + description: Seconds to sleep before the second attempt + required: false + default: '45' + +outputs: + digest: + description: Manifest digest from the successful build attempt + value: ${{ steps.resolve.outputs.digest }} + +runs: + using: composite + steps: + - name: Build and push (attempt 1) + id: try1 + continue-on-error: true + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: ${{ inputs.context }} + file: ${{ inputs.file }} + platforms: ${{ inputs.platforms }} + push: ${{ inputs.push == 'true' }} + tags: ${{ inputs.tags }} + labels: ${{ inputs.labels }} + cache-from: ${{ inputs.cache-from }} + cache-to: ${{ inputs.cache-to }} + provenance: mode=max + sbom: true + + - name: Backoff before Docker build retry + if: steps.try1.outcome == 'failure' + shell: bash + env: + RETRY_WAIT_SECONDS: ${{ inputs.retry-wait-seconds }} + run: | + echo "::warning::Docker build-push attempt 1 failed; retrying in ${RETRY_WAIT_SECONDS}s…" + sleep "${RETRY_WAIT_SECONDS}" + + - name: Build and push (attempt 2) + id: try2 + if: steps.try1.outcome == 'failure' + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: ${{ inputs.context }} + file: ${{ inputs.file }} + platforms: ${{ inputs.platforms }} + push: ${{ inputs.push == 'true' }} + tags: ${{ inputs.tags }} + labels: ${{ inputs.labels }} + cache-from: ${{ inputs.cache-from }} + cache-to: ${{ inputs.cache-to }} + provenance: mode=max + sbom: true + + - name: Resolve image digest + id: resolve + if: always() + shell: bash + run: | + set -euo pipefail + if [ "${{ steps.try1.outcome }}" = "success" ]; then + echo "digest=${{ steps.try1.outputs.digest }}" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ steps.try2.outcome }}" = "success" ]; then + echo "::notice::docker-build-push retry succeeded (attempt 2); investigate if this recurs across runs." + echo "digest=${{ steps.try2.outputs.digest }}" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::error::Docker build and push failed after two attempts (registry/cache flake or real build error)." + exit 1 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a4a2682220..e1e2f3fdc3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -184,9 +184,14 @@ jobs: type=semver,pattern={{major}} type=raw,value=${{ steps.version.outputs.version }},enable=${{ inputs.tag != '' }} + # Transient 502s from GHCR / Docker Hub / GHA cache during multi-platform + # exports are retried inside `.github/actions/docker-build-push-retry` + # (see docker/build-push-action#1422 — retry policy stays out of the + # upstream action). `ignore-error=true` on cache-to avoids cache export + # flakes failing an otherwise successful push. - name: Build and push id: build - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: ./.github/actions/docker-build-push-retry with: context: . file: ${{ matrix.image.dockerfile }} @@ -195,9 +200,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=${{ matrix.image.slug }} - cache-to: type=gha,mode=max,scope=${{ matrix.image.slug }} - provenance: mode=max - sbom: true + cache-to: type=gha,mode=max,scope=${{ matrix.image.slug }},ignore-error=true # Cosign keyless signing. Each pushed tag is signed by the workflow's # OIDC identity, so consumers can verify the image with the strict,