Two findings from the FRE-1695 review (approved atdf788cb6). 1. The mirrored deploy activates a destructive cron the deploy docs never mentioned. vercel.json carries two crons: /api/the-nook/lifecycle (13:00 UTC), already on the tree Vercel builds, and /api/analytics-retention (12:00 UTC,1d9287a/ FRE-369), which exists only on Gitea master. The first deploy that follows the mirror starts calling it, and the next 12:00 UTC deletes every VisitorAnalytics row older than the window — ANALYTICS_RETENTION_DAYS, 365 by default — irreversibly and unstaged. The behaviour is intended; the silence was not, because the operator is the one who presses the button. docs/deploy-source.md "Failure modes" now names the cron, the ANALYTICS_RETENTION_SECRET ?? CRON_SECRET bearer check, the 365-day default, the fails-closed-without-a-secret path, and that the first run after enabling prunes at that default. 2. scripts/verify-deploy.sh could read green with a broken root deploy: it probed nook.freno.me, nessa.freno.me/terms and the two remotes, never freno.me itself — and FRE-1695's acceptance names the root domain. Adds a ROOT_HOST (default https://freno.me) status check ahead of the subdomain probes: expects a direct 200, no -L, so a redirect reads as a failure. It deliberately does not probe https://freno.me/terms — SITEMAP_ROUTES.main has no /terms; Terms of Use is a Nessa surface, so a root /terms 404 is the design. The script stays read-only and credential-free.
8.0 KiB
The deploy line (FRE-1695)
Written 2026-11-09 by the CTO. Supersedes the implicit assumption in every earlier freno-dev landing report that "merged on Gitea" meant "live".
The defect
Production freno.me and its subdomains are built by Vercel from GitHub
mikefreno/freno-dev. Agents merge into Gitea Mike/freno-dev. The two
masters diverged at merge-base dd368d2:
| Line | Head at filing | Ahead of merge-base |
|---|---|---|
Gitea Mike/freno-dev master |
a8ce226b (2026-11-09 16:16Z) |
12 commits — FRE-371, FRE-366, FRE-202, FRE-997, FRE-1670 |
GitHub mikefreno/freno-dev master |
4e412ede (2026-11-09 15:40Z) |
2 commits — NOOK-1037, NOOK-1038 |
Every Gitea merge since dd368d2 was undeployed, including the FRE-371
client-IP fix and the whole Nessa /terms page. Measured on the live site at
2026-11-09T20:46Z: /nook/agents served Agent coverage (the GitHub line's
title) and /terms returned 404 (the Gitea line's page). No agent run can push
to GitHub — /paperclip/.git-credentials holds git.freno.me entries only — so
the divergence could not be closed from the agent side.
Decision
Gitea Mike/freno-dev is the canonical repository. GitHub
mikefreno/freno-dev is a mirror, and it is the tree Vercel builds.
One line, one direction: Gitea master → GitHub master → Vercel → live.
Why this shape and not the alternatives
- Vercel cannot build from Gitea. Vercel's git integrations are GitHub, GitLab and Bitbucket; a Gitea host is not a supported provider. So "point Vercel at the repo agents push to" is not a setting that exists. Even with a Gitea-hosted mirror on GitLab, the deploy repo would still be a mirror.
- Agents can only push to Gitea. The per-agent credential is Gitea-only
(
git.freno.me), and every review/CI path the company runs — the PR relay (FRE-653, FRE-1680), the--checkssweep (FRE-1364), the verdict rows — is Gitea-shaped. Making GitHub canonical would mean re-issuing GitHub credentials to every agent and rewiring all of that. Keeping Gitea canonical keeps the authoring side exactly as it is. - Deploying from CI instead of a mirror (Gitea Actions →
vercel --prod) is the third option and it is worse: freno-dev has no workflow files today, Gitea Actions cannot run in this installation (FRE-1267), and it would need a Vercel token on a runner. A mirror needs one credential, in one place, and no new CI.
Reconcile first, then mirror
The mirror must fast-forward. The two GitHub-only commits are sponsor-authored landing work that is live today (NOOK-1037 refresh, NOOK-1038 scoped-remote restore); they cannot be dropped. So the GitHub line is merged into Gitea master first:
git merge gh/master # merge commit, no history rewrite
After that merge, GitHub master is an ancestor of Gitea master, and a plain
git push (no --force) is a fast-forward. The mirror is configured without
force-push so that any GitHub-side drift fails loudly instead of being silently
overwritten.
Two conflicts were resolved in the merge, both copy contracts:
src/components/nook/FeatureBreakdowns.tsx— took NOOK-1038's refreshed island bullets and kept FRE-997's18 agentsbullet (the roster on /agents lists all eighteen;agents.test.tsasserts equality).src/routes/nook/remote-claims.test.ts— one file holding both contracts. NOOK-1038 (2026-11-09, Mike's correction) supersedes FRE-323's blanket remote hold: scoped one-click setup for Tailscale and authorized known hosts is a supported claim, blanket promises stay banned. FRE-997 (2026-11-04) stands: eighteen is publishable, stale thirteens stay banned. The only NOOK-1038 line dropped is its repetition of the older "no coverage count" clause, which FRE-997 superseded when it completed the roster.
How a merge becomes a deploy
- An agent merges a reviewed PR into Gitea
Mike/freno-devmaster(or Mike merges it in the Gitea UI). Nothing else changes for the engineer. - A Gitea push mirror on
Mike/freno-devpushesmastertohttps://github.com/mikefreno/freno-dev.gitwith sync-on-commit enabled. - Vercel sees the GitHub push and builds production from GitHub
master. scripts/verify-deploy.shconfirms it: same SHA on both remotes, and the live site serving copy that only exists on the reconciled line.
Who can trigger a deploy: anyone who can merge a PR into Gitea master — every engineer, plus Mike. No agent needs a GitHub credential and no human action is needed per deploy once the mirror exists. The manual fallback is a Vercel redeploy by Mike, which is only useful after step 2 has moved GitHub master.
Who must set it up once: the repo owner on Gitea (Mike — the agent token
has push but not admin, so an agent cannot create the mirror). The operator
runbook is on the FRE-1695 operator row; the short version is: create a
fine-grained GitHub token limited to mikefreno/freno-dev with contents
read/write, add it as the push mirror's credential in Gitea repo settings, tick
"Sync when commits are pushed", then run scripts/verify-deploy.sh.
Until that one-time step lands, GitHub master is behind and the site is stale — exactly as it is today.
Failure modes this deliberately keeps loud
- A commit made directly on GitHub master is not in Gitea. The mirror push
then rejects (non-fast-forward) and Gitea shows the mirror error. Do not commit
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.shbefore calling a site change published. - The first mirrored deploy switches on a cron that deletes production
rows.
vercel.jsoncarries 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 in1d9287a, FRE-369) is only on Gitea master: the first deploy that follows the mirror starts calling it, and the next 12:00 UTC runsDELETE 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 isANALYTICS_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 withAuthorization: Bearer <ANALYTICS_RETENTION_SECRET ?? CRON_SECRET>(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 suppliesCRON_SECRETto 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.
Verifying
scripts/verify-deploy.sh
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.