From 4e9dc64f4b142680dc3cc198f119dba01dc42630 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Fri, 3 Jul 2026 11:09:53 +0200 Subject: [PATCH 1/5] Harden pull image action against GHA connectivity instability --- .github/actions/pull_images/action.yml | 35 ++---------- .github/actions/pull_images/main.sh | 78 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 30 deletions(-) create mode 100755 .github/actions/pull_images/main.sh diff --git a/.github/actions/pull_images/action.yml b/.github/actions/pull_images/action.yml index 80d7c6fe5a8..42d316289cf 100644 --- a/.github/actions/pull_images/action.yml +++ b/.github/actions/pull_images/action.yml @@ -66,34 +66,9 @@ runs: env: PYTHONPATH: "." - - name: Login to Docker Hub - if: inputs.dockerhub_username != '' && inputs.dockerhub_token != '' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ inputs.dockerhub_username }} - password: ${{ inputs.dockerhub_token }} - - - name: Pull + - name: Login to Docker Hub and pull images shell: bash - run: | - max_attempts=5 - timeout_seconds=900 - attempt=1 - while true; do - echo "Running docker compose pull (attempt ${attempt}/${max_attempts}, timeout ${timeout_seconds}s)..." - - if timeout "${timeout_seconds}" docker compose pull; then - break - fi - - if [ "${attempt}" -ge "${max_attempts}" ]; then - echo "docker compose pull failed after ${attempt} attempts" - exit 1 - fi - - # Exponential backoff: 30s, 60s, 120s, 240s - retry_wait_seconds=$(( 30 * (1 << (attempt - 1)) )) - echo "docker compose pull failed (attempt ${attempt}/${max_attempts}), retrying in ${retry_wait_seconds}s..." - sleep "${retry_wait_seconds}" - attempt=$((attempt + 1)) - done + run: .github/actions/pull_images/main.sh + env: + DOCKERHUB_USERNAME: ${{ inputs.dockerhub_username }} + DOCKERHUB_TOKEN: ${{ inputs.dockerhub_token }} diff --git a/.github/actions/pull_images/main.sh b/.github/actions/pull_images/main.sh new file mode 100755 index 00000000000..6865369439f --- /dev/null +++ b/.github/actions/pull_images/main.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Logs in to Docker Hub (if credentials are provided) and pulls the images +# listed in compose.yaml, retrying on failure. +# +# When a command fails, unless the error is known to not be retryable, the command will be retried +# +# Expects DOCKERHUB_USERNAME / DOCKERHUB_TOKEN in the environment (may be empty). + +set -euo pipefail + +# Backoff delay (seconds) indexed by attempt number (1-based) +# since github action +BACKOFF_SECONDS=(10 20 40 80 160 320 320 320 320 320 320 320 320 320 320 320) +MAX_ATTEMPTS="${#BACKOFF_SECONDS[@]}" + +# Error patterns that should never be retried (auth/permission/not-found errors +# won't fix themselves on a retry). +NON_RETRYABLE_PATTERNS=( + "incorrect username or password" + "unauthorized" + "unknown command: docker compose" + "no configuration file provided: not found" + "empty compose file" + "yaml: construct errors" + "failed to resolve reference" +) + +retry_with_timeout() { + local description="$1" + local timeout_seconds="$2" + shift 2 + + local attempt=1 + local retry_wait_seconds + local output_file + output_file="$(mktemp)" + trap 'rm -f "${output_file}"' RETURN + + while true; do + echo "${description} (attempt ${attempt}/${MAX_ATTEMPTS}, timeout ${timeout_seconds}s)..." + + # Both save the output in output_file and print it to stdout. + # exit function if the command succeed + if timeout "${timeout_seconds}" "$@" 2>&1 | tee "${output_file}"; then + return 0 + fi + + for pattern in "${NON_RETRYABLE_PATTERNS[@]}"; do + if grep -q "${pattern}" "${output_file}"; then + echo "${description} failed with a non-retryable error: ${pattern}" + return 1 + fi + done + + echo "${description} failed (attempt ${attempt}/${MAX_ATTEMPTS})" + if [ "${attempt}" -ge "${MAX_ATTEMPTS}" ]; then + echo "${description} failed after ${attempt} attempts" + return 1 + fi + + retry_wait_seconds="${BACKOFF_SECONDS[attempt - 1]}" + attempt=$((attempt + 1)) + + echo "retrying in ${retry_wait_seconds}s..." + sleep "${retry_wait_seconds}" + done +} + +docker_login() { + echo "${DOCKERHUB_TOKEN}" | docker login --username "${DOCKERHUB_USERNAME}" --password-stdin +} + +if [ -n "${DOCKERHUB_USERNAME:-}" ] && [ -n "${DOCKERHUB_TOKEN:-}" ]; then + export -f docker_login # makes the function docker_login available in any child process spawned from it + retry_with_timeout "Logging in to Docker Hub" 60 bash -c docker_login +fi + +retry_with_timeout "Running docker compose pull" 900 docker compose pull From 2d6497115666196c8bd2f820a53c4e5b2095cddb Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Fri, 3 Jul 2026 11:13:21 +0200 Subject: [PATCH 2/5] lint --- .github/actions/pull_images/main.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/pull_images/main.sh b/.github/actions/pull_images/main.sh index 6865369439f..c0d4afb7438 100755 --- a/.github/actions/pull_images/main.sh +++ b/.github/actions/pull_images/main.sh @@ -71,7 +71,7 @@ docker_login() { } if [ -n "${DOCKERHUB_USERNAME:-}" ] && [ -n "${DOCKERHUB_TOKEN:-}" ]; then - export -f docker_login # makes the function docker_login available in any child process spawned from it + export -f docker_login # makes the function docker_login available in any child process spawned from it retry_with_timeout "Logging in to Docker Hub" 60 bash -c docker_login fi From 3a3307bc20dfa7cef84ff7f738769358eccc1642 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Fri, 3 Jul 2026 11:21:41 +0200 Subject: [PATCH 3/5] better GHA output --- .github/actions/pull_images/main.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/pull_images/main.sh b/.github/actions/pull_images/main.sh index c0d4afb7438..e8c2709f358 100755 --- a/.github/actions/pull_images/main.sh +++ b/.github/actions/pull_images/main.sh @@ -75,4 +75,4 @@ if [ -n "${DOCKERHUB_USERNAME:-}" ] && [ -n "${DOCKERHUB_TOKEN:-}" ]; then retry_with_timeout "Logging in to Docker Hub" 60 bash -c docker_login fi -retry_with_timeout "Running docker compose pull" 900 docker compose pull +retry_with_timeout "Running docker compose pull" 900 docker compose --progress plain pull From 65831783ec97e6b192c76bd9a6a6879708c4c2f4 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 6 Jul 2026 13:58:41 +0200 Subject: [PATCH 4/5] fix CI orchestrator --- utils/scripts/libraries_and_scenarios_rules.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/scripts/libraries_and_scenarios_rules.yml b/utils/scripts/libraries_and_scenarios_rules.yml index 8d4154b7fbe..fdff7fa34a6 100644 --- a/utils/scripts/libraries_and_scenarios_rules.yml +++ b/utils/scripts/libraries_and_scenarios_rules.yml @@ -157,7 +157,7 @@ patterns: # This section contains rules to apply to files related to GitHub Actions. ################################################################################ - .github/actions/pull_images/action.yml: + .github/actions/pull_images/*: scenario_groups: end_to_end .github/chainguard/*: scenario_groups: null From a2c00ede7572ae36c963440c1e672eec048a57a9 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 6 Jul 2026 14:00:11 +0200 Subject: [PATCH 5/5] Remove too generic error --- .github/actions/pull_images/main.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/pull_images/main.sh b/.github/actions/pull_images/main.sh index e8c2709f358..b3f517e9908 100755 --- a/.github/actions/pull_images/main.sh +++ b/.github/actions/pull_images/main.sh @@ -22,7 +22,6 @@ NON_RETRYABLE_PATTERNS=( "no configuration file provided: not found" "empty compose file" "yaml: construct errors" - "failed to resolve reference" ) retry_with_timeout() {