Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/cloudflare-architecture-v2-cutover.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_DEPLOY_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: pnpm exec wrangler deploy --config wrangler.quick-production.jsonc
- name: Wait for the exact Submission Container rollout before activation
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_DEPLOY_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: >-
node scripts/wait-container-rollout.mjs
--config wrangler.quick-production.jsonc
--output cutover-evidence/container-rollout.json
- name: Atomically activate the deployed release through the Admin API
env:
WASM_OJ_ADMIN_SESSION: ${{ secrets.WASM_OJ_CUTOVER_ADMIN_SESSION }}
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/cloudflare-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_DEPLOY_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: pnpm exec wrangler deploy --config wrangler.quick-production.jsonc
- name: Wait for the exact Submission Container rollout
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_DEPLOY_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: >-
node scripts/wait-container-rollout.mjs
--config wrangler.quick-production.jsonc
--output release-evidence/container-rollout.json
- name: Smoke test
run: |
curl --fail --retry 6 --retry-all-errors --retry-delay 5 \
Expand Down
20 changes: 16 additions & 4 deletions docs/cloudflare-deployment-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ digest. SBOM, audit, and tests remain required explicit evidence files.

An Owner starts **Deploy Cloudflare production** manually. The workflow installs the pinned Node
and pnpm versions, validates and binds the release coordinates, typechecks, builds, applies
non-reset migrations, deploys, and verifies
non-reset migrations, deploys, waits for the exact Submission Container rollout, and verifies
`/api/health/live` and `/api/health/ready`.

The committed production config contains release placeholders, never a previous release ID or
Expand All @@ -89,6 +89,15 @@ window. Before applying even a non-reset migration, the production migration pre
that D1 ID/digest pair to exactly match the rendered config. A candidate that has not already been
activated therefore fails before the first Cloudflare mutation.

`scripts/wait-container-rollout.mjs` runs immediately after `wrangler deploy` and before health
checks. It reads the digest-pinned image from the rendered Worker config, then polls the pinned
Wrangler JSON interface for at most 15 minutes. Two consecutive observations must agree on the
application ID, exact image and generated application version; report `ready`; have every reported
application instance healthy with zero active, assigned, stopped, failed, scheduling, or starting
instances; and have no live Durable Object placement on an older version. Lookup or status errors
remain pending only within that bound, then fail closed. The successful receipt is retained with
the release OCI evidence.

`scripts/production-migrations.mjs normal` first proves that `0017_architecture_reset.sql` is
already present in D1's migration ledger. Before the one-time cutover it fails closed. It never
applies 0017 as part of an ordinary deployment. There is no v1 catalog backfill, catalog-count
Expand Down Expand Up @@ -123,13 +132,16 @@ then executes this fail-closed sequence:
token, and that 0017 is the only pending migration. Only then apply the reset.
6. Deploy the v2 Worker, Submission Workflow, Catalog Workflow, and Submission Container using the
release ID, canonical manifest digest, and digest-pinned Container reference from that request.
7. Submit the already-validated canonical activation request to
7. Before activation, require `scripts/wait-container-rollout.mjs` to prove the digest-pinned
Container application is terminal, fully healthy, and free of a live older application version.
Preserve its receipt in the cutover evidence.
8. Submit the already-validated canonical activation request to
`POST /api/admin/releases/activate`. D1 inserts or verifies the immutable release manifest and
performs an expected-current environment-pointer CAS in one batch; the endpoint also requires
the manifest identity to match the deployed Worker. The environment pointer is the sole active
release authority.
8. Verify liveness and readiness while formal mutations remain paused.
9. Preserve the inventory, tombstone receipt, activation request, and response as a 30-day GitHub
9. Verify liveness and readiness while formal mutations remain paused.
10. Preserve the inventory, tombstone receipt, rollout receipt, activation request, and response as a 30-day GitHub
artifact named with the cutover run ID.

If any step fails, the maintenance gate remains closed. Correct the cause and deploy forward; do not
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"package-manager:verify": "node scripts/verify-package-manager.mjs",
"docs:verify": "node scripts/verify-docs.mjs",
"architecture-reset:preflight": "node scripts/architecture-reset-preflight.mjs",
"github:verify": "node --test scripts/architecture-reset-migration.test.mjs scripts/architecture-reset-preflight.test.mjs scripts/architecture-reset-r2.test.mjs scripts/configure-production-release.test.mjs scripts/container-build-context.test.mjs scripts/generate-production-release-inputs.test.mjs scripts/library-private-path.test.mjs scripts/prepare-production-release.test.mjs scripts/production-migrations.test.mjs scripts/single-store-migration.test.mjs scripts/verify-oci-release-image.test.mjs && node scripts/verify-github-workflows.mjs",
"github:verify": "node --test scripts/architecture-reset-migration.test.mjs scripts/architecture-reset-preflight.test.mjs scripts/architecture-reset-r2.test.mjs scripts/configure-production-release.test.mjs scripts/container-build-context.test.mjs scripts/generate-production-release-inputs.test.mjs scripts/library-private-path.test.mjs scripts/prepare-production-release.test.mjs scripts/production-migrations.test.mjs scripts/single-store-migration.test.mjs scripts/verify-oci-release-image.test.mjs scripts/wait-container-rollout.test.mjs && node scripts/verify-github-workflows.mjs",
"conformance:server": "WASM_OJ_RUN_CONFORMANCE=1 vitest run --config vitest.config.ts src/server/conformance.integration.test.ts --reporter=verbose",
"conformance:browser": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/run-browser-conformance.mjs",
"conformance:compare": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/compare-conformance.ts",
Expand Down
1 change: 1 addition & 0 deletions scripts/verify-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ for (const required of [
"scripts/prepare-production-release.mjs",
"scripts/verify-oci-release-image.mjs",
"scripts/configure-production-release.mjs",
"scripts/wait-container-rollout.mjs",
"wasm_oj_active_releases",
"architecture-v2-production-smoke-passed",
"enabled: true",
Expand Down
5 changes: 5 additions & 0 deletions scripts/verify-github-workflows.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ requireText(sources.production, "--oci-evidence release-evidence/oci/evidence.js
requireText(sources.production, "production-release-oci-${{ github.run_id }}", "Production deployment");
requireText(sources.production, "production-migrations.mjs normal", "Production deployment");
requireText(sources.production, "wrangler deploy --config wrangler.quick-production.jsonc", "Production deployment");
requireText(sources.production, "wait-container-rollout.mjs", "Production deployment");
requireText(sources.production, "release-evidence/container-rollout.json", "Production deployment");
requireText(sources.production, "/api/health/live", "Production deployment");
requireText(sources.production, "/api/health/ready", "Production deployment");
forbidText(sources.production, "wrangler d1 migrations apply", "Production deployment");
Expand All @@ -49,6 +51,7 @@ const orderedProductionSteps = [
"configure-production-release.mjs",
"production-migrations.mjs normal",
"wrangler deploy --config wrangler.quick-production.jsonc",
"wait-container-rollout.mjs",
"/api/health/live",
"/api/health/ready",
];
Expand Down Expand Up @@ -80,6 +83,7 @@ const orderedCutoverSteps = [
"production-migrations.mjs architecture-reset",
"wrangler secret put MAINTENANCE_SMOKE_TOKEN",
"wrangler deploy --config wrangler.quick-production.jsonc",
"wait-container-rollout.mjs",
"/api/admin/releases/activate",
"/api/health/live",
"/api/health/ready",
Expand All @@ -100,6 +104,7 @@ requireText(sources.cutover, "--confirm-workflows-drained", "Architecture v2 cut
requireText(sources.cutover, "architecture-reset-preflight.mjs", "Architecture v2 cutover");
requireText(sources.cutover, "source-tombstone-receipt.json", "Architecture v2 cutover");
requireText(sources.cutover, "wrangler secret put MAINTENANCE_SMOKE_TOKEN", "Architecture v2 cutover");
requireText(sources.cutover, "cutover-evidence/container-rollout.json", "Architecture v2 cutover");
forbidText(sources.cutover, "architecture-reset-r2.mjs cleanup", "Architecture v2 cutover");

requireText(sources.cleanup, "actions/download-artifact@", "Architecture v2 cleanup");
Expand Down
Loading