petrova-host production deploys move to the deploy hook
Date: 2026-08-31 Status: closed Supersedes: none Superseded-by: none — current
Context
Section titled “Context”petrova-host is the single largest line in the Vercel account: 23h52m of build CPU in the
Aug 20–31 cycle, 29.7% of the whole. The cause is volume, not waste. main took 265
non-merge commits in eleven days — most of them hourly chore(state): scheduled sweep pushes
from petrova-bot — and each one triggers a full production build that installs three times,
compiles the CLI, host and dashboard, and copies the staged trees.
A path guard does not help: only 4 of those 265 commits (1.5%) touch none of its build inputs,
because scripts/vercel-build.sh bakes state/, docs/, contracts/, spec/, core/ and
registry.yaml into the function bundle. That guard shipped today anyway, for correctness
(2026-08-31-the-host-ignore-step-skipped-on-author.md), and it is not a cost measure.
The remaining lever is deployment frequency, and this repo already owns the mechanism.
.github/workflows/deploy-batch.yml flushes drifted projects once a day at 08:00 UTC through
per-project VERCEL_DEPLOY_HOOK_<PROJECT> secrets, and its header states the intent: “Vercel
frontends have prod auto-deploy disabled; merges accumulate as deploy drift … Clean projects
are skipped — no drift, no build, no build minutes.”
petrova-host was never enrolled, and neither was anything else: the repo held no
VERCEL_DEPLOY_HOOK_* secret at all, so the daily flush has been reporting-and-skipping every
project since it was written. The estate believed it batched. It did not.
Decision
Section titled “Decision”Disable git-triggered production deployments for main on petrova-host and deploy it through
the hook instead. "git": {"deploymentEnabled": {"main": false}} lands in vercel.json rather
than in the project-settings UI, so the rule is reviewable in a diff — the same reasoning that
put ignoreCommand there.
deploymentEnabled is an allow-map whose unlisted branches default to enabled, so listing
main: false disables exactly one branch and leaves preview deployments untouched. Previews
remain the pre-production compile for this surface and are guarded by the fork-point predicate
shipped earlier today.
A deploy hook named deploy-batch (governed daily flush) was created on main and stored as
VERCEL_DEPLOY_HOOK_PETROVA_HOST. It was fired once before this change to prove the path:
deployment dpl_EusMybsAGdcRDurz3tCLZXyzbtvs, target production, state READY, carrying
deployHookId: Zx04cELi60.
Known gap, accepted deliberately
Section titled “Known gap, accepted deliberately”The daily batch will not yet pick this project up automatically. hubble’s
/api/deploy-drift computes drift from the GitHub compare API and is returning 401 on every
private repository — 35 of the 71 projects it tracks, petrova-host among them, report
status: "unknown" rather than "drift". deploy-batch.yml only POSTs a hook for
status == "drift", so until that credential is repaired the daily run will pass over this
project in silence. The compare call succeeds with a valid token, so the fault is hubble’s
credential and not the repository.
The operator was shown this and chose to proceed. Until hubble is fixed, production deploys of
petrova-host are a manual act: run deploy-batch via workflow_dispatch with
only_project: petrova-host, or fire the hook directly. Merging to main will no longer
publish, and nothing will announce that.
That is the whole risk of this decision and it is stated here rather than discovered later: a surface that has stopped deploying looks exactly like a surface with nothing to deploy.
Alternatives considered
Section titled “Alternatives considered”- Fix hubble’s token first, then flip — the safer order, and it would have repaired drift for all 35 private-repo projects at once. Not chosen; the operator took the cost reduction now.
- Batch the
state/sweep bot instead — leaves auto-deploy alone and cuts the same volume at source, but edits a governed workflow to solve a deploy-path problem. - Do nothing — leaves 29.7% of the account’s build minutes on hourly generated commits.
Consequences
Section titled “Consequences”- Production builds for this project fall from roughly 24 a day to at most one, and to zero on days nobody runs the batch.
- The site can lag
mainby up to a day by design, and further while the drift gap is open. - Reverting is one line: remove the
gitblock fromvercel.json. - The next honest number comes off the usage dashboard at the end of the cycle. Nothing is claimed here beyond the mechanism being proven.
Verification
Section titled “Verification”- Hook proven before the flip —
dpl_EusMybsAGdcRDurz3tCLZXyzbtvs, production, READY. - After this merges: fire the hook once and confirm a new production deployment appears, which
proves
deploymentEnabled: falsedoes not block hook-triggered builds. - Then confirm the next ordinary push to
mainproduces no production deployment.
Re-verified 2026-09-04
Section titled “Re-verified 2026-09-04”dpl_EusMybsAGdcRDurz3tCLZXyzbtvs confirmed live via the Vercel API: deployHookId: Zx04cELi60, deployHookName: "deploy-batch (governed daily flush)", target: production,
readyState: READY. The mechanism is real and fires. vercel.json’s deploymentEnabled.main: false still holds at HEAD, and no main-sourced deployment appears in the last 20 production
deployments (spanning back to 2026-08-27) — criterion 3 holds.
The “Known gap, accepted deliberately” risk is now materialized, not hypothetical: zero
hook-fired deployments since 2026-08-31, four days idle, because hubble’s /api/deploy-drift
401 on private repos was never repaired. This is the exact outcome this doc predicted in
advance and accepted the cost of, not a new defect — closing on that basis rather than
reopening.
Named plainly: the hook is called deploy-batch (governed daily flush) and it has fired
once, ever. One historical deployment carries deployHookId; zero since. A job named
daily that has run once is a dead cron by any other name — the same shape as CAIRNET,
gate.yml, sync-docs and .panto/ in this estate’s recent history. Signing this doc on
“the mechanism can fire” is correct; leaving “the daily flush isn’t daily” unrecorded is not.
Belongs in whatever alerting scope this estate builds next: **no deploy-batch flush in
48h** is a cheap, checkable rule and it is the exact silence this doc named in advance.
Sign-off
Section titled “Sign-off”- subagent: claude-sonnet-5, re-verification above
- human: alex@devarno.com