PR preview deployments
September 3, 2026 · View on GitHub
The GitHub Actions preview workflow creates per-preview Cloudflare resources so each PR preview is isolated. See the setup index for the other setup pages.
- App worker:
<preview-worker-name>(for kody:kody-pr-<n>). Generated preview config setsworkers_dev: trueso secret-bulk reapply cannot drop the<name>.<subdomain>.workers.devtrigger (Cloudflare error 1042). - Platform worker:
<preview-worker-name>-platform - Runtime worker:
<preview-worker-name>-runtime - Jobs worker:
<preview-worker-name>-jobs - Highlight worker:
<preview-worker-name>-highlight - App D1 database:
<preview-worker-name>-db - Audit D1 database:
<preview-worker-name>-audit-db - Jobs D1 database: shared
kody-preview-jobs(ensured byjobs-worker-resources.ts; not per-PR) - KV namespace (OAuth state):
<preview-worker-name>-oauth-kv - Mock workers:
<preview-worker-name>-mock-<service>
When a PR is closed, the cleanup job deletes the preview
app/platform/runtime/jobs Workers, mock Workers, Queues, per-preview D1/KV/R2,
and these per-PR resources. It does not delete shared names such as
kody-preview-jobs or any production name.
Cleanup is bounded, retry-safe, and idempotent. Transient Cloudflare 429 and 5xx responses (including wrangler 504 Gateway Timeout) retry with backoff inside the four-minute job budget. Permanent 401/403 failures are not retried. A failure on one resource does not skip later independent resources: the sweep finishes, then fails once with every leftover name. Already-missing resources count as success. Queue consumers are removed before Workers, and Workers before Queues, because those deletes depend on each other. Non-empty preview R2 buckets are emptied (objects deleted) before the bucket delete; leftover objects surface in the same aggregate failure instead of a warning.
Cloudflare Workers supports version preview_urls, but those preview URLs are
not available for Workers that use Durable Objects. The main app Worker binds
MCP_OBJECT, so app previews use per-PR Worker names. Mock Workers do not use
Durable Objects, so their Wrangler configs opt into preview_urls = true and
the workflow includes mock version preview links when Cloudflare returns them.
Production deploys also ensure required Cloudflare resources exist before migrations/deploy:
- D1 database: from
env.production.d1_databasesbindingAPP_DB - KV namespace:
OAUTH_KV(defaults to<worker-name>-oauthwhen creating)
Both the preview and production deploy workflows run post-deploy healthchecks and fail the job if any expected worker is missing the deployed commit:
- origin:
<deploy-url>/health→{ ok: true, commitSha, commit, pullRequest, deploy }(commitShais the version pin; browsers that prefer HTML get clickable commit/PR/job links) - platform:
/__platform/health→{ status: "ok", commitSha } - runtime:
/__runtime/health→{ status: "ok", commitSha } - jobs:
/healthon the jobs workers.dev URL →{ ok: true, commit }(jobs has no public hostname; the workflow uses the deploy output URL)
Preview deploys also run node tools/seed-test-data.ts --remote after deploy,
seeding me@kentcdodds.com / ilikecode (a non-admin account; the jane
companion account is only seeded locally). See .github/workflows/preview.yml
for the exact invocation.
Preview cleanup also deletes the matching GitHub environment
(preview-<pr-number>). That API requires repository administration write
access, so the repo must define a PREVIEW_ENVIRONMENT_ADMIN_TOKEN Actions
secret with a token that has that permission. Cleanup intentionally fails when
that secret is missing or under-scoped so permission regressions are visible.
Every Cloudflare delete in tools/ci/preview-resources.ts (Workers, D1, KV, R2,
Queues and their consumers) first passes through
assertPreviewResourceName(name, kind), which throws unless the name matches
^kody-(pr-<number>|branch-<slug>)(-<segment>)*$. Production names (kody,
kody-platform, kody-audit, kody-webhook-dispatch, ...) and the shared
kody-preview* names never match, so a bug or an empty PR number cannot compute
a production name and delete it. npm run deploy-guardrails:check fails if a
destructive call in that script is not preceded by the guard in the same
function.
The production deploy workflow can also be started manually from GitHub Actions
via Run workflow on main. The manual path verifies that the selected
commit is the current origin/main HEAD before it deploys.
If you ever need to do the same operations manually, use:
node tools/ci/preview-resources.ts ensure --worker-name <name> --out-config <path>node tools/ci/preview-resources.ts cleanup --worker-name <name>node tools/ci/production-resources.ts ensure --out-config <path>
To manually test a PR preview (find the URL, sign in as the seeded user,
create specific data with --request, assert the change), see
Manual preview testing and run
npm run preview:manual-test.