From 4ca762dc5c8194613b5d95c5a4337099a7c1f689 Mon Sep 17 00:00:00 2001 From: Austin Burdine Date: Tue, 8 Sep 2026 18:18:17 -0400 Subject: [PATCH] Fixed issues with update workflow no ref - add retry logic to versions.sh to handle publish races between this workflow and the Ghost release asset publish - retry until the release asset itself is present, since a release can go live before its assets finish uploading - bound every curl with connect/total timeouts so a stalled connection retries instead of hanging the job - add slack notify step to notify on failure, and reject a non-https webhook URL --- .github/workflows/update.yml | 44 ++++++++++++++++++++++++++++++++ versions.sh | 49 +++++++++++++++++++++++++++++++----- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 1fd525e8..01f2ba0e 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -206,3 +206,47 @@ jobs: --title "$COMMIT_MESSAGE" \ --body "$COMMIT_MESSAGE"$'\n\n'"Generated from https://github.com/${GITHUB_REPOSITORY}/commit/${HEAD_SHA}." fi + + # surface failures in Slack — this workflow runs unattended on + # repository_dispatch, so a broken run would otherwise go unnoticed. + # configure the SLACK_WEBHOOK_URL secret (an incoming webhook) to enable; + # the step no-ops when it is unset + - name: Notify Slack on failure + if: failure() && env.SLACK_WEBHOOK_URL != '' + # avoid -x here so the webhook URL is not echoed into the logs + shell: 'bash -Eeuo pipefail {0}' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + RUN_NUMBER: ${{ github.run_number }} + EVENT_NAME: ${{ github.event_name }} + REPOSITORY: ${{ github.repository }} + # empty when the run failed before the message was computed + COMMIT_MESSAGE: ${{ steps.message.outputs.message }} + run: | + # a webhook secret set to a plaintext URL would put the credential on the wire + if [ "${SLACK_WEBHOOK_URL#https://}" = "$SLACK_WEBHOOK_URL" ]; then + echo >&2 'error: SLACK_WEBHOOK_URL must be an https:// URL' + exit 1 + fi + + jq --null-input \ + --arg run_url "$RUN_URL" \ + --arg run_number "$RUN_NUMBER" \ + --arg event "$EVENT_NAME" \ + --arg repo "$REPOSITORY" \ + --arg commit_message "${COMMIT_MESSAGE:-}" ' + ":x: *<\($run_url)|Update Dockerfiles failed>* in `\($repo)`" + + "\n_event_ `\($event)` · _run_ `#\($run_number)`" + + (if $commit_message == "" then "" else "\n_update_ `\($commit_message)`" end) + | { + text: "Update Dockerfiles failed in \($repo) (run #\($run_number))", + blocks: [ { type: "section", text: { type: "mrkdwn", text: . } } ] + } + ' \ + | curl --silent --show-error --fail-with-body \ + --connect-timeout 10 \ + --max-time 30 \ + --header 'Content-Type: application/json' \ + --data @- \ + "$SLACK_WEBHOOK_URL" diff --git a/versions.sh b/versions.sh index a9a1d075..bb7921b0 100755 --- a/versions.sh +++ b/versions.sh @@ -3,6 +3,37 @@ set -Eeuo pipefail cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" +retryAttempts=6 + +# the ghost-version-publish dispatch fires as soon as the tag lands upstream, which is seconds +# before raw.githubusercontent.com serves that tag and before the GitHub release (and its +# assets) are published; retry these fetches instead of failing the whole update run on the race +# +# $1: url +# $2: optional jq filter that must produce output before the response counts as ready -- a +# release goes live seconds before its assets finish uploading, so HTTP 200 is not on its +# own proof that what we came for is in the body +fetch() { + local url="$1" filter="${2-}" attempt body reason + for (( attempt = 1; attempt <= retryAttempts; attempt++ )); do + # capture per attempt so a partial body from a failed one is never emitted + # without a deadline a stalled connection would hang here instead of retrying + if ! body="$(curl -fsSL --connect-timeout 10 --max-time 120 "$url")"; then + reason='request failed' + elif [ -n "$filter" ] && [ -z "$(jq --raw-output "$filter" <<<"$body")" ]; then + reason='response is missing what we need' + else + printf '%s\n' "$body" + return 0 + fi + echo >&2 "warning: $url: $reason (attempt $attempt/$retryAttempts)" + if [ "$attempt" -lt "$retryAttempts" ]; then + sleep "${attempt}0" + fi + done + return 1 +} + versions=( "$@" ) if [ ${#versions[@]} -eq 0 ]; then versions=( */ ) @@ -86,7 +117,7 @@ for version in "${versions[@]}"; do # get a list of architectures supported by the sharp module's prebuilt libraries # we cannot build it on other arches since the dep, libvips, is usually too old in Debian and Alpine - doc="$(curl -fsSL "https://raw.githubusercontent.com/TryGhost/Ghost/refs/tags/v$fullVersion/pnpm-lock.yaml" \ + doc="$(fetch "https://raw.githubusercontent.com/TryGhost/Ghost/refs/tags/v$fullVersion/pnpm-lock.yaml" \ | jq --compact-output --raw-input --null-input ' reduce ( inputs @@ -125,17 +156,23 @@ for version in "${versions[@]}"; do # These assets start at 6.60.0; anything older has no tarball to install from. if [ -n "$isNext" ]; then tarballName="ghost-$fullVersion.tgz" - releaseJson="$(curl -fsSL "https://api.github.com/repos/TryGhost/Ghost/releases/tags/v$fullVersion")" + # read by the readiness filter below; passing it through the environment keeps the asset + # name out of the jq program text + export tarballName + if ! releaseJson="$(fetch \ + "https://api.github.com/repos/TryGhost/Ghost/releases/tags/v$fullVersion" \ + '.assets[]? | select(.name == env.tarballName) | .browser_download_url // empty' \ + )"; then + echo >&2 "error: the GitHub release for 'v$fullVersion' has no '$tarballName' asset (these start at 6.60.0)" + exit 1 + fi + # guaranteed non-empty: fetch only returns once the filter above matches tarballUrl="$(jq <<<"$releaseJson" --raw-output --arg name "$tarballName" ' .assets[]? | select(.name == $name) | .browser_download_url // empty ')" tarballDigest="$(jq <<<"$releaseJson" --raw-output --arg name "$tarballName" ' .assets[]? | select(.name == $name) | .digest // empty ')" - if [ -z "$tarballUrl" ]; then - echo >&2 "error: the GitHub release for 'v$fullVersion' has no '$tarballName' asset (these start at 6.60.0)" - exit 1 - fi # GitHub reports the asset digest as "sha256:"; refuse anything else rather than # writing a hash the Dockerfile would then check with the wrong algorithm