Skip to content

Deployment

Alokai CMS ships two workers per environmentcms (the dashboard + API) and alokai-cms-mcp (the AI tooling worker that the dashboard binds to). Both must be deployed in lockstep: MCP first, CMS second. We also apply pending D1 migrations against the target environment as part of each deploy.

There are two deployment paths:

  • GitHub Actions (preferred) — ci.yml runs build/test/lint on every PR and push to main, and on push-to-main it chains a deploy-staging job that deploys MCP + CMS to staging once tests pass. deploy-prod.yml is a manual workflow_dispatch flow gated by the production GitHub Environment’s required reviewer.
  • Manual local deploy (break-glass) — run yarn workspace cms deploy:staging / deploy:prod plus the MCP equivalents from a developer machine. Use this only when GitHub Actions is unavailable.

Prerequisites

Before either path will work, make sure:

  • Cloudflare resources exist for the target environment (D1, R2, KV, Durable Objects, MCP worker).
  • All required secrets are set on the target Worker.
  • For GitHub Actions: the staging and production Environments exist in repo settings, each holding CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, and VERDACCIO_AUTH_TOKEN.

Pre-deploy checklist

Run before triggering prod:

  • CI on main is green (build + test + lint).
  • Staging is at the same commit as main and the staging dashboard loads, login works, and a sample page renders.
  • If new D1 migrations exist, yarn workspace cms db:migrate:list:staging shows them as already applied (the staging deploy applies first).
  • No outstanding wrangler.jsonc schema changes need a backfill.
  • You know which version bump (patch / minor / major) you’ll pick for the prod deploy.

Stage deploy (automated)

ci.yml runs two jobs in sequence:

  1. build-test-lint — runs on every PR and every push to main.

    • Installs (yarn install --immutable) with the enterprise registry token.
    • Builds cms, @alokai/cms-api, @alokai/cms-mcp via Turbo.
    • Runs yarn turbo run test --filter=cms and yarn turbo run lint.
    • On a PR, the workflow stops here.
  2. deploy-staging — only runs on push to main (or a manual workflow_dispatch), and only after build-test-lint succeeds.

    • Auto-backfills d1_migrations if the staging DB has the schema applied but the wrangler migration tracker is empty (legacy runtime-migrated DB). See Migrations below.
    • Applies pending D1 migrations with wrangler d1 migrations apply alokai-cms-db-staging --remote --env staging.
    • Deploys MCP (alokai-cms-mcp-staging) so the CMS service binding resolves. Stamps a deploy message: GitHub Actions deploy by <actor> @ <sha> (<run-url>).
    • Deploys CMS (cms-staging) with the same message.
    • Smoke checks https://cms-staging.alokai-apps.com/api/health (5 retries, 5s apart). Job fails loudly if it never returns 200.

To rerun without a code change, click Run workflow on the CI action page in GitHub — workflow_dispatch re-triggers both jobs.

The deploy message lands in the Cloudflare dashboard’s Workers & Pages → cms-staging → Deployments list and in wrangler deployments list --env staging, so you can map any prod deployment back to the GitHub Actions run that produced it.

Production deploy (manual, gated)

deploy-prod.yml is workflow_dispatch only. To trigger it:

  1. Open Actions → Deploy to Production → Run workflow.
  2. Pick version_bump: patch (default), minor, or major.
  3. Click Run workflow. The job pauses until a member of the production GitHub Environment’s required-reviewers list approves.
  4. After approval the workflow:
    • Runs yarn turbo run test --filter=cms as a final gate.
    • Bumps apps/cms/package.json via a direct JSON rewrite (the sidebar badge and /api/health read this — it’s how we tell stale bundles from fresh). We don’t use npm version because npm 10+ in workspace mode does a peer-deps re-resolution that conflicts with @storefront-ui/react’s React-18 peer.
    • Builds, auto-backfills d1_migrations if needed (see Migrations), applies pending D1 migrations to prod, deploys MCP, deploys CMS — each deploy stamped with cms@X.Y.Z prod deploy by <actor> @ <sha> (<run-url>).
    • Smoke-checks https://cms.alokai-apps.com/api/health and asserts the deployed version field matches the bumped value.
    • Commits the version bump, tags cms-vX.Y.Z, pushes, and creates a GitHub Release with auto-generated notes.

If the smoke check fails, the workflow stops after the deploy itself completed but before the tag/release commit. See Rollback for what to do.

Manual local deploy (break-glass)

When GitHub Actions can’t run (outage, fork, ad-hoc hotfix from a laptop):

Terminal window
# Make sure you're authenticated to the right Cloudflare account.
npx wrangler whoami
# Build everything first.
yarn build:cms
# Apply pending migrations to the target environment.
yarn workspace cms db:migrate:staging # or db:migrate:prod
# Deploy MCP first, then CMS.
yarn workspace @alokai/cms-mcp deploy:staging # or deploy:prod
yarn workspace cms deploy:staging # or deploy:prod
# Smoke-check the new deployment.
curl -fsS https://cms-staging.alokai-apps.com/api/health

For prod, also run yarn workspace cms prepare:deployment:patch (or :minor / :major) before the build step so the version bump is recorded; otherwise the sidebar badge / /api/health won’t reflect the shipped change.

Post-deploy verification

After every deploy:

  • curl https://<env-host>/api/health → expect 200 and the version you just shipped.
  • Open the dashboard, log in, and load a sample page in the Page Builder.
  • Spot-check the published delivery API on a known page slug.
  • Watch wrangler tail --env <env> for ~1 minute for unexpected errors.

Rollback

The CMS uses Cloudflare Workers’ built-in deployment history.

Terminal window
# List recent deployments for the target worker.
npx wrangler deployments list --env staging # omit --env for prod
# Roll back to the previous deployment.
npx wrangler rollback --message "rolling back: <reason>" --env staging

Repeat for the MCP worker if its rollback is needed:

Terminal window
cd packages/cms-mcp
npx wrangler rollback --message "rolling back MCP: <reason>" --env staging

Migrations

Schema lives in apps/cms/migrations/*.sql, applied by wrangler d1 migrations apply. The wrangler d1_migrations table on each D1 instance records which files have been applied.

Why we have an auto-backfill step. The CMS originally used a runtime self-heal in apps/cms/src/server/migrate.ts that materialized the schema on first request. Production and staging DBs that pre-date the wrangler-tracked convention have the full schema but an empty d1_migrations table — so a naive wrangler d1 migrations apply re-runs all 35 historical files and trips “duplicate column” on the first ALTER TABLE ADD COLUMN (SQLite has no IF NOT EXISTS for ADD COLUMN, so individual migration files cannot be made idempotent in pure SQL).

The deploy workflows handle this automatically:

Terminal window
# Pseudocode of the step:
if "organizations" exists AND "d1_migrations" does not exist:
wrangler d1 execute --file=scripts/backfill-d1-migrations.sql
else:
skip # tracker is in sync, or DB is fresh

apps/cms/scripts/backfill-d1-migrations.sql does INSERT OR IGNORE INTO d1_migrations (name) VALUES ('001_initial.sql'), ... for all 35 file names. Idempotent on every dimension — both the detection and the SQL.

Adding a new migration:

  1. Create the next file in order: apps/cms/migrations/036_my_change.sql.
  2. Use idempotent statements where SQLite supports it (CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS, INSERT OR IGNORE). For ALTER ADD COLUMN and other non-idempotent DDL, the d1_migrations tracker is the safety net.
  3. Add the filename to apps/cms/scripts/backfill-d1-migrations.sql’s list only if existing prod DBs already have the corresponding schema applied via runtime self-heal. New columns added post-cutover don’t need backfill entries.
  4. Apply locally with yarn workspace cms db:migrate.

Manual backfill (if you ever need to run it by hand for a fresh environment standup):

Terminal window
yarn workspace cms db:migrate:backfill:staging # or :prod
yarn workspace cms db:migrate:list:staging # confirm 35 ✅
yarn workspace cms db:migrate:staging # "No migrations to apply!"

What gets deployed

  • CMS Workerapps/cms/src/server/index.ts bundled by Wrangler.
  • CMS Static assetsapps/cms/dist/ (the React SPA) uploaded to Cloudflare’s asset storage and served via the STATIC_ASSETS binding. The Worker serves API routes under /api/* and falls back to index.html for everything else.
  • MCP Workerpackages/cms-mcp/bin/worker.mjs bundled by Wrangler.

Continuous deployment for documentation

The two Astro Starlight sites (docs/usage-docs, docs/dev-docs) deploy automatically to Cloudflare Pages, mirroring the worker’s PR → staging → production lifecycle:

TriggerPages branchURL pattern
PR opens / pushespr-<n>-<branch>https://pr-12-foo.alokai-cms-dev-docs.pages.dev (auto)
Push to mainstaginghttps://staging.alokai-cms-dev-docs.pages.dev
Manual Deploy to Production workflowmainhttps://cms-devs.alokai-apps.com (custom domain)

All three flows are paths-filtered: if docs/usage-docs/** and docs/dev-docs/** are unchanged versus the relevant baseline (the PR base, the previous main commit, or the previous cms-v* tag), the deploy step skips entirely so the Cloudflare Pages dashboard doesn’t fill up with no-op deployments.

On PRs, the workflow comments back with the unique preview URLs so reviewers can click through. The branch slug is sanitized to Cloudflare’s allowed character set and capped to 28 chars.

Manual local fallback (still works when CI is unavailable):

Terminal window
yarn deploy:usage-docs # → alokai-cms-docs project, --branch main
yarn deploy:dev-docs # → alokai-cms-dev-docs project, --branch main