Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
98 changes: 98 additions & 0 deletions .github/workflows/release-and-purge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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
# 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.
# 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]}"
# 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}" \
-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
Loading