Skip to content

Commit bc11bbf

Browse files
authored
Skip rebuilds when Docker Hub tag check is inconclusive (#357)
## Summary The scheduled workflow's image-existence check silently defaults to "rebuild" whenever the Docker Hub tags API returns anything other than a parseable `{results: [...]}` body. Rate limits, 5xx, connection resets, and non-JSON edge responses all collapse to the same "tag missing" signal, so successful builds get re-queued an hour later even when the upstream source hasn't moved. Concrete example that triggered this PR: run [24721350367](https://github.com/ethpandaops/eth-client-docker-image-builder/actions/runs/24721350367) queued a full rebuild of `ethpandaops/nimbus-eth2:epbs-devnet-1-a23ebfd` and its `-minimal` variant even though: - `status-im/nimbus-eth2#epbs-devnet-1` HEAD hadn't moved in a week (`a23ebfd`, committed 2026-04-14). - Both tags were already on Docker Hub from a prior run the same day (pushed 10:47 UTC). ## What changed `.github/workflows/scheduled.yml` → `process_image()`: - Captures HTTP status separately from body via `curl -w '\n%{http_code}' --max-time 20`. - Classifies the response: - `200` with a well-formed `.results` array → authoritative, compute `exists` from an **exact** `.name == $tag` match (was a substring match, which could have masked a missing tag with a prefix-matching sibling). - `404` with `{"message":"object not found"}` → repo doesn't exist yet on Hub → build (first push). - Anything else (non-JSON, 429, 5xx, timeout, empty body) → retry up to 3 times with 2/4/6s backoff. - On persistent failure emits `exists:"unknown"` and logs a WARN, instead of silently falling back to "rebuild". Build-decision loop: - Only queues a build when `images[$IMAGE] == "false"` (authoritative empty). - `"unknown"` and missing entries log a skip line and are passed over. ## Verification Ran the new `process_image()` against the live Docker Hub API for six scenarios: | Case | Expected | Got | |---|---|---| | Exact tag present | `true` | `true` | | Tag genuinely absent (200, empty results) | `false` | `false` | | Repo missing on Hub (404 object not found) | `false` | `false` | | Unreachable host | `unknown` after 3 retries | `unknown` | | Non-JSON 4xx body | `unknown` after 3 retries | `unknown` | | Substring-only match (would have fooled old logic) | `false` | `false` | Also re-ran the full check path for `status-im/nimbus-eth2#epbs-devnet-1` and the `-minimal` variant against the live API — both now correctly resolve to `exists:"true"` → skip build, matching reality. `bash -n` on the extracted embedded script passes. ## Test plan - [ ] Next scheduled run completes without queuing nimbus-eth2 `epbs-devnet-1` (or any other already-present tag). - [ ] If Docker Hub returns transient errors during a future run, log shows `Skipping …: Docker Hub existence check was inconclusive.` instead of a queued rebuild. - [ ] First build for a brand-new client (Docker Hub repo not yet created) still triggers correctly via the 404 path. Signed-off-by: Barnabas Busa <busa.barnabas@gmail.com>
1 parent ad0e985 commit bc11bbf

1 file changed

Lines changed: 51 additions & 10 deletions

File tree

.github/workflows/scheduled.yml

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,55 @@ jobs:
8484
local LINE=$1
8585
local IMAGE=$2
8686
local URL=$3
87+
local IMAGE_TAG=$4
8788
8889
local imageOutput="${TEMP_DIR}/${LINE}_image.json"
8990
touch $imageOutput
90-
local exists=$(curl -s $URL | jq '.results | length > 0')
9191
92-
# check if exists == true
93-
if [ "$exists" == "true" ]; then
94-
exists=true
95-
else
96-
exists=false
92+
# Classify the Docker Hub response so we only trigger a build on an
93+
# authoritative "tag missing" answer. Retries absorb transient
94+
# failures (rate limits, 5xx, connection resets, non-JSON bodies).
95+
local exists="unknown"
96+
local attempt response http_code body count
97+
for attempt in 1 2 3; do
98+
response=$(curl -s --max-time 20 -w $'\n%{http_code}' "$URL")
99+
if [ $? -ne 0 ] || [ -z "$response" ]; then
100+
sleep $((attempt * 2))
101+
continue
102+
fi
103+
http_code=$(printf '%s' "$response" | tail -n1)
104+
body=$(printf '%s' "$response" | sed '$d')
105+
106+
if [ "$http_code" = "200" ]; then
107+
# Require a well-formed tags payload and exact-match the tag
108+
# name so a substring hit (e.g. `foo-abc` vs `foo-abcdef`)
109+
# can't mask a missing tag.
110+
count=$(echo "$body" | jq -e --arg tag "$IMAGE_TAG" '[.results[]? | select(.name == $tag)] | length' 2>/dev/null)
111+
if [ -n "$count" ]; then
112+
if [ "$count" -gt 0 ]; then
113+
exists=true
114+
else
115+
exists=false
116+
fi
117+
break
118+
fi
119+
elif [ "$http_code" = "404" ]; then
120+
# Repository itself doesn't exist yet on Docker Hub — first build needs to push.
121+
if echo "$body" | jq -e '.message == "object not found"' >/dev/null 2>&1; then
122+
exists=false
123+
break
124+
fi
125+
fi
126+
127+
echo "[LINE:$LINE] Docker Hub check attempt $attempt inconclusive for ${IMAGE} (http=$http_code), retrying." >&2
128+
sleep $((attempt * 2))
129+
done
130+
131+
if [ "$exists" = "unknown" ]; then
132+
echo "[LINE:$LINE] WARN: Docker Hub check inconclusive for ${IMAGE} after retries, skipping build." >&2
97133
fi
98134
99-
echo "{\"line\": \"$LINE\", \"image\": \"$IMAGE\", \"exists\": $exists}" >> $imageOutput
135+
echo "{\"line\": \"$LINE\", \"image\": \"$IMAGE\", \"exists\": \"$exists\"}" >> $imageOutput
100136
}
101137
102138
# Get commit hashes for each configuration in parallel
@@ -126,7 +162,7 @@ jobs:
126162
IMAGE_TAG="${TARGET_TAG}-${COMMIT_HASH}"
127163
IMAGE="${TARGET_REPOSITORY}:${IMAGE_TAG}"
128164
URL="https://hub.docker.com/v2/repositories/${TARGET_REPOSITORY}/tags?page_size=25&page=1&ordering=&name=${IMAGE_TAG}"
129-
process_image $LINE $IMAGE $URL &
165+
process_image "$LINE" "$IMAGE" "$URL" "$IMAGE_TAG" &
130166
done < <(yq -r 'to_entries | map_values({"value":.value, "index":.key}) | .[] | [.index, .value.source.repository, .value.source.ref, .value.target.repository, .value.target.tag] | @tsv' "$CONFIG_FILE")
131167
132168
wait
@@ -154,8 +190,13 @@ jobs:
154190
IMAGE="${TARGET_REPOSITORY}:${IMAGE_TAG}"
155191
CLIENT="${TARGET_REPOSITORY#*/}"
156192
157-
# Build if image doesn't exist OR if we couldn't determine existence (fail-safe)
158-
if [ -z "${images[$IMAGE]}" ] || [ "${images[$IMAGE]}" == "false" ]; then
193+
# Only build when Docker Hub authoritatively reported the tag as missing.
194+
# "unknown" (check failed after retries) or missing entries are skipped to
195+
# avoid burning runner time on rebuilds that the existence check couldn't verify.
196+
if [ "${images[$IMAGE]}" != "true" ] && [ "${images[$IMAGE]}" != "false" ]; then
197+
echo "[LINE:$LINE] Skipping ${IMAGE}: Docker Hub existence check was inconclusive."
198+
fi
199+
if [ "${images[$IMAGE]}" == "false" ]; then
159200
# Handle platforms and runners, ensuring output files are created even if empty
160201
platforms=$(yq e ".$CLIENT[]" "$PLATFORMS_FILE")
161202
platformsArr=""

0 commit comments

Comments
 (0)