Commit & Deploy
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
Section titled “Golden rules”- Commit only when asked. Don’t auto-commit work in progress.
- New commits, never
--amend(unless explicitly asked) — a failed hook means the commit didn’t happen; amending would rewrite the previous one. - Add specific files by name, not
git add -A/.— avoids sweeping in.env.local, data dumps, or secrets. - Never commit secrets.
.env.localis gitignored and stays that way. - Every commit ends with the
Co-Authored-By: …trailer when AI-assisted. - 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.
Ship everything — scripts/ship.sh
Section titled “Ship everything — scripts/ship.sh”The recommended entrypoint when you’ve changed code, docs, or both. A bare
git push does not update the live docs site (docs-site/ is a gitignored
build artifact), and the Worker is a separate deploy — so shipping used to mean
remembering two scripts in the right order. ship.sh detects what changed since
the last deploy and routes automatically:
scripts/ship.sh # detect + deploy whatever changedscripts/ship.sh --dry-run # print the plan, change nothingscripts/ship.sh -m "message" # commit the in-scope changes first, then deployscripts/ship.sh --docs-only | --worker-only- worker/ (incl.
wrangler.toml) changed since the lastdeploy/worker/*tag → runsdeploy-worker.sh. - a published doc source (read from the built
docs-site/sources.json), anySKILL.md, orscripts/build-docs-site.mjschanged since the lastdeploy/docs/*tag → runsdeploy-docs.sh(build → Pages → reindex → tag → push). - Worker ships first (the docs reindex calls the worker), then docs.
- It does not auto-commit unless you pass
-m; the underlying scripts keep their own clean-tree gates, tags, and pushes.--force/--no-pushpass through to both;--fcr/--cdto the worker;--no-reindexto docs.
The two scripts below are still callable directly (and are what ship.sh calls).
The Worker deploy — scripts/deploy-worker.sh
Section titled “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 onlyscripts/deploy-worker.sh --force # deploy a dirty tree (emergencies only)Preflight gates (why deploys are safe):
- Clean-tree gate — refuses to deploy if anything under
worker/is uncommitted. (Scoped toworker/only — dirty root-level docs/data don’t block.)--forcebypasses but then “you owe a commit.” no-undeflint gate — runseslintwithscripts/eslint-no-undef.config.mjsoverworker/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 fcrwithCF_API_TOKEN_FCR. - Personal (dev):
wrangler deploy --env=""withCF_API_TOKEN_CDandCLOUDFLARE_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 togit stash push <file>the unrelated change, deploy, thengit stash pop— don’t--forcepast it.
The UI deploy (Cloudflare Pages)
Section titled “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(notfcr-dashboard). - Must deploy to
--branch=productionto update the live site. - Uses
CF_API_TOKEN_FCR(production account). - Live at the apex
https://fcrinsights.ie/dashboard.html(andfcr-dashboard-ui.pages.dev). - The project is NOT GitHub-connected (
source: null) — agit pushdoes not deploy the UI. You must run the build +wrangler pages deployabove (ornpm run deploy) every time.
The docs micro-site deploy — scripts/deploy-docs.sh
Section titled “The docs micro-site deploy — scripts/deploy-docs.sh”The architecture/onboarding docs are published as a shareable HTML site at
https://fcr-dashboard-docs.pages.dev (rendered from docs/ — humans read
HTML, not markdown), and that site has a RAG search bot backed by the
fcr-internal-docs Vectorize index. Those three must stay in sync — the
committed markdown, the published HTML, and the index — so deploy them as one
gated step:
scripts/deploy-docs.sh # build → deploy Pages → reindex (+ tag)scripts/deploy-docs.sh --no-reindex # build → deploy only (skip the index)scripts/deploy-docs.sh --force # deploy a dirty doc tree (emergencies)What it does, in order:
- Clean-tree gate — refuses if
docs/orscripts/build-docs-site.mjsis uncommitted (so prod docs == main).--forcebypasses. (Externally referencedsrcfiles — e.g.data/keyword-intelligence/*, root*.mdpulled in via theNAVset — aren’t gated; commit those too.) - Build —
node scripts/build-docs-site.mjs→docs-site/*.htmlplusdocs-site/search-manifest.json(the page list the reindexer reads). - Deploy —
wrangler pages deploy docs-sitetofcr-dashboard-docs,--branch=production, withCF_API_TOKEN_FCR+ the FCR account ID. - Reindex — waits for the production deploy to be reachable, then
POST /dashboard-docs-reindex(with the workerx-api-key) so the search bot reflects the new content. The index does not auto-update otherwise. - Tag —
deploy/docs/<UTCstamp>-<sha>(skipped on--force).
- The curated page set + order is the
NAVtree inscripts/build-docs-site.mjs— add a doc there to include it (and it’s auto-picked up by the reindex). docs-site/is generated (gitignored); only the build script is committed.- The search bot itself (the
dashboard-docs-searchRAG route + the “Ask the docs” widget) ships with the worker + the build script — seeworker/src/handlers/docs-search.js/docs-reindex.js.
The HubSpot card deploy
Section titled “The HubSpot card deploy”The Prospect Advisor card is a HubSpot UI extension, deployed to HubSpot (not CF):
cd hubspot-extensionhs project upload # builds + uploads to HubSpotNote: card source includes a gitignored
config.local.tsholding the Worker API key — copy it from the example file before first upload.
Rollback
Section titled “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
Section titled “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
Section titled “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/ship.sh # detects worker+docs changes, deploys both, tags + pushes4. smoke-test the route (curl with x-api-key)Prefer
scripts/ship.sh— it routes todeploy-worker.shand/ordeploy-docs.shand pushes the branch + deploy tag, so GitHub never lags live. Run the individual scripts directly only when you want one surface in isolation.