Docs / ci-cd.md · mirrored from the repo
Namaste Ji — CI/CD & deploy strategy
Status: design + initial setup. How a growing monorepo builds, tests, and deploys many independent targets on Cloudflare — without rebuilding/redeploying everything on every push. Aligns with the documented GitHub Actions + Alchemy IaC direction (BACK-OFFICE.md).
The problem
Cloudflare Pages’ native Git integration rebuilds + redeploys on every push to the
production branch, regardless of what changed. In a monorepo with many deployables
(consumer app, back-office console, Workers/services, the brand site, …) that is wasteful
and wrong: a push to services/api must not redeploy the brand page.
Principles
- CI is the deployer — not the platform’s Git integration. All deploys run through GitHub Actions → Wrangler. Pages projects are Direct Upload (Git auto-build OFF), so CI is the single, path-scoped deploy path — uniform across Pages and Workers.
- Deploy only what changed. Each target deploys only when its paths change
(
paths:filters now → Turborepo--affectedonce JS/TS apps land). - One target = one deploy unit, independently versioned and deployed.
- Environments + previews. PRs → preview deploys (per-PR URLs);
main→ production; environment protection / manual approval where it matters. - Resources as code (IaC). D1 / R2 / KV / Vectorize / Pages projects + bindings are declared via Alchemy, not click-ops — reproducible across staging/prod.
The model (look-ahead)
push / PR
└─ GitHub Actions
├─ detect changed targets (paths: filters now → Turbo `--affected` at scale)
└─ per affected target: build + test (Turbo cache) → wrangler deploy
• apps/web → consumer app (Pages/Workers)
• apps/console → back office (Pages/Workers)
• apps/brand → brand book (Pages) ← first instance
• services/* → Workers
└─ Alchemy reconciles infra (resources + bindings) per environment
- Turborepo (when the JS monorepo exists):
turbo run build test deploy --affected+ remote cache → only affected packages run; stays fast as the repo grows. cloudflare/wrangler-actiondeploys each target; secrets live in GitHub Actions (CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID).- Per-target workflows (or one matrix workflow keyed on changed paths) keep triggers explicit and scoped.
First instance — the brand site (set up now)
- Deploys via
.github/workflows/deploy-brand.yml, triggered only whenindex.htmlordocs/brand/**change (plus manualworkflow_dispatch). A push touching anything else does not redeploy it. - The Pages project is Direct Upload (created by
wrangler pages project create) — no Git-connected auto-build. - Gated by Cloudflare Access (email policy); page is
noindex.
One-time setup (manual — account-gated)
- CF dashboard → My Profile → API Tokens → create a token with Pages: Edit (and
Account → read). Add it to GitHub repo → Settings → Secrets → Actions as
CLOUDFLARE_API_TOKEN. - Add
CLOUDFLARE_ACCOUNT_ID(CF dashboard → account ID) as a second secret. - Trigger the workflow (push a brand change, or run it via Actions → Deploy brand site →
Run workflow). First run creates the
namaste-ji-brandPages project and deploys. - Zero Trust → Access → Applications → add the email policy to the
*.pages.devdomain.
Do not connect the Pages project to Git in the dashboard — CI is the only deployer.
Implemented — the console (back office)
Three workflows realize the BACK-OFFICE.md §11 split (staging automatic, prod manual/gated):
deploy-console.yml— staging, on merge tomain(pathsapps/console/**,services/console-api/**,packages/**) +workflow_dispatch. Builds the UI same-origin (noVITE_API_TARGET→ relative/api) to thenamaste-ji-console-stagingPages project, and the Worker viawrangler deploy --env staging. The UI servesbo-staging.namasteji.org/*, the Worker the/api/*route — one origin, no CORS, whole host behind Cloudflare Access.deploy-console-prod.yml— production,workflow_dispatchonly, gated by aconfirminput + theproductionGitHub Environment (set required reviewers under repo Settings → Environments). Deploys the prod Worker +namaste-ji-consolePages project.infra.yml— Alchemy resource provisioning (workflow_dispatch).
Runtime notes:
- CI
CLOUDFLARE_API_TOKENneeds Workers + Pages + Secrets Store scope (the staging Worker has asecrets_store_secretsbinding). Set GitHub secrets viagh secret set NAME --body "$VALUE"— a value piped over stdin can carry a trailing newline → wrangler6111 Invalid Authorization header. - Console deploys run on Node 22 (wrangler v4 requires Node ≥22).
- SSL (free plan): Universal SSL covers only root + first-level subdomains, so BO hosts stay
first-level (
bo-staging,bo); a second-level host can’t get a free edge cert. - Secrets: Langfuse keys live in Cloudflare Secrets Store (BO §7), bound via
secrets_store_secrets— no per-worker secret injection in CI.
Decisions
| # | Decision | Rationale |
|---|---|---|
| CI-1 | CI-as-deployer (GitHub Actions + Wrangler); Pages on Direct Upload | Uniform, path-scoped control across Pages + Workers; no every-push rebuilds |
| CI-2 | Path/affected-scoped deploys (paths: now → Turbo --affected later) | A push only deploys the targets it actually changed |
| CI-3 | Per-target deploy units; PR previews + prod on main; env protection | Independent, safe, reviewable deploys |
| CI-4 | Alchemy IaC owns CF resources + bindings per environment | Reproducible infra, no click-ops |
| CI-5 | Console: merge→staging auto-deploy; prod is manual + GitHub-Environment-gated | Matches BO §11; staging stays current, prod ships deliberately |
| CI-6 | BO hosts are first-level subdomains (bo-staging, bo) | Free Universal SSL covers root + one level only; second-level needs paid ACM |
Open
- Adopt Turborepo + workspaces when the first JS/TS app lands; move the brand site to
apps/brand/and update the path filter. - Turbo remote cache backend (CF R2/Workers vs Turbo’s hosted).
- Migrate the brand Pages-project creation from the
wranglerbootstrap step to Alchemy. - Add PR preview deploys (branch deploys) per target once previews are wanted.