From 5e849650f1ade0ef02da6a5449169882ef04fc61 Mon Sep 17 00:00:00 2001 From: Vighnesh Radhakrishnan Date: Fri, 4 Sep 2026 00:31:25 +0530 Subject: [PATCH 1/4] ci: add Surface Tag release+purge (Step 2) and drop the stale bundle-guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit surface-forms (packages/surface-scripts) now owns the tag source and pushes the built surface_tag.js / surface_embed_v1.js to this repo. Add the release-and-purge workflow: on a push to main that changes those files, cut the next semver release (so jsDelivr @latest advances) and purge the CDN (raw + .min.js) fail-loud. Remove the ci.yml 'Bundle is up to date' step — it rebuilds from this repo's now non-authoritative src/ and would diff against the pushed bytes and red main. --- .github/workflows/ci.yml | 11 ++-- .github/workflows/release-and-purge.yml | 75 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/release-and-purge.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 990be87..342454b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,10 +27,7 @@ jobs: - name: Unit tests run: pnpm test - - # surface_tag.js / surface_embed_v1.js are committed CDN artifacts. - # Fail if src/ was edited without rebuilding them. - - name: Bundle is up to date - run: | - pnpm run build - git diff --exit-code surface_tag.js surface_embed_v1.js + # NOTE: the "Bundle is up to date" build-guard was removed — surface-forms + # (packages/surface-scripts) now owns the tag source and pushes the built + # surface_tag.js / surface_embed_v1.js here, so rebuilding from this repo's + # (now non-authoritative) src/ would diff against the pushed bytes and fail main. diff --git a/.github/workflows/release-and-purge.yml b/.github/workflows/release-and-purge.yml new file mode 100644 index 0000000..9894fee --- /dev/null +++ b/.github/workflows/release-and-purge.yml @@ -0,0 +1,75 @@ +# STEP 2 of the two-step Surface Tag publish pipeline. +# +# Step 1 (surface-forms/.github/workflows/push-scripts-to-cdn.yml): builds the two bundles and pushes +# them to this repo's main in one commit (via the "Scripts Repo Deploy" GitHub App). +# Step 2 (this file): that push triggers this workflow, which cuts a release (advancing jsDelivr +# @latest) and purges the CDN so customers get the new bytes immediately. +# +# No provisioned token needed — this Action acts on its OWN repo, so the built-in GITHUB_TOKEN (granted +# contents:write below) is enough. surface-forms is now the source of truth; this repo only serves the +# built artifacts it pushes (the ci.yml "Bundle is up to date" build-guard is removed in this same PR). + +name: Release Surface Tag + purge CDN + +on: + push: + branches: [main] + # Only react to a real tag-bundle change, not docs/readme commits. + paths: + - surface_tag.js + - surface_embed_v1.js + +permissions: + contents: write + +# Never let two releases race (e.g. two quick pushes); run them one at a time. +concurrency: + group: release-surface-tag + cancel-in-progress: false + +jobs: + release-and-purge: + runs-on: ubuntu-latest + steps: + # 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 + # pushed commit, so @latest serves precisely these bytes. + - name: Cut a release for the pushed commit + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + latest=$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name' 2>/dev/null || echo "v1.1.0") + ver=${latest#v} # strip an optional leading v + IFS=. read -r MAJOR MINOR PATCH <<< "${ver}" + RELEASE_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))" + echo "Latest release ${latest}; cutting ${RELEASE_TAG} at ${GITHUB_SHA}" + gh api -X POST "repos/${GITHUB_REPOSITORY}/releases" \ + -f tag_name="${RELEASE_TAG}" \ + -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." + + # jsDelivr caches aggressively; purge so @latest / @main serve the new bytes immediately instead of + # up to 7 days later. A swallowed purge failure would show a green release while customers keep stale + # bytes, so fail loudly. + - name: Purge jsDelivr cache + run: | + set -euo pipefail + purge_failed=0 + for f in surface_tag.js surface_tag.min.js surface_embed_v1.js surface_embed_v1.min.js; do + for ref in latest main; do + url="https://purge.jsdelivr.net/gh/${GITHUB_REPOSITORY}@${ref}/${f}" + echo "Purging ${url}" + if ! curl -sfS "${url}"; then + echo "::warning::purge failed for ${url}" + purge_failed=1 + fi + done + done + if [ "${purge_failed}" -ne 0 ]; then + echo "::error::One or more jsDelivr purges failed; @latest/@main may serve stale bytes. Re-run the purge." + exit 1 + fi From 5ea5cc77165420509203a89b59401956a16f6e9b Mon Sep 17 00:00:00 2001 From: Vighnesh Radhakrishnan Date: Fri, 4 Sep 2026 01:37:26 +0530 Subject: [PATCH 2/4] ci: validate the latest release tag shape before deriving the next patch A malformed or pre-release latest tag (e.g. v1.1.9-beta, nightly) would otherwise split into non-numeric fields and produce an invalid/duplicate tag that silently stops @latest from advancing. Validate ^v?N.N.N and fail loud on a bad shape. --- .github/workflows/release-and-purge.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-and-purge.yml b/.github/workflows/release-and-purge.yml index 9894fee..5e65c53 100644 --- a/.github/workflows/release-and-purge.yml +++ b/.github/workflows/release-and-purge.yml @@ -41,8 +41,13 @@ jobs: run: | set -euo pipefail latest=$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name' 2>/dev/null || echo "v1.1.0") - ver=${latest#v} # strip an optional leading v - IFS=. read -r MAJOR MINOR PATCH <<< "${ver}" + # Validate vMAJOR.MINOR.PATCH before the arithmetic — a malformed or pre-release latest tag + # would otherwise yield an invalid/duplicate tag and silently stop @latest from advancing. + if [[ ! "${latest}" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "::error::Latest release tag '${latest}' is not vMAJOR.MINOR.PATCH; refusing to derive the next release. Fix the latest release tag." + exit 1 + fi + MAJOR="${BASH_REMATCH[1]}"; MINOR="${BASH_REMATCH[2]}"; PATCH="${BASH_REMATCH[3]}" RELEASE_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))" echo "Latest release ${latest}; cutting ${RELEASE_TAG} at ${GITHUB_SHA}" gh api -X POST "repos/${GITHUB_REPOSITORY}/releases" \ From d949d557bba16e1fcdffe7355daaf3ea95328f81 Mon Sep 17 00:00:00 2001 From: Vighnesh Radhakrishnan Date: Fri, 4 Sep 2026 16:43:28 +0530 Subject: [PATCH 3/4] ci: don't fabricate a release version on a non-404 API failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gh api releases/latest exits non-zero on 404 (no releases yet) AND on auth/rate-limit/network/5xx. The old '|| echo v1.1.0' treated both the same, so a transient API error derived v1.1.1 — which either collides with an existing tag (hard fail after the bytes are already on main, so @latest is stuck) or, when below the true latest, silently fails to advance @latest while the job goes green. Only fall back to v1.1.0 on a genuine 404; fail loud on any other error. --- .github/workflows/release-and-purge.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-and-purge.yml b/.github/workflows/release-and-purge.yml index 5e65c53..5bff13f 100644 --- a/.github/workflows/release-and-purge.yml +++ b/.github/workflows/release-and-purge.yml @@ -40,7 +40,22 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - latest=$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name' 2>/dev/null || echo "v1.1.0") + # Read the latest release tag. Only a genuine "no releases yet" (HTTP 404) may fall back to + # v1.1.0 — any OTHER failure (auth, rate limit, network, 5xx) must NOT fabricate a version: + # a fabricated low tag either collides with an existing one (hard fail AFTER the bytes are + # already on main, so @latest is stuck) or, when it's below the true latest, silently fails + # to advance @latest while this job goes green. + err=$(mktemp) + if latest=$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name' 2>"${err}"); then + : + elif grep -q "HTTP 404" "${err}"; then + latest="v1.1.0" + else + cat "${err}" >&2 + echo "::error::Could not read the latest release (not a 404). Refusing to fabricate a version and risk a stale @latest — re-run once the GitHub API is reachable." + exit 1 + fi + rm -f "${err}" # Validate vMAJOR.MINOR.PATCH before the arithmetic — a malformed or pre-release latest tag # would otherwise yield an invalid/duplicate tag and silently stop @latest from advancing. if [[ ! "${latest}" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then From 1a00e0e32a95480854334dcc63f642f355936e7f Mon Sep 17 00:00:00 2001 From: Vighnesh Radhakrishnan Date: Sat, 5 Sep 2026 02:15:00 +0530 Subject: [PATCH 4/4] ci: reject leading-zero release-tag components + base-10 increment The tag validation accepted zero-padded numeric components (e.g. v1.1.008), whose patch is then read by Bash arithmetic as octal ("008" -> value too great for base), aborting release creation and leaving @latest stale. Tighten the regex to reject leading zeros (no-leading-zero semver), so a malformed latest tag fails loudly and early, and force base-10 on the increment as defense-in-depth. --- .github/workflows/release-and-purge.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-and-purge.yml b/.github/workflows/release-and-purge.yml index 5bff13f..a09c190 100644 --- a/.github/workflows/release-and-purge.yml +++ b/.github/workflows/release-and-purge.yml @@ -58,12 +58,15 @@ jobs: rm -f "${err}" # Validate vMAJOR.MINOR.PATCH before the arithmetic — a malformed or pre-release latest tag # would otherwise yield an invalid/duplicate tag and silently stop @latest from advancing. - if [[ ! "${latest}" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - echo "::error::Latest release tag '${latest}' is not vMAJOR.MINOR.PATCH; refusing to derive the next release. Fix the latest release tag." + # Each component must have NO leading zeros: a zero-padded value like 008 is not valid semver, + # and Bash would parse it as octal ("008" -> value-too-great-for-base) and abort the release. + if [[ ! "${latest}" =~ ^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "::error::Latest release tag '${latest}' is not vMAJOR.MINOR.PATCH with no leading zeros; refusing to derive the next release. Fix the latest release tag." exit 1 fi MAJOR="${BASH_REMATCH[1]}"; MINOR="${BASH_REMATCH[2]}"; PATCH="${BASH_REMATCH[3]}" - RELEASE_TAG="v${MAJOR}.${MINOR}.$((PATCH + 1))" + # Force base-10 so the increment can never be misread as octal, even if the guard above changes. + RELEASE_TAG="v${MAJOR}.${MINOR}.$((10#${PATCH} + 1))" echo "Latest release ${latest}; cutting ${RELEASE_TAG} at ${GITHUB_SHA}" gh api -X POST "repos/${GITHUB_REPOSITORY}/releases" \ -f tag_name="${RELEASE_TAG}" \