pcbjam/deploy/site/verify.sh
Viktor Vaczi fdc4583288 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

118 lines
4.8 KiB
Shell
Executable file

#!/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"