diff --git a/docs/deploy-source.md b/docs/deploy-source.md index 1561387..057f902 100644 --- a/docs/deploy-source.md +++ b/docs/deploy-source.md @@ -106,6 +106,28 @@ exactly as it is today. to GitHub `master`; land it in Gitea and let it mirror. - **A merge that is only a Gitea merge** is not live until the mirror runs. Check `scripts/verify-deploy.sh` before calling a site change published. +- **The first mirrored deploy switches on a cron that deletes production + rows.** `vercel.json` carries two crons. `/api/the-nook/lifecycle` + (13:00 UTC) is already on the tree Vercel builds today, so it runs. + `/api/analytics-retention` (12:00 UTC, added in `1d9287a`, FRE-369) is + only on Gitea master: the first deploy that follows the mirror starts + calling it, and the next 12:00 UTC runs + `DELETE FROM VisitorAnalytics WHERE created_at < datetime('now','-365 days')` + against the production database. Not staged, not reversible — whatever is + older than the window + is gone the first time it fires. The window is + `ANALYTICS_RETENTION_DAYS`, and unset, non-positive, or over 730 each + resolve to the 365-day default (`src/server/analytics-retention.ts`). + 365 is the conservative end — picked so that switching the schedule on is + not itself a data-loss event — but conservative still means *over a year + old gets deleted*. Set the window in Vercel **before** the first mirrored + deploy if 365 is not what you want. The cron call is authorized with + `Authorization: Bearer ` + (`src/routes/api/analytics-retention.ts`), and the endpoint fails closed: + with no secret configured every call is a 401, so a missing secret is a + table that quietly stops being pruned, never an unauthenticated bulk + delete. Vercel supplies `CRON_SECRET` to its own crons and production + already runs the 13:00 job, so assume the secret is there and it runs. - **The FRE-371 fix's actual exploitability in production** depends on how the edge sets `x-forwarded-for`/`cf-connecting-ip`. That is measured and gated by the Security Engineer, not assumed here. @@ -114,6 +136,15 @@ exactly as it is today. scripts/verify-deploy.sh -It is read-only and needs no credentials. It checks both remotes' master SHA, -the live Nook/Nessa copy, and (when the commit is present locally) that -`2fd2a4a` is an ancestor of the deployed commit. +It is read-only and needs no credentials — that is what makes it safe to run +from an agent run as well as by the operator, and it is why the checks stay +read-only. It checks both remotes' master SHA, that `https://freno.me/` +answers a direct 200, the live Nook/Nessa copy, and (when the commit is +present locally) that `2fd2a4a` is an ancestor of the deployed commit. + +The root probe is there because every other check can stay green while the +root deploy is broken. It is status-only, and it deliberately does not probe +`https://freno.me/terms`: `SITEMAP_ROUTES.main` in +`src/lib/sitemap-routes.ts` has no `/terms` — Terms of Use is a Nessa +surface (`nessa.freno.me/terms`), so a 404 on the root domain's `/terms` is +the design, not a defect. diff --git a/scripts/verify-deploy.sh b/scripts/verify-deploy.sh index 079b71c..799b32c 100755 --- a/scripts/verify-deploy.sh +++ b/scripts/verify-deploy.sh @@ -3,11 +3,13 @@ # # Proves, in one run: # 1. Gitea master and GitHub master are the same commit (the mirror converged). -# 2. The live site serves copy that only exists on the reconciled line: +# 2. The root domain answers on its own terms: https://freno.me/ returns a direct 200, +# so a broken root deploy cannot read as green while the subdomains look fine. +# 3. The live site serves copy that only exists on the reconciled line: # NOOK-1038's scoped remote claim (GitHub-only before the merge) *and* # FRE-997's "Supported agents" (Gitea-only before the merge). Both at once # can only come from the merge commit. -# 3. The served tree therefore contains FRE-371 (`2fd2a4a`), which is an +# 4. The served tree therefore contains FRE-371 (`2fd2a4a`), which is an # ancestor of that merge — checked locally when the commit is in the object # store, so run this from a freno-dev checkout. # @@ -17,6 +19,7 @@ set -uo pipefail GITEA_REMOTE="${GITEA_REMOTE:-https://git.freno.me/Mike/freno-dev.git}" GITHUB_REMOTE="${GITHUB_REMOTE:-https://github.com/mikefreno/freno-dev.git}" +ROOT_HOST="${ROOT_HOST:-https://freno.me}" NOOK_HOST="${NOOK_HOST:-https://nook.freno.me}" NESSA_HOST="${NESSA_HOST:-https://nessa.freno.me}" @@ -43,6 +46,17 @@ fi echo echo "== live copy ==" +# The root is every other surface's entry point and deploys from the same tree, so it is +# probed first. No -L: a redirect away from 200 is the failure we want to see, not a pass. +root_code=$(curl -sS -o /dev/null -w '%{http_code}' "$ROOT_HOST/" 2>/dev/null) +if [ "$root_code" = "200" ]; then + ok "$ROOT_HOST/ serves 200" +elif [ -z "$root_code" ] || [ "$root_code" = "000" ]; then + bad "$ROOT_HOST/ gave no response, expected 200" +else + bad "$ROOT_HOST/ returned $root_code, expected 200 (green subdomains do not cover the root)" +fi + page=$(curl -sS "$NOOK_HOST/" 2>/dev/null) case "$page" in *"One-click setup for Tailscale and authorized known hosts"*)