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
118 lines
4.8 KiB
Shell
Executable file
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"
|