pcbjam/deploy/site/06-verify-deploy.sh

99 lines
4.2 KiB
Shell
Raw Normal View History

feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
#!/usr/bin/env bash
# Sweep a Pages deployment on its *.pages.dev URL — the real Cloudflare runtime,
# before any DNS is touched. One script for both the preview and the production
# deployment; only the stamp it writes differs.
#
# deploy/site/06-verify-deploy.sh --latest
# deploy/site/06-verify-deploy.sh --url https://abc123.pcbjam-site.pages.dev --scope prod-deploy
set -euo pipefail
. "$(dirname "$0")/lib/common.sh"
. "$(dirname "$0")/lib/parity.sh"
. "$(dirname "$0")/lib/cf-api.sh"
require_cmd curl jq awk sed
URL=""; SCOPE=preview
while [ $# -gt 0 ]; do
case "$1" in
--url) URL="$2"; shift 2 ;;
--latest) URL=""; shift ;;
--scope) SCOPE="$2"; shift 2 ;;
*) die "unknown arg: $1" ;;
esac
done
if [ -z "$URL" ] && [ -s "$STATE_DIR/state/last-deploy.json" ]; then
URL="$(jq -r .url "$STATE_DIR/state/last-deploy.json")"
fi
[ -n "$URL" ] || die "no deployment URL — pass --url, or run 05-deploy.sh first"
section "target"
echo " $URL (scope=$SCOPE)"
rc=0
assert_parity "$URL" --scope preview || rc=$?
section "Pages-specific checks"
# If the Function were not bundled (e.g. deployed from the wrong directory) the
# whole endpoint would simply be a static 404. The parity sweep would catch it,
# but say so explicitly — it is the most likely deploy-time mistake.
st="$(curl -sS -o /dev/null -w '%{http_code}' -X OPTIONS "$URL/api/waitlist" \
-H 'Origin: https://demo.pcbjam.com' 2>/dev/null || true)"
if [ "$st" = "404" ]; then
echo "FAIL /api/waitlist is 404 — the Function was not bundled."
echo " wrangler discovers functions/ relative to \$PWD; deploy from site/."
rc=1
else
echo "PASS /api/waitlist is routed (status $st)"
fi
# _routes.json restricts the Function to /api/*. Confirm that did not also break
# static 404 handling for everything else.
nf="$(curl -sS -L -o /dev/null -w '%{http_code}' "$URL/__cfm-parity-404__/" 2>/dev/null || true)"
[ "$nf" = "404" ] && echo "PASS static 404 handling intact under _routes.json" \
|| { echo "FAIL unknown path returned $nf, expected 404"; rc=1; }
if [ "$SCOPE" = prod-deploy ]; then
section "confirming this is the PRODUCTION deployment"
# Via wrangler, not the REST API: `wrangler login` is enough for this, whereas
# the REST call would need an API token that the Pages steps otherwise don't.
latest="$($WRANGLER pages deployment list --project-name "$PAGES_PROJECT" \
--environment production --json 2>/dev/null || true)"
envname="$(printf '%s' "$latest" | jq -r '.[0].Environment // "?"' 2>/dev/null || echo '?')"
did="$(printf '%s' "$latest" | jq -r '.[0].Id // "?"' 2>/dev/null || echo '?')"
brn="$(printf '%s' "$latest" | jq -r '.[0].Branch // "?"' 2>/dev/null || echo '?')"
if [ "$envname" != "Production" ]; then
echo "FAIL latest deployment environment is '$envname', not 'Production'"
feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
echo " (a branch name other than $PAGES_PROD_BRANCH makes it a preview)"
rc=1
else
echo "PASS latest deployment is Production (branch=$brn id=$did)"
feat(site): move the marketing site from Vercel to Cloudflare Pages www.pcbjam.com was the last piece of the stack on Vercel. It is now a Cloudflare Pages project (pcbjam-site) deployed by deploy-site.yml on every push to main touching site/** — content must not wait for a release tag. The Astro adapter is gone entirely: the build is pure static and the one dynamic route, /api/waitlist, is a Pages Function. Going adapter-free (rather than swapping in @astrojs/cloudflare, which has dropped Pages support and only targets Workers) removes three problems at once — no Astro/adapter major-version coupling, Footer.astro's build-time execSync keeps working because prerendering stays in Node, and image optimisation stays plain build-time sharp with no Cloudflare Images binding. Verified against a real Pages runtime (wrangler pages dev): 21/21 parity probes pass, versus 19/21 on live Vercel. The scripted runbook is in deploy/site/ — every mutating step is dry-run by default. Four behaviour differences were found by measurement and are handled here: - The blog post's COOP/COEP was already broken in production. vercel.json scoped the headers to the bare URL, but the page's own canonical is the trailing-slash form, which served 200 with no isolation headers — so search arrivals lost SharedArrayBuffer and the embedded Gerber viewer degraded. public/_headers covers both forms. - Pages answers unknown URLs with the homepage at HTTP 200 when the output has no 404.html — a soft-404 that invites indexing junk URLs as the homepage. Hence src/pages/404.astro. - Vercel's edge refused cross-site form POSTs ("Cross-site POST form submissions are forbidden"); Pages does not, and a cross-site <form> submit needs no CORS permission to be sent, so the allowlist cannot stop it. The Function reproduces the guard; JSON posts stay exempt as that is demo.pcbjam.com's allowlisted path. - Cache-Control: immutable on /_astro/* came from the Vercel adapter's generated route config, so it is now an explicit _headers rule. Secrets move to `wrangler pages secret put --project-name pcbjam-site` (RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL); WAITLIST_ALLOWED_ORIGINS stays unset so the allowlist stays in code. Local dev reads .dev.vars, now gitignored — the root repo's **/.dev.vars does not cover a nested git repo. privacy.md and cookies.md named Vercel as a GDPR Art. 28 processor; those mentions are removed and the existing Cloudflare entry widened to cover website hosting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 13:34:12 +02:00
fi
fi
section "diff vs the Vercel baseline (informational)"
BASE="$STATE_DIR/baseline/vercel"
if [ -d "$BASE" ]; then
OUT="$STATE_DIR/baseline/pages-$(date +%s)"; mkdir -p "$OUT"
for p in / /pricing /blog /privacy /blog/porting-kicad-graphics-to-webgl-in-2026 /gerber-demo/boot.js; do
f="$(printf '%s' "$p" | sed 's|/|_|g')"; [ "$f" = "_" ] && f="_home"
eff="$(_trace "$URL$p" | cut -f1)"
_headers "$eff" | grep -vE '^(date|age|etag|last-modified|content-length|server|cf-ray|cf-cache-status|nel|report-to|alt-svc|set-cookie|x-vercel-id|x-vercel-cache|x-matched-path|expires|via):' | sort > "$OUT/$f.headers"
if [ -f "$BASE/$f.headers" ]; then
d="$(diff -u "$BASE/$f.headers" "$OUT/$f.headers" || true)"
[ -n "$d" ] && { echo "--- $p"; printf '%s\n' "$d" | sed -n '3,$p'; }
fi
done
echo "(header differences above are for human review, not assertions)"
else
warn "no baseline at $BASE — run 00-baseline.sh while Vercel is still live"
fi
[ "$rc" -eq 0 ] || die "deployment verification FAILED — do not cut DNS."
case "$SCOPE" in
prod-deploy) stamp_write 06-prod-deploy-parity "url=$URL" "deployment_id=${did:-unknown}" ;;
*) stamp_write 06-preview-parity "url=$URL" ;;
esac
section "done"
echo "done: verified $URL"