Skip to content

CI/CD Deployment

Purpose

Both deployment paths in full - what actually happens, in what order, and the exact gaps between them.

Verifying whether the README's "manual deployment" claim still holds

It no longer needs to be verified against a stale claim - README.md has been corrected. It previously said: "The deployment workflow is available under .github/workflows/deploy-cloudflare.yml and is intentionally manual until Cloudflare repository secrets are configured." Reading .github/workflows/ci.yml directly shows a second, automatic job:

deploy-production:
  needs: validate
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'

This runs on every push to main that passes validate - no manual trigger, no approval gate. docs/DEPLOYMENT.md (the pre-existing deployment runbook) already documented this automatic pipeline in full detail and did not repeat the README's "manual" framing. As of fix/platform-hardening, README.md itself now describes this automatic pipeline accurately too.

Path 1: automatic (ci.yml's deploy-production)

Trigger: push to main (after validate passes).

feature branch --(PR)--> main --(push)--> deploy-production

Order (each step's failure stops everything after it - see Operations → Rollback for how to read a failure):

Apply D1 migrations  ->  Deploy API  ->  Deploy Corporate  ->  Deploy Software  ->
Deploy Logistics  ->  Deploy Commerce  ->  Deploy Embroidery  ->  Deploy Admin  ->
Production smoke test

Not included: Investors. See "Coverage gaps" below.

Concurrency: group: akshaya-production, cancel-in-progress: false - a second push while a deploy is running queues behind it rather than cancelling or racing it.

Path 2: manual (deploy-cloudflare.yml, workflow_dispatch)

Trigger: Actions tab → "Deploy Cloudflare" → Run workflow (human-initiated only).

Two independent jobs:

build-and-deploy-web (matrix, fail-fast: false):
  Corporate, Software, Logistics, Commerce, Embroidery, Admin, Investors
  - each: npm install -> npm run build -w <workspace> -> wrangler deploy

deploy-api:
  npm install
  -> inject CLOUDFLARE_D1_DATABASE_ID into workers/api/wrangler.jsonc via sed
     (see "The sed step is a no-op today" below)
  -> wrangler d1 migrations apply --remote
  -> wrangler deploy --config workers/api/wrangler.jsonc

Admin and Embroidery were added to this matrix in fix/platform-hardening, resolving the gap that used to exist here (see "Coverage gaps" below for its prior state). Does not run the production smoke test - run npm run smoke:production yourself afterward if you use this path.

Concurrency: same akshaya-production group as Path 1 - queues behind an in-flight automatic deploy rather than racing it.

Coverage gaps (real, documented, not silently "fixed")

App Automatic (ci.yml) Manual (deploy-cloudflare.yml)
Corporate, Software, Logistics, Commerce, Embroidery, Admin
Investors ❌ (out of scope / inactive) ✅ (pre-existing, unchanged)

Before fix/platform-hardening, Admin and Embroidery were missing from the manual path's matrix - docs/DEPLOYMENT.md §3 flagged this explicitly and deliberately did not "fix" it by expanding the matrix, since it might have been intentional. That gap has since been confirmed accidental and resolved by adding both to the matrix. The one gap left standing is Investors, which is a deliberate, scope-driven asymmetry (see Architecture → Applications "Investors is the outlier"), not an oversight - its pre-existing manual-deploy entry was left as-is rather than removed, since removing it would be a separate, deliberate change to existing production deploy capability.

The sed step is a no-op today

deploy-cloudflare.yml's deploy-api job runs sed -i "s/REPLACE_AFTER_D1_CREATE/${CLOUDFLARE_D1_DATABASE_ID}/g" workers/api/wrangler.jsonc before deploying - but workers/api/wrangler.jsonc already has a real database_id committed and no longer contains that placeholder anywhere. Confirmed by direct read. docs/DEPLOYMENT.md §2 already documents this precisely; left as-is here too, since deciding whether to remove the step (and the CLOUDFLARE_D1_DATABASE_ID secret) is a deliberate call for whoever owns this workflow, not a drive-by cleanup this portal should make.

Required secrets

See Secrets for the full GitHub-secret-vs-Worker-secret distinction.

Secret Used by
CLOUDFLARE_API_TOKEN Both workflows, every wrangler call
CLOUDFLARE_ACCOUNT_ID Both workflows, every wrangler call
CLOUDFLARE_D1_DATABASE_ID deploy-cloudflare.yml's deploy-api only (see "no-op" above)

Database / R2 requirements

D1 migrations are applied before any app/API deploy in both paths - by the time new code that expects a new column/table is live, the schema change has already landed. No R2-specific provisioning step exists in either workflow - the bucket is assumed to already exist (created once via scripts/cloudflare-bootstrap.sh, see Infrastructure → Cloudflare).

Expected outputs

A successful deploy-production run: 8 green steps (7 deploys + smoke test), each step's log showing a successful wrangler deploy/migration apply. The smoke test step's own output lists each of the 7 endpoints with [PASS]/[FAIL] and a reason per endpoint.

Rollback

See Operations → Rollback - no automated rollback exists, by design; docs/DEPLOYMENT.md §5 has the full manual procedure.