pcbjam/deploy/site/verify.sh

118 lines
4.8 KiB
Shell
Raw Normal View History

chore(deploy/site): retire the migration scaffolding, keep the health check The Vercel -> Cloudflare Pages move is done and the Vercel project is deleted, so the one-shot scripts have no remaining purpose. Nothing in CI ever called them — deploy-site.yml runs npm ci / test / build / pages deploy inline — so this removes 10 files and orphans nothing. Deleted: 00-baseline (refused to run without x-vercel-id, so permanently unrunnable), 01-preflight (proved Vercel state and API-token scopes), 07-dns-cutover (the phased cutover; in the end the records were attached through the dashboard, and the rules/apex phases went unused once we chose APEX_MODE=serve), 09-detach-vercel (its target project is gone), plus 03-ensure-project, 04-set-secrets, 05-deploy, 06-verify-deploy, 02-verify-local and 99-rollback, all either spent or duplicating CI. Their lib/cf-api.sh went with them: the survivors use wrangler, so the whole remaining path needs only `wrangler login` and no zone scopes. What is kept is the part with ongoing value: lib/parity.sh, the assertion set that caught five real defects during the migration — the live COOP/COEP bug on the post's canonical URL, the soft-404 Pages would have introduced, the cross-site form-POST guard Vercel had been providing for free, the missing immutable header, and HSTS max-age=0. "Does the page return 200" catches none of those. 08-verify-prod.sh becomes verify.sh, since the numbered sequence it belonged to no longer exists. It drops the stamp machinery, the dry-run plumbing and the Vercel-fallback messaging (there is no fallback now: recovery is promoting a previous Pages deployment), and gains --skip-dns / --skip-domains so it can be pointed at a single deployment via PROD_BASE before promoting it. The README is rewritten around the four invariants that fail SILENTLY — never widen _headers to /*, keep both URL forms of the Gerber post, never delete 404.astro, keep the cross-site form-POST guard — each with the reason, since the reason is the only thing that stops someone simplifying them back out. Verified after: 21 probes, 20 pass, 1 warn (HSTS max-age is 6 months vs Vercel's 2 years — on, just shorter), 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
2026-07-27 15:08:56 +02:00
#!/usr/bin/env bash
# Health check for www.pcbjam.com — the Astro marketing site on Cloudflare Pages.
#
# deploy/site/verify.sh # production
# PROD_BASE=https://abc123.pcbjam-site.pages.dev \
# deploy/site/verify.sh --skip-dns --skip-domains # one deployment
# EXPECT_HSTS=1 deploy/site/verify.sh # strict on HSTS
#
# Read-only: it makes GET/OPTIONS/POST requests and changes nothing. The POSTs are
# side-effect-free — the full-path probe uses the endpoint's honeypot branch,
# which returns before validation, before the rate limiter and before any Resend
# call, so it sends no mail and creates no contact.
#
# Exit 0 if every hard assertion passes. Warnings do not fail.
set -euo pipefail
. "$(dirname "$0")/lib/common.sh"
. "$(dirname "$0")/lib/parity.sh"
require_cmd curl jq awk sed
SKIP_DNS=0; SKIP_DOMAINS=0
while [ $# -gt 0 ]; do
case "$1" in
--skip-dns) SKIP_DNS=1; shift ;;
--skip-domains) SKIP_DOMAINS=1; shift ;;
-h|--help) sed -n '2,14p' "$0"; exit 0 ;;
*) die "unknown arg: $1" ;;
esac
done
rc=0
if [ "$SKIP_DNS" = 0 ]; then
require_cmd dig
section "DNS"
# Both hosts are PROXIED Pages custom domains, so they answer with Cloudflare
# anycast A records and expose no CNAME — an empty CNAME here is the expected
# state, not a problem. Failing to resolve at all is what's worth catching.
for h in "$ZONE_NAME" "www.$ZONE_NAME"; do
a="$(dig +short A "$h" | tr '\n' ' ')"
printf ' %-20s A=%s\n' "$h" "${a:-<none>}"
[ -n "$a" ] || { echo " FAIL $h does not resolve"; rc=1; }
done
fi
section "served by Cloudflare"
h="$(_headers "$PROD_BASE/")"
if [ -n "$(_hdr "$h" cf-ray)" ]; then
echo " PASS cf-ray present"
else
echo " FAIL no cf-ray — not being served through Cloudflare"; rc=1
fi
assert_parity "$PROD_BASE" --scope prod || rc=$?
assert_apex "$APEX_BASE" || rc=$?
section "cross-origin isolation on BOTH URL forms of the Gerber post"
# Explicit and duplicated on purpose. The bare form 308s to the trailing-slash
# form, and the trailing-slash form is the page's own canonical — what search
# sends people to. Scoping these headers to one form only is exactly the defect
# this site shipped with before the migration, and it is silent: the page renders
# fine, the embedded viewer just loses SharedArrayBuffer and degrades.
for path in /blog/porting-kicad-graphics-to-webgl-in-2026 /blog/porting-kicad-graphics-to-webgl-in-2026/; do
eff="$(_trace "$PROD_BASE$path" | cut -f1)"
hh="$(_headers "$eff")"
coop="$(_hdr "$hh" cross-origin-opener-policy)"; coep="$(_hdr "$hh" cross-origin-embedder-policy)"
if [ "$coop" = same-origin ] && [ "$coep" = require-corp ]; then
echo " PASS $path -> isolated"
else
echo " FAIL $path -> coop='${coop:-absent}' coep='${coep:-absent}' ($eff)"; rc=1
fi
done
section "demo integration (load-bearing)"
# demo.pcbjam.com has no backend of its own and cross-posts the waitlist form
# here. A CORS preflight cannot follow a redirect, so this must be 204 with zero
# hops — put a redirect in front of the endpoint and the demo breaks silently.
hops="$(curl -sS -o /dev/null -w '%{num_redirects}' -X OPTIONS "$PROD_BASE/api/waitlist" \
-H 'Origin: https://demo.pcbjam.com' 2>/dev/null || echo 9)"
st="$(curl -sS -o /dev/null -w '%{http_code}' -X OPTIONS "$PROD_BASE/api/waitlist" \
-H 'Origin: https://demo.pcbjam.com' -H 'Access-Control-Request-Method: POST' 2>/dev/null || true)"
if [ "$st" = "204" ] && [ "$hops" = "0" ]; then
echo " PASS preflight 204 with 0 redirects"
else
echo " FAIL status=$st hops=$hops (both 204 and 0 hops are required)"; rc=1
fi
if [ "$SKIP_DOMAINS" = 0 ]; then
section "custom domains on $PAGES_PROJECT"
doms="$($WRANGLER pages project list --json 2>/dev/null \
| jq -r --arg n "$PAGES_PROJECT" '.[] | select(."Project Name"==$n) | ."Project Domains"' || true)"
if [ -z "$doms" ]; then
warn "could not list projects (is \`wrangler login\` valid?) — skipping"
else
echo " $doms"
want="www.$ZONE_NAME"
[ "${APEX_MODE:-serve}" = serve ] && want="$want $ZONE_NAME"
for d in $want; do
case ",$(printf '%s' "$doms" | tr -d ' ')," in
*",$d,"*) echo " PASS $d attached" ;;
*) echo " FAIL $d NOT attached"; rc=1 ;;
esac
done
fi
fi
section "summary"
if [ "$rc" -ne 0 ]; then
echo "FAILED. To recover, promote a previous deployment:"
echo " Workers & Pages -> $PAGES_PROJECT -> Deployments -> Rollback to this deployment"
echo "or redeploy a known-good tree (git stays the source of truth):"
echo " git checkout <good-commit> -- site/"
echo " (cd site && npm ci && npm run build && npx --yes wrangler@4 pages deploy \\"
echo " --project-name $PAGES_PROJECT --branch production)"
exit 1
fi
echo "All checks passed."
echo "done: $PROD_BASE"