Skip to content

Cloudflare Pages V2 Dev Deploy Runbook

Status: existing Pages projects verified; current HOCTAPAZ v2 production-candidate release remains blocked until backend/Workers acceptance passes

Source Of Truth

Manifest:

txt
deploy/cloudflare/pages.frontend.json

Deploy script:

txt
scripts/deploy/cloudflare-pages.mjs

The deployment method is Cloudflare Pages Direct Upload with Wrangler:

bash
npx wrangler pages deploy <DIRECTORY> --project-name <PROJECT_NAME> --branch master

Official references:

  • Cloudflare Pages Direct Upload: https://developers.cloudflare.com/pages/get-started/direct-upload/
  • Cloudflare Pages Git integration: https://developers.cloudflare.com/pages/configuration/git-integration/
  • Cloudflare Pages custom domains: https://developers.cloudflare.com/pages/configuration/custom-domains/
  • Cloudflare Pages REST API: https://developers.cloudflare.com/pages/configuration/api/
  • Cloudflare Pages monorepos: https://developers.cloudflare.com/pages/configuration/monorepos/

This runbook keeps Direct Upload because all seven existing v2-dev projects were created in that mode. Cloudflare does not support converting an existing Direct Upload project to native Git integration. Therefore the dashboard's No Git connection state is expected: repository automation is provided by GitHub Actions plus Wrangler, not by Pages native Git builds.

Do not delete and recreate these projects merely to obtain a native Git connection. That operation would discard the deployment history used by the rollback contract, require every custom domain to be reattached, and can cause production-candidate downtime. A future native-Git migration requires a separate approved migration plan with replacement projects, domain cutover, rollback evidence, and Cloudflare's per-repository project limits reviewed at that time.

GitHub Actions Direct Upload is the only deployment authority for these existing projects. Keep native Pages Git builds disconnected to avoid two independent publishers racing the rollback/provenance contract. The cloudflare-pages.yml workflow is check-only; only hoctapaz-v2-production.yml may upload production Pages artifacts. Each production dispatch must provide the exact reviewed 40-character commit in approved_release_sha; the workflow rejects a value that differs from GITHUB_SHA before accessing runtime deployment credentials. When deploy=true, the dispatcher must also type DEPLOY_HOCTAPAZ_V2_PRODUCTION. This exact-SHA confirmation is the current fail-closed authorization contract; do not claim a human reviewer approval unless the live GitHub Environment protection rules actually enforce one. Production capture and every pre-deploy project read fail closed if Cloudflare returns a non-null Git source, even when its automatic deploy toggles appear disabled. The first-six rollout also requires an existing successful canonical production deployment for every project, so downstream failure always has a real Pages baseline to restore.

After the six non-Admin custom domains pass Chrome acceptance, the same production workflow records the Admin Pages activation in a separately rollbackable Helm revision while preserving the exact five role custom-domain CORS origins, captures the existing Admin Direct Upload deployment, and runs:

bash
pnpm deploy:cloudflare:pages -- \
  --app admin-web \
  --include-admin \
  --branch master \
  --ensure-production-branch \
  --rollback-state-input <admin-rollback-state.json>

The workflow then repeats browser/provenance acceptance with HOCTAPAZ_QA_ADMIN_WEB_URL=https://v2.dev.admin.hoctapaz.com. Admin Pages and the Admin Helm state have independent rollback artifacts so a failed Admin activation does not roll back the already accepted six non-Admin Pages release.

That separate Helm mutation occurs only on the first accepted Admin activation. On later releases, the root rollout proves and preserves the existing five-origin Helm/ConfigMap state, and the Admin stage verifies the complete manifest before publishing Admin Pages. If an already-active Admin fails its release acceptance, recovery is release-wide in Worker -> Admin Pages -> six non-Admin Pages -> root Helm order. The Admin state transition leaves the Gateway and all owner-service runtime checksums unchanged.

Domain Map

AppPages projectDomain
Adminhoctapaz-v2-dev-admin-webv2.dev.admin.hoctapaz.com
Staffhoctapaz-v2-dev-staff-webv2.dev.staff.hoctapaz.com
Teacherhoctapaz-v2-dev-teacher-webv2.dev.teacher.hoctapaz.com
Studenthoctapaz-v2-dev-student-webv2.dev.student.hoctapaz.com
Parenthoctapaz-v2-dev-parent-webv2.dev.parent.hoctapaz.com
Publichoctapaz-v2-dev-public-webv2.dev.hoctapaz.com
Docshoctapaz-v2-dev-docs-webv2.dev.docs.hoctapaz.com

Provisioned Cloudflare State

As of 2026-07-05, Cloudflare account 9dcb06f92077d2d922d47c332b54cd32 owns the active hoctapaz.com zone 1d768be3b6af3f0a6e52acce15fef09b.

The seven Pages projects exist. GitHub Actions/Direct Upload uses repository branch master; the first production-candidate rollout deploys Public, Docs, Staff, Parent, Student and Teacher in that locked order. Admin is an explicit follow-up enabled only after those six pass acceptance.

Pages projectPages subdomain
hoctapaz-v2-dev-admin-webhoctapaz-v2-dev-admin-web.pages.dev
hoctapaz-v2-dev-staff-webhoctapaz-v2-dev-staff-web.pages.dev
hoctapaz-v2-dev-teacher-webhoctapaz-v2-dev-teacher-web.pages.dev
hoctapaz-v2-dev-student-webhoctapaz-v2-dev-student-web.pages.dev
hoctapaz-v2-dev-parent-webhoctapaz-v2-dev-parent-web.pages.dev
hoctapaz-v2-dev-public-webhoctapaz-v2-dev-public-web.pages.dev
hoctapaz-v2-dev-docs-webhoctapaz-v2-dev-docs-web.pages.dev

Each target hostname is attached to its Pages project as a custom domain and has a proxied CNAME in the hoctapaz.com zone:

txt
v2.dev.admin.hoctapaz.com   -> hoctapaz-v2-dev-admin-web.pages.dev
v2.dev.staff.hoctapaz.com   -> hoctapaz-v2-dev-staff-web.pages.dev
v2.dev.teacher.hoctapaz.com -> hoctapaz-v2-dev-teacher-web.pages.dev
v2.dev.student.hoctapaz.com -> hoctapaz-v2-dev-student-web.pages.dev
v2.dev.parent.hoctapaz.com  -> hoctapaz-v2-dev-parent-web.pages.dev
v2.dev.hoctapaz.com         -> hoctapaz-v2-dev-public-web.pages.dev
v2.dev.docs.hoctapaz.com    -> hoctapaz-v2-dev-docs-web.pages.dev

dig @1.1.1.1 resolves all seven names to Cloudflare edge IPs. Cloudflare API reports all seven custom domains as active.

Initial verified deployment evidence:

AppDeployment IDPreview URLCustom domain status
Adminbccd99d5-49a9-4fab-85ff-0954bfa92d71https://bccd99d5.hoctapaz-v2-dev-admin-web.pages.devactive
Staff333f56d2-4a94-488b-8e80-c442840c091ahttps://333f56d2.hoctapaz-v2-dev-staff-web.pages.devactive
Teachere800a534-8ffb-440d-bfbf-b1ea5570e6b9https://e800a534.hoctapaz-v2-dev-teacher-web.pages.devactive
Student10955c91-7270-49f9-bcdd-ab46f8b0cf98https://10955c91.hoctapaz-v2-dev-student-web.pages.devactive
Parentc8d70b0f-1d6d-4547-8d92-a44212303e9ehttps://c8d70b0f.hoctapaz-v2-dev-parent-web.pages.devactive
Public32ad82e9-f8c6-4ab4-ac9a-065674a2b05ehttps://32ad82e9.hoctapaz-v2-dev-public-web.pages.devactive
Docsc54b85c8-6d70-4f84-943f-7281842b4f7ehttps://c54b85c8.hoctapaz-v2-dev-docs-web.pages.devactive

Prerequisites

Wrangler must be authenticated by one of:

bash
npx wrangler login

or non-interactive environment variables:

bash
export CLOUDFLARE_API_TOKEN=...
export CLOUDFLARE_ACCOUNT_ID=...
export VITE_API_BASE_URL=https://<gateway-host>

Token permissions must include Cloudflare Pages edit access. Custom domains require the hoctapaz.com zone to be available in the same Cloudflare account or a valid CNAME/custom-domain setup. The deploy script compiles apps with VITE_APP_ENV=v2-dev. VITE_API_BASE_URL and VITE_APP_ENV are both required at runtime; a missing, blank or unknown app environment fails closed instead of silently generating localhost, demo or legacy role links. The v2-dev manifest pins that value to https://api.v2.dev.hoctapaz.com. Each Pages build validates the exact origin before compiling and writes provenance into its dist/ output. --skip-build verifies the same provenance before upload; rebuild instead of deploying an artifact that lacks it. The validator rejects legacy hoctapaz.com, Kubernetes service hosts and non-Gateway loopback ports. There is no runtime or deployment fallback to the legacy Gateway.

The Direct Upload path publishes a non-hidden /deployment-provenance.json, attaches the full source SHA through Wrangler --commit-hash, and waits for each project API to report a new successful canonical production deployment on that exact SHA before moving to the next project. Post-deploy Chrome acceptance fetches the provenance through every custom domain with cache busting and requires the exact app ID, source SHA and API origin. All seven projects, including docs-web, publish the same exact five-key provenance contract: repository, sourceSha, artifactDigest, appId and apiBaseUrl.

turbo.json whitelists VITE_API_BASE_URL and VITE_APP_ENV for the build task. If you prebuild with Turbo and then deploy with --skip-build, use --force when changing these env values:

bash
VITE_API_BASE_URL=https://api.v2.dev.hoctapaz.com VITE_APP_ENV=v2-dev pnpm turbo run build '--filter=./apps/*' --force

Commands

Deploy all apps:

bash
pnpm deploy:cloudflare:pages

Deploy one app:

bash
pnpm deploy:cloudflare:pages -- --app public-web

Deploy only the VitePress docs site:

bash
pnpm docs:build
pnpm deploy:cloudflare:pages -- --app docs-web --skip-build

Create Pages projects before deploying when they do not exist. The v2-dev projects listed above already exist, so this flag should not be used for the current account unless a project was deleted:

bash
pnpm deploy:cloudflare:pages -- --create-projects

Use existing build outputs:

bash
pnpm deploy:cloudflare:pages -- --skip-build

Dry-run command expansion:

bash
pnpm deploy:cloudflare:pages -- --dry-run --skip-build

Custom Domains

After the first successful deployment, attach each domain through Pages Custom domains. Cloudflare docs require associating the hostname with the Pages project; adding a DNS CNAME alone is not sufficient.

If the zone is managed by the same Cloudflare account, the dashboard can create the DNS records during custom-domain setup. Otherwise create the CNAME to the project *.pages.dev hostname after the Pages custom-domain association is created.

Verification

For each domain:

bash
curl -I https://v2.dev.hoctapaz.com/
curl -I https://v2.dev.admin.hoctapaz.com/
curl -I https://v2.dev.staff.hoctapaz.com/
curl -I https://v2.dev.teacher.hoctapaz.com/
curl -I https://v2.dev.student.hoctapaz.com/
curl -I https://v2.dev.parent.hoctapaz.com/
curl -I https://v2.dev.docs.hoctapaz.com/

Expected:

  • 200 for /.
  • SPA fallback works for /login, /register, and /auth/login.
  • Static assets return long cache headers from _headers.

2026-07-05 verification:

  • npx wrangler whoami authenticated against account 9dcb06f92077d2d922d47c332b54cd32.
  • pnpm deploy:cloudflare:pages -- --skip-build uploaded all six app outputs.
  • pnpm deploy:cloudflare:pages -- --app docs-web --skip-build uploaded the VitePress docs output from docs/.vitepress/dist.
  • Cloudflare API reported each latest deployment stage as success.
  • Cloudflare API reported each attached custom domain as active.
  • curl returned 200 text/html for all six frontend custom domains and representative /login//register SPA fallback paths.
  • curl returned 200 text/html for https://v2.dev.docs.hoctapaz.com/, /api/, and /contracts/openapi/.
  • Live Playwright smoke passed for https://v2.dev.hoctapaz.com/ public auth hub and https://v2.dev.admin.hoctapaz.com/ admin login redirect.
  • Live Playwright smoke passed for https://v2.dev.docs.hoctapaz.com/ home, API guide, and OpenAPI contracts pages.

Notes

The Cloudflare API connector can manage projects, custom domains, DNS, and deployment metadata. Local asset upload still uses Wrangler because Pages Direct Upload sends build files from dist/ through Wrangler's upload-token and asset upload flow.

Go-platform documentation is generated from repository Markdown.