Skip to content

Commit 5b1966c

Browse files
committed
ci(docker): add retry wrapper for build-push with visibility and hardened shell
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.
1 parent 57808ef commit 5b1966c

2 files changed

Lines changed: 112 additions & 4 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Wraps docker/build-push-action with one automatic retry. Upstream explicitly
2+
# keeps retry out of the action (docker/build-push-action#1422); a local
3+
# composite keeps docker.yml readable and pins the same action SHA in one place.
4+
name: Docker build-push (with retry)
5+
description: >-
6+
Runs docker/build-push-action twice on failure with a configurable backoff,
7+
then exposes the digest from whichever attempt succeeded.
8+
9+
inputs:
10+
context:
11+
description: Build context path
12+
required: false
13+
default: '.'
14+
file:
15+
description: Dockerfile path (relative to repo root)
16+
required: true
17+
platforms:
18+
description: Comma-separated platforms list for buildx
19+
required: true
20+
push:
21+
description: Whether to push (string 'true' or 'false')
22+
required: true
23+
tags:
24+
description: Newline-separated image tags (from docker/metadata-action)
25+
required: true
26+
labels:
27+
description: Labels string (from docker/metadata-action)
28+
required: true
29+
cache-from:
30+
description: buildx cache-from value
31+
required: true
32+
cache-to:
33+
description: buildx cache-to value (include ignore-error=true for GHA cache flakes)
34+
required: true
35+
retry-wait-seconds:
36+
description: Seconds to sleep before the second attempt
37+
required: false
38+
default: '45'
39+
40+
outputs:
41+
digest:
42+
description: Manifest digest from the successful build attempt
43+
value: ${{ steps.resolve.outputs.digest }}
44+
45+
runs:
46+
using: composite
47+
steps:
48+
- name: Build and push (attempt 1)
49+
id: try1
50+
continue-on-error: true
51+
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
52+
with:
53+
context: ${{ inputs.context }}
54+
file: ${{ inputs.file }}
55+
platforms: ${{ inputs.platforms }}
56+
push: ${{ inputs.push == 'true' }}
57+
tags: ${{ inputs.tags }}
58+
labels: ${{ inputs.labels }}
59+
cache-from: ${{ inputs.cache-from }}
60+
cache-to: ${{ inputs.cache-to }}
61+
provenance: mode=max
62+
sbom: true
63+
64+
- name: Backoff before Docker build retry
65+
if: steps.try1.outcome == 'failure'
66+
shell: bash
67+
env:
68+
RETRY_WAIT_SECONDS: ${{ inputs.retry-wait-seconds }}
69+
run: |
70+
echo "::warning::Docker build-push attempt 1 failed; retrying in ${RETRY_WAIT_SECONDS}s…"
71+
sleep "${RETRY_WAIT_SECONDS}"
72+
73+
- name: Build and push (attempt 2)
74+
id: try2
75+
if: steps.try1.outcome == 'failure'
76+
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
77+
with:
78+
context: ${{ inputs.context }}
79+
file: ${{ inputs.file }}
80+
platforms: ${{ inputs.platforms }}
81+
push: ${{ inputs.push == 'true' }}
82+
tags: ${{ inputs.tags }}
83+
labels: ${{ inputs.labels }}
84+
cache-from: ${{ inputs.cache-from }}
85+
cache-to: ${{ inputs.cache-to }}
86+
provenance: mode=max
87+
sbom: true
88+
89+
- name: Resolve image digest
90+
id: resolve
91+
if: always()
92+
shell: bash
93+
run: |
94+
set -euo pipefail
95+
if [ "${{ steps.try1.outcome }}" = "success" ]; then
96+
echo "digest=${{ steps.try1.outputs.digest }}" >> "$GITHUB_OUTPUT"
97+
exit 0
98+
fi
99+
if [ "${{ steps.try2.outcome }}" = "success" ]; then
100+
echo "::notice::docker-build-push retry succeeded (attempt 2); investigate if this recurs across runs."
101+
echo "digest=${{ steps.try2.outputs.digest }}" >> "$GITHUB_OUTPUT"
102+
exit 0
103+
fi
104+
echo "::error::Docker build and push failed after two attempts (registry/cache flake or real build error)."
105+
exit 1

.github/workflows/docker.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,14 @@ jobs:
184184
type=semver,pattern={{major}}
185185
type=raw,value=${{ steps.version.outputs.version }},enable=${{ inputs.tag != '' }}
186186
187+
# Transient 502s from GHCR / Docker Hub / GHA cache during multi-platform
188+
# exports are retried inside `.github/actions/docker-build-push-retry`
189+
# (see docker/build-push-action#1422 — retry policy stays out of the
190+
# upstream action). `ignore-error=true` on cache-to avoids cache export
191+
# flakes failing an otherwise successful push.
187192
- name: Build and push
188193
id: build
189-
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
194+
uses: ./.github/actions/docker-build-push-retry
190195
with:
191196
context: .
192197
file: ${{ matrix.image.dockerfile }}
@@ -195,9 +200,7 @@ jobs:
195200
tags: ${{ steps.meta.outputs.tags }}
196201
labels: ${{ steps.meta.outputs.labels }}
197202
cache-from: type=gha,scope=${{ matrix.image.slug }}
198-
cache-to: type=gha,mode=max,scope=${{ matrix.image.slug }}
199-
provenance: mode=max
200-
sbom: true
203+
cache-to: type=gha,mode=max,scope=${{ matrix.image.slug }},ignore-error=true
201204

202205
# Cosign keyless signing. Each pushed tag is signed by the workflow's
203206
# OIDC identity, so consumers can verify the image with the strict,

0 commit comments

Comments
 (0)