Skip to content

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.


  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 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:

Terminal window
scripts/ship.sh # detect + deploy whatever changed
scripts/ship.sh --dry-run # print the plan, change nothing
scripts/ship.sh -m "message" # commit the in-scope changes first, then deploy
scripts/ship.sh --docs-only | --worker-only
  • worker/ (incl. wrangler.toml) changed since the last deploy/worker/* tag → runs deploy-worker.sh.
  • a published doc source (read from the built docs-site/sources.json), any SKILL.md, or scripts/build-docs-site.mjs changed since the last deploy/docs/* tag → runs deploy-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-push pass through to both; --fcr/--cd to the worker; --no-reindex to 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.

Terminal window
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 web UI is a separate Pages project — not shipped by deploy-worker.sh.

Terminal window
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).
  • Live at the apex https://fcrinsights.ie/dashboard.html (and fcr-dashboard-ui.pages.dev).
  • The project is NOT GitHub-connected (source: null) — a git push does not deploy the UI. You must run the build + wrangler pages deploy above (or npm 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:

Terminal window
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:

  1. Clean-tree gate — refuses if docs/ or scripts/build-docs-site.mjs is uncommitted (so prod docs == main). --force bypasses. (Externally referenced src files — e.g. data/keyword-intelligence/*, root *.md pulled in via the NAV set — aren’t gated; commit those too.)
  2. Buildnode scripts/build-docs-site.mjsdocs-site/*.html plus docs-site/search-manifest.json (the page list the reindexer reads).
  3. Deploywrangler pages deploy docs-site to fcr-dashboard-docs, --branch=production, with CF_API_TOKEN_FCR + the FCR account ID.
  4. Reindex — waits for the production deploy to be reachable, then POST /dashboard-docs-reindex (with the worker x-api-key) so the search bot reflects the new content. The index does not auto-update otherwise.
  5. Tagdeploy/docs/<UTCstamp>-<sha> (skipped on --force).
  • The curated page set + order is the NAV tree in scripts/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-search RAG route + the “Ask the docs” widget) ships with the worker + the build script — see worker/src/handlers/docs-search.js / docs-reindex.js.

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

Terminal window
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.


  • 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.

AccountWorkerUsed for
ProductionFCR Media (0481b38f…)…fcrmedia.workers.devlive — Roam, HubSpot card, UI
DevPersonal (1ceb8928…)…cathaldempsey.workers.devtesting before prod

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


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 + pushes
4. smoke-test the route (curl with x-api-key)

Prefer scripts/ship.sh — it routes to deploy-worker.sh and/or deploy-docs.sh and pushes the branch + deploy tag, so GitHub never lags live. Run the individual scripts directly only when you want one surface in isolation.

Ask the docsRAG over this site
Ask anything about the FCR Dashboard platform — architecture, BigQuery, the worker routes, billing rules, the LRC stack, scoring… Answers are grounded in this documentation, with source links.
How does the deal-brief refresh work? Which routes are Worker vs n8n? How is account health scored?