Commit & Deploy Policy

How code goes from a local edit to live in production, and the rules around it. The guiding principle: production must always be reachable from main — what's deployed should equal what's committed.


Golden rules

  1. Commit only when asked. Don't auto-commit work in progress.
  2. New commits, never --amend (unless explicitly asked) — a failed hook means the commit didn't happen; amending would rewrite the previous one.
  3. Add specific files by name, not git add -A/. — avoids sweeping in .env.local, data dumps, or secrets.
  4. Never commit secrets. .env.local is gitignored and stays that way.
  5. Every commit ends with the Co-Authored-By: … trailer when AI-assisted.
  6. Deploy ships what's in the worker tree (bundled by esbuild), so the worker tree must be committed before a (non-forced) deploy — see the gate.

The Worker deploy — scripts/deploy-worker.sh

This is the only way the Worker ships. Never run raw wrangler deploy.

scripts/deploy-worker.sh            # both accounts (default)
scripts/deploy-worker.sh --fcr      # production only (FCR Media)
scripts/deploy-worker.sh --cd       # personal dev only
scripts/deploy-worker.sh --force    # deploy a dirty tree (emergencies only)

Preflight gates (why deploys are safe):

  1. Clean-tree gate — refuses to deploy if anything under worker/ is uncommitted. (Scoped to worker/ only — dirty root-level docs/data don't block.) --force bypasses but then "you owe a commit."
  2. no-undef lint gate — runs eslint with scripts/eslint-no-undef.config.mjs over worker/src/. Catches the classic trap: a free variable referenced in a tool branch that the outer try/catch would swallow into {"error":…}. Two such bugs shipped on 2026-05-17 before this gate existed.

Deploy targets:

  • FCR Media (production): wrangler deploy --env fcr with CF_API_TOKEN_FCR.
  • Personal (dev): wrangler deploy --env="" with CF_API_TOKEN_CD and CLOUDFLARE_ACCOUNT_ID (the worker name exists on both accounts — without the account ID, wrangler resolves to FCR and the CD token 401s).

Both tokens are read from .env.local at the repo root.

Postflight: tags every non-forced deploy deploy/worker/<UTCstamp>-<sha> so each deployed state is a named ref. --force skips the tag (the SHA wouldn't reflect what shipped). Tags are local until git push --tags.

Recurring footnote in this repo: the deploy gate frequently catches unrelated in-progress edits under worker/ (e.g. someone else's WIP). The clean way past is to git stash push <file> the unrelated change, deploy, then git stash pop — don't --force past it.


The UI deploy (Cloudflare Pages)

The web UI is a separate Pages project — not shipped by deploy-worker.sh.

npm run build                 # vite build → dist/
CLOUDFLARE_API_TOKEN=$CF_API_TOKEN_FCR \
  npx wrangler pages deploy dist --project-name=fcr-dashboard-ui --branch=production
  • Project name is fcr-dashboard-ui (not fcr-dashboard).
  • Must deploy to --branch=production to update the live site.
  • Uses CF_API_TOKEN_FCR (production account).

The docs micro-site deploy (Cloudflare Pages)

The architecture/onboarding docs are also published as a shareable HTML site at https://fcr-dashboard-docs.pages.dev (rendered from docs/ — humans read HTML, not markdown). Rebuild + redeploy after editing any doc:

node scripts/build-docs-site.mjs            # docs/*.md → docs-site/*.html
CLOUDFLARE_API_TOKEN=$CF_API_TOKEN_FCR CLOUDFLARE_ACCOUNT_ID=0481b38f86c96053c528ec08d7d7dddc \
  npx wrangler pages deploy docs-site --project-name=fcr-dashboard-docs --branch=production --commit-dirty=true
  • The curated page set + order is the PAGES array in scripts/build-docs-site.mjs — add a doc there to include it.
  • docs-site/ is generated (gitignored); only the build script is committed.

The HubSpot card deploy

The Prospect Advisor card is a HubSpot UI extension, deployed to HubSpot (not CF):

cd hubspot-extension
hs project upload          # builds + uploads to HubSpot

Note: card source includes a gitignored config.local.ts holding the Worker API key — copy it from the example file before first upload.


Rollback

  • Worker: git checkout <deploy/worker/...-sha>scripts/deploy-worker.sh, or roll back to a previous version in the CF dashboard (Workers → Deployments). Every deploy is tagged, so any prior state is reachable.
  • Pages: re-deploy a previous build, or use Pages → Deployments → rollback.

Two Cloudflare accounts

Account Worker Used for
Production FCR Media (0481b38f…) …fcrmedia.workers.dev live — Roam, HubSpot card, UI
Dev Personal (1ceb8928…) …cathaldempsey.workers.dev testing before prod

Both carry the same bindings + secrets. deploy-worker.sh (no flag) ships to both; use --fcr to ship prod only.


Typical change → live flow

1. Edit worker/src/handlers/<x>.js  (+ import in index.js if new route)
2. git add <those files>;  git commit -m "feat(...): …  Co-Authored-By: …"
3. scripts/deploy-worker.sh            # gates: clean tree + lint; ships both; tags
4. git push origin main;  git push --tags   # keep GitHub == prod
5. smoke-test the route (curl with x-api-key)

FCR Dashboard documentation · generated from docs/ · keep counts verified, not guessed.