From dded4ba6ce40cc8cb68e086b1b560e8f2f7b5369 Mon Sep 17 00:00:00 2001 From: Arsh Date: Wed, 11 Nov 2026 08:38:41 +0530 Subject: [PATCH 1/3] fix(release): purge after jsDelivr's version list refreshes, in a job of its own --- .github/scripts/purge-jsdelivr.sh | 19 ++++--- .github/scripts/stale-jsdelivr-files.sh | 11 ++++ .github/workflows/release-and-purge.yml | 73 ++++++++++++++----------- 3 files changed, 63 insertions(+), 40 deletions(-) create mode 100755 .github/scripts/stale-jsdelivr-files.sh diff --git a/.github/scripts/purge-jsdelivr.sh b/.github/scripts/purge-jsdelivr.sh index 34a5fe6..0d2d786 100755 --- a/.github/scripts/purge-jsdelivr.sh +++ b/.github/scripts/purge-jsdelivr.sh @@ -1,10 +1,15 @@ #!/usr/bin/env bash -# Purges @latest and @main for each file given. -set -euo pipefail +# Purges @latest for each file given. A throttled purge still answers HTTP 200, so the body decides. +set -uo pipefail +failed=0 for f in "$@"; do - for ref in latest main; do - url="https://purge.jsdelivr.net/gh/${GITHUB_REPOSITORY}@${ref}/${f}" - echo "Purging ${url}" - curl -sfS --retry 3 "${url}" > /dev/null || { echo "::error::Purge failed for ${url}"; exit 1; } - done + url="https://purge.jsdelivr.net/gh/${GITHUB_REPOSITORY}@latest/${f}" + body=$(curl -sfS --max-time 30 --retry 3 --retry-max-time 60 "${url}") || body="" + if jq -e '[.paths[] | .throttled == false and ([.providers[]] | all)] | all' <<< "${body}" > /dev/null 2>&1; then + echo "Purged ${url}" + else + echo "::error::Purge failed or was throttled for ${url}: ${body:-no response}" + failed=1 + fi done +exit "${failed}" diff --git a/.github/scripts/stale-jsdelivr-files.sh b/.github/scripts/stale-jsdelivr-files.sh new file mode 100755 index 0000000..7eccafb --- /dev/null +++ b/.github/scripts/stale-jsdelivr-files.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Prints each file whose @latest copy is not version $1. +set -uo pipefail +version=$1 +shift +for f in "$@"; do + v=$(curl -sfSI --max-time 20 "https://cdn.jsdelivr.net/gh/${GITHUB_REPOSITORY}@latest/${f}" \ + | tr -d '\r' | awk -F': ' 'tolower($1) == "x-jsd-version" { print $2 }') || v="" + echo "${f}: ${v:-unavailable}" >&2 + [ "${v}" = "${version}" ] || echo "${f}" +done diff --git a/.github/workflows/release-and-purge.yml b/.github/workflows/release-and-purge.yml index f2e444e..89bd892 100644 --- a/.github/workflows/release-and-purge.yml +++ b/.github/workflows/release-and-purge.yml @@ -31,11 +31,12 @@ env: FILES: surface_tag.js surface_tag.min.js surface_embed_v1.js surface_embed_v1.min.js jobs: - release-and-purge: + release: runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + version: ${{ steps.release.outputs.version }} steps: - - uses: actions/checkout@v4 - # jsDelivr @latest resolves to the highest SEMVER release, and this repo already uses v1.1.x — so # bump the PATCH of the latest release (v1.1.8 -> v1.1.9). Date-based tags would break @latest: # leading zeros (2026.09.02) are not valid semver, so jsDelivr would ignore them. target = the exact @@ -79,41 +80,47 @@ jobs: -f target_commitish="${GITHUB_SHA}" \ -f name="${RELEASE_TAG}" \ -f body="Surface Tag synced from surface-forms (packages/surface-scripts)." - echo "Released ${RELEASE_TAG}; jsDelivr @latest now resolves to this commit." + echo "Released ${RELEASE_TAG} at ${GITHUB_SHA}." | tee -a "${GITHUB_STEP_SUMMARY}" echo "version=${RELEASE_TAG#v}" >> "${GITHUB_OUTPUT}" - # An edge that refills between the purge and jsDelivr picking up the new release caches the - # previous version for 12 hours. Purge, re-purge until every @latest file reports the new - # version, then purge once more. + # A separate job, so "Re-run failed jobs" repeats the purge without cutting another release. + purge: + needs: release + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + VERSION: ${{ needs.release.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Purge jsDelivr cache run: .github/scripts/purge-jsdelivr.sh ${FILES} - - name: Wait for @latest to serve v${{ steps.release.outputs.version }} + - name: Check @latest serves v${{ needs.release.outputs.version }} + id: check + run: | + stale=$(.github/scripts/stale-jsdelivr-files.sh "${VERSION}" ${FILES} | xargs) + echo "stale=${stale}" >> "${GITHUB_OUTPUT}" + echo "- Stale after the first purge: ${stale:-none}" | tee -a "${GITHUB_STEP_SUMMARY}" + + # jsDelivr caches a repo's version list for up to 10 minutes and a purge doesn't refresh it, so a + # purge before then refills with the previous release (jsdelivr/jsdelivr#18376). + - name: Wait 10 minutes, then purge the stale files again + if: ${{ steps.check.outputs.stale != '' }} env: - VERSION: ${{ steps.release.outputs.version }} + STALE: ${{ steps.check.outputs.stale }} run: | - set -euo pipefail - served_version() { - curl -sfS -D - -o /dev/null "https://cdn.jsdelivr.net/gh/${GITHUB_REPOSITORY}@latest/$1" \ - | tr -d '\r' | awk -F': ' 'tolower($1) == "x-jsd-version" { print $2 }' - } - for i in $(seq 1 30); do - sleep 10 - stale="" - for f in ${FILES}; do - v=$(served_version "${f}" || true) - [ "${v}" = "${VERSION}" ] || stale="${stale} ${f}" - echo "${f}: ${v:-unavailable}" - done - if [ -z "${stale}" ]; then - echo "@latest serves ${VERSION} for every file." | tee -a "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - echo "Stale (${i}/30):${stale}" - .github/scripts/purge-jsdelivr.sh ${stale} - done - echo "::error::@latest still serves an older version after 5 minutes:${stale}" - exit 1 + sleep 600 + .github/scripts/purge-jsdelivr.sh ${STALE} - - name: Purge jsDelivr cache again - run: .github/scripts/purge-jsdelivr.sh ${FILES} + - name: Check @latest serves v${{ needs.release.outputs.version }} again + if: ${{ steps.check.outputs.stale != '' }} + run: | + stale=$(.github/scripts/stale-jsdelivr-files.sh "${VERSION}" ${FILES} | xargs) + if [ -n "${stale}" ]; then + echo "::error::@latest still serves an older version of: ${stale}. Customer sites load these files. Purging again within the hour is throttled; re-run failed jobs after an hour." + exit 1 + fi + echo "- Every @latest file serves ${VERSION}." | tee -a "${GITHUB_STEP_SUMMARY}" From e201a55836fa167ad15eeac5684a204896bc29d2 Mon Sep 17 00:00:00 2001 From: Arsh Date: Wed, 11 Nov 2026 08:41:25 +0530 Subject: [PATCH 2/3] fix(release): let the checks decide after a failed first purge --- .github/workflows/release-and-purge.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-and-purge.yml b/.github/workflows/release-and-purge.yml index 89bd892..a6919dc 100644 --- a/.github/workflows/release-and-purge.yml +++ b/.github/workflows/release-and-purge.yml @@ -95,7 +95,9 @@ jobs: with: persist-credentials: false + # The checks below decide the outcome; a file this purge missed is purged again. - name: Purge jsDelivr cache + continue-on-error: true run: .github/scripts/purge-jsdelivr.sh ${FILES} - name: Check @latest serves v${{ needs.release.outputs.version }} From d49795fcd22a9e1e892faa5c0f8e0c8dc5b56aef Mon Sep 17 00:00:00 2001 From: Arsh Date: Wed, 11 Nov 2026 16:03:10 +0530 Subject: [PATCH 3/3] fix(release): retry files whose first purge failed --- .github/scripts/purge-jsdelivr.sh | 8 +++++--- .github/workflows/release-and-purge.yml | 24 +++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/scripts/purge-jsdelivr.sh b/.github/scripts/purge-jsdelivr.sh index 0d2d786..0148a2c 100755 --- a/.github/scripts/purge-jsdelivr.sh +++ b/.github/scripts/purge-jsdelivr.sh @@ -1,14 +1,16 @@ #!/usr/bin/env bash -# Purges @latest for each file given. A throttled purge still answers HTTP 200, so the body decides. +# Purges @latest for each file given and prints the files whose purge failed. A throttled purge still +# answers HTTP 200, so the body decides. set -uo pipefail failed=0 for f in "$@"; do url="https://purge.jsdelivr.net/gh/${GITHUB_REPOSITORY}@latest/${f}" body=$(curl -sfS --max-time 30 --retry 3 --retry-max-time 60 "${url}") || body="" if jq -e '[.paths[] | .throttled == false and ([.providers[]] | all)] | all' <<< "${body}" > /dev/null 2>&1; then - echo "Purged ${url}" + echo "Purged ${url}" >&2 else - echo "::error::Purge failed or was throttled for ${url}: ${body:-no response}" + echo "::error::Purge failed or was throttled for ${url}: ${body:-no response}" >&2 + echo "${f}" failed=1 fi done diff --git a/.github/workflows/release-and-purge.yml b/.github/workflows/release-and-purge.yml index a6919dc..ab0d995 100644 --- a/.github/workflows/release-and-purge.yml +++ b/.github/workflows/release-and-purge.yml @@ -95,30 +95,36 @@ jobs: with: persist-credentials: false - # The checks below decide the outcome; a file this purge missed is purged again. - name: Purge jsDelivr cache - continue-on-error: true - run: .github/scripts/purge-jsdelivr.sh ${FILES} + id: purge + run: | + failed=$(.github/scripts/purge-jsdelivr.sh ${FILES} | xargs) || true + echo "failed=${failed}" >> "${GITHUB_OUTPUT}" + echo "- Purge failed for: ${failed:-none}" | tee -a "${GITHUB_STEP_SUMMARY}" + # A file whose purge failed is retried even when this runner's CDN server already serves it fresh. - name: Check @latest serves v${{ needs.release.outputs.version }} id: check + env: + FAILED: ${{ steps.purge.outputs.failed }} run: | stale=$(.github/scripts/stale-jsdelivr-files.sh "${VERSION}" ${FILES} | xargs) - echo "stale=${stale}" >> "${GITHUB_OUTPUT}" + retry=$(printf '%s\n' ${stale} ${FAILED} | sort -u | xargs) + echo "retry=${retry}" >> "${GITHUB_OUTPUT}" echo "- Stale after the first purge: ${stale:-none}" | tee -a "${GITHUB_STEP_SUMMARY}" # jsDelivr caches a repo's version list for up to 10 minutes and a purge doesn't refresh it, so a # purge before then refills with the previous release (jsdelivr/jsdelivr#18376). - - name: Wait 10 minutes, then purge the stale files again - if: ${{ steps.check.outputs.stale != '' }} + - name: Wait 10 minutes, then purge those files again + if: ${{ steps.check.outputs.retry != '' }} env: - STALE: ${{ steps.check.outputs.stale }} + RETRY: ${{ steps.check.outputs.retry }} run: | sleep 600 - .github/scripts/purge-jsdelivr.sh ${STALE} + .github/scripts/purge-jsdelivr.sh ${RETRY} > /dev/null - name: Check @latest serves v${{ needs.release.outputs.version }} again - if: ${{ steps.check.outputs.stale != '' }} + if: ${{ steps.check.outputs.retry != '' }} run: | stale=$(.github/scripts/stale-jsdelivr-files.sh "${VERSION}" ${FILES} | xargs) if [ -n "${stale}" ]; then