Runbook: Deploy
How a deploy works end to end, and how to roll back. Architecture: ADR 0009 (CI/CD), ADR 0008 (IaC), ADR 0006 (Cloudflare), ADR 0014 (data tier), ADR 0013 (staging + previews).
Environments
Section titled “Environments”One Worker codebase, two environments + ephemeral previews (ADR 0013):
| Environment | Host | Gate (repo variable) | Data tier |
|---|---|---|---|
| staging | staging.quarry.team (noindex, behind Cloudflare Access — @quarry.team OTP) |
STAGING_DEPLOY_ENABLED=true |
D1 marketing-site-form-submissions-staging + own KV |
| production | quarry.team / www |
DEPLOY_ENABLED=true |
D1 marketing-site-form-submissions + own KV |
| PR previews | ephemeral *.workers.dev URL |
follows the staging gate | staging’s (versions upload — no deploy, no traffic promotion) |
The environment is selected at build time: the @astrojs/cloudflare adapter reads
apps/web/wrangler.toml for the environment named by CLOUDFLARE_ENV (unset = production,
staging = [env.staging]) and emits the fully-resolved deployable config to
dist/server/wrangler.json. All wrangler commands must target the emitted config
(-c dist/server/wrangler.json) — the source toml has no main/assets.
Principle: two independent tracks
Section titled “Principle: two independent tracks”Infrastructure and the application ship on separate, independently triggered tracks. wrangler deploy ships the Worker on push to main; Terraform owns the durable Cloudflare config plane (zone, D1, R2, Turnstile, WAF/cache) and is reconciled on its own cadence: terraform plan on infra PRs for review, a weekly drift check, and a deliberate, gated manual apply. Keeping apply off the push path is intentional — an auto-applied plan could destroy stateful resources (a D1 database, an R2 bucket), so an infra change is applied by a human who has read the plan, not on every merge. Nothing is deployed by hand to dashboards.
End-to-end flow
Section titled “End-to-end flow”- Open a PR. CI runs the validate suite:
pnpm typecheck,pnpm lint,pnpm format:check,pnpm test(Vitest),pnpm test:e2e(Playwright), axe a11y, Lighthouse CI budgets, the token-freshness check, andterraform planfor any changed environment. All required checks must pass. - Review the preview. The
Preview Web (PR)workflow uploads a Worker version of the staging environment (wrangler versions upload) and posts its ephemeral preview URL as a sticky PR comment. Reviewers inspect the actual deployed result against staging’s D1/KV. - Merge to
main. On push tomain, the Deploy Web workflow (.github/workflows/deploy-web.yml) ships the site — there is no Terraform step in this path:pnpm build(builds@quarry/uitokens then@quarry/web), thenwrangler deploy -c dist/server/wrangler.jsonships the Worker and its static assets after applying any pending D1 migrations (apps/web/migrations/). Staging deploys on every push tomainonceSTAGING_DEPLOY_ENABLED=true; the production job runs the same steps behind its ownDEPLOY_ENABLEDgate.- The
@quarry/webpostbuildstep runsscripts/inject-csp-hashes.mjs, which pins the SHA-256 hashes of every inline script and style the site emits intoscript-src/style-srcofdist/client/_headers(replacing the__CSP_SCRIPT_HASHES__/__CSP_STYLE_HASHES__tokens inpublic/_headers) so the shipped CSP is strict — no'unsafe-*'anywhere (ADR 0015). It fails the build if a token is missing or no inline scripts/styles are found, and the e2e suite (tests/e2e/csp.spec.ts) loads every backbone route under the built policy and fails on any violation. Edit the CSP inpublic/_headers, never the built copy.
- The
- Infrastructure is a separate track, not triggered by the merge.
terraform applyruns only by gated manual dispatch (.github/workflows/infra.yml→workflow_dispatch→apply), withterraform planon infra PRs and a weekly drift check. State is in Cloudflare R2 (S3-compatible). Reconcile infra deliberately — ahead of a deploy that depends on new resources — rather than on every push.
- Post-deploy verification (see checklist).
Docs site (docs.quarry.team)
Section titled “Docs site (docs.quarry.team)”The Starlight docs app (apps/docs) deploys separately from the marketing site, via the Deploy Docs workflow (.github/workflows/deploy-docs.yml):
- The site is a generated mirror of
docs/.apps/docs/scripts/sync-docs.mjsprojects the canonical Markdown underdocs/(ADRs, BRD, guides, runbooks) intoapps/docs/src/content/docs/**, rewriting cross-doc links to site routes and everything else to GitHub URLs. It runs asprebuild/predev, so any build reflects the currentdocs/. Never hand-edit the generated sections — editdocs/, runpnpm --filter @quarry/docs sync, and commit. CI (ci.yml) regenerates and fails on drift, so the mirror can’t fall out of step with the source. - It is a pure-static, assets-only Worker named
quarry-docs(apps/docs/wrangler.toml): nomain, no D1/KV, no secrets, no build-time env — justastro buildoutput served ondocs.quarry.team. Cloudflare creates the custom-domain DNS record on deploy, so there is no Terraform record for the docs host (same pattern as apex/www). - The job triggers on pushes to
maintouchingapps/docs/**ordocs/**(an edit to the canonical docs must redeploy the mirror) and is gated on the repo variableDOCS_DEPLOY_ENABLED=true(unset until the first deliberate go-live). It needs the sameCLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_IDsecrets and adocsenvironment. - PR previews:
Preview Docs (PR)(.github/workflows/preview-docs.yml) uploads a Worker version of the docs site (wrangler versions upload,preview_urls = true) on PRs touchingapps/docs/**ordocs/**and posts its ephemeral URL as a sticky comment under a marker (quarry-docs-preview-url) distinct from the web preview’s — so the two never collide. Same gate/guards as the web preview (DOCS_DEPLOY_ENABLED, same-repo human PRs only). - Manual deploy:
pnpm deploy:docs(builds thenwrangler deployfromapps/docs). - First-time go-live: set
DOCS_DEPLOY_ENABLED=true, ensure the Cloudflare token may create thedocs.quarry.teamcustom domain, then run the workflow (workflow_dispatch) or push a docs change. - Rollback:
wrangler rollbackinapps/docs, or revert the commit and let the workflow redeploy.
Pre-deploy checklist
Section titled “Pre-deploy checklist”- Validate suite green on the PR (types, lint, format, unit, e2e, axe, Lighthouse, token freshness).
-
terraform planreviewed — no unexpected resource destruction. - Content frontmatter valid (build would fail otherwise); no unintended
draft: true/falseflips. - Secrets present in GitHub Actions / provider stores; none added to the repo.
- Redirect map for
/cases/* → /work/*current (ADR 0011).
Post-deploy verification
Section titled “Post-deploy verification”- Production loads; home, a
/work/<slug>, and a/blog/<slug>render correctly. - A sample legacy
/cases/<slug>returns 301 to/work/<slug>. - Contact and careers forms submit end to end (Turnstile passes, record persists).
-
curl -sI https://quarry.team | grep -i content-security-policyshows hash-pinnedscript-srcandstyle-srcand nounsafe-anywhere in the policy (strict CSP intact; ADR 0015). - The contact page’s “Book a 30-min meet & greet” dialog opens and the Calendly booking page loads inside it (the iframe is our only Calendly surface; their widget JS must stay gone).
-
sitemap.xml,robots.txt,llms.txtresolve. - Cloudflare Web Analytics receiving data; no console errors (other than the intentional easter egg).
- Lighthouse spot-check within budget.
Rollback
Section titled “Rollback”The Worker and its data tier roll back independently.
Site (Cloudflare Worker + assets):
- Roll back to the previous Worker version with
wrangler rollback(or promote a prior deployment from the Cloudflare dashboard), or - Revert the offending commit and let the pipeline redeploy the prior build via
wrangler deploy.
Infrastructure (Terraform):
- Revert the infrastructure change and
terraform applyto reconcile back to the prior known-good state (single Cloudflare provider). Inspectplancarefully for destructive changes before applying.
Database (Cloudflare D1):
- Schema issues are handled by rolling forward a corrective migration; data issues are recovered via D1 Time Travel (point-in-time restore). Treat data-tier rollback as an incident — see
incident-response.md.
Rollback decision guide
Section titled “Rollback decision guide”| Symptom | First action |
|---|---|
| Bad content/markup, site up | Roll back to previous Worker version (wrangler rollback) |
| Form submissions failing | Roll back the Worker; check Turnstile/secrets |
| Infra misconfiguration (DNS, WAF, redirects) | Revert infra change, terraform apply |
| Data corruption / bad migration | Invoke incident response; corrective migration / D1 Time Travel |