Skip to content

Commit 459dbd5

Browse files
clauderjhuijsman
authored andcommitted
Add a pre-pull-docker-image composite action
Docker Hub intermittently returned 5xx errors that aborted `docker buildx` before it could resolve a build's base image, failing CI jobs before they ran any useful work; consumers had started hand-rolling pre-pull-with-retry steps inline in their workflows. Added a reusable composite action that pulls the image named by a Dockerfile's first `FROM` line into the runner's local image store, retrying transient failures with capped exponential backoff. A subsequent build in the same job using the default `docker` buildx driver then resolves the `FROM` image from the local store, keeping flaky registries off the build's critical path. The action is non-fatal: if every attempt fails it warns and exits successfully so the build can retry the pull itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPXVU5ytyWJxSL4YChKhm6
1 parent 1d72ae3 commit 459dbd5

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

pre-pull-docker-image/action.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# A composite action that pulls a Docker image into the runner's local
2+
# image store, retrying transient registry failures (e.g. 5xx errors
3+
# from Docker Hub or GHCR) with capped exponential backoff. Running it
4+
# before a `docker build`/`docker buildx` step (with the default
5+
# `docker` buildx driver, which resolves `FROM` images from the local
6+
# store when they are already present) keeps a flaky registry off the
7+
# build's critical path.
8+
#
9+
# The action is deliberately non-fatal: if every attempt fails it warns
10+
# and exits successfully, leaving the build to try the pull itself, so
11+
# adding it to a job can only help and never hurt.
12+
13+
name: "Pre-pull Docker Image"
14+
description:
15+
"Pull the Docker image named by a Dockerfile's first `FROM` line
16+
with retries, so that a later build in the same job finds it in the
17+
local image store."
18+
19+
inputs:
20+
dockerfile:
21+
description:
22+
"Path to a Dockerfile; the image to pull is taken from its first
23+
`FROM` line (the base of its first stage)."
24+
required: false
25+
default: "Dockerfile"
26+
max-attempts:
27+
description: "Maximum number of pull attempts."
28+
required: false
29+
default: "5"
30+
initial-delay-seconds:
31+
description:
32+
"Delay before the second attempt; doubles after every further
33+
failed attempt."
34+
required: false
35+
default: "5"
36+
37+
runs:
38+
using: "composite"
39+
steps:
40+
- name: Pull the image, retrying transient failures
41+
shell: bash
42+
# Bind the inputs to environment variables so the script body
43+
# never interpolates workflow expressions directly.
44+
env:
45+
DOCKERFILE: ${{ inputs.dockerfile }}
46+
MAX_ATTEMPTS: ${{ inputs.max-attempts }}
47+
INITIAL_DELAY_SECONDS: ${{ inputs.initial-delay-seconds }}
48+
run: |
49+
set -u
50+
if [ ! -f "${DOCKERFILE}" ]; then
51+
echo "warning: '${DOCKERFILE}' not found; skipping pre-pull" >&2
52+
exit 0
53+
fi
54+
# Take the image from the first `FROM` line, skipping options
55+
# like `--platform=...`.
56+
from_line="$(grep -iE '^[[:space:]]*FROM[[:space:]]' \
57+
"${DOCKERFILE}" | head -n1)"
58+
image="$(echo "${from_line}" \
59+
| awk '{ for (i = 2; i <= NF; i++)
60+
if ($i !~ /^--/) { print $i; exit } }')"
61+
if [ -z "${image}" ]; then
62+
echo "warning: could not determine an image to pull;" \
63+
"skipping pre-pull" >&2
64+
exit 0
65+
fi
66+
case "${image}" in
67+
*'$'*)
68+
# A reference like `FROM base-${VARIANT}` needs build args
69+
# to resolve, which this action doesn't have.
70+
echo "warning: image reference '${image}' contains an" \
71+
"unresolved variable; skipping pre-pull" >&2
72+
exit 0
73+
;;
74+
esac
75+
echo "Pre-pulling image: ${image}"
76+
attempt=1
77+
delay="${INITIAL_DELAY_SECONDS}"
78+
while true; do
79+
if docker pull "${image}"; then
80+
break
81+
fi
82+
if [ "${attempt}" -ge "${MAX_ATTEMPTS}" ]; then
83+
echo "warning: pull of ${image} failed after" \
84+
"${MAX_ATTEMPTS} attempts; continuing so the build can" \
85+
"retry the pull itself" >&2
86+
break
87+
fi
88+
echo "pull attempt ${attempt}/${MAX_ATTEMPTS} failed;" \
89+
"retrying in ${delay}s" >&2
90+
sleep "${delay}"
91+
attempt=$((attempt + 1))
92+
delay=$((delay * 2))
93+
done

0 commit comments

Comments
 (0)