CI/CD with GitHub Actions
- Status: Accepted
- Date: 2026-06-06
- Deciders: Engineering, Platform
Context
Section titled “Context”The repo is a pnpm monorepo on GitHub (quarry-design-group/quarry.team). Deploying the site depends on Cloudflare infrastructure (single provider, ADR 0008) being provisioned and current, but that infrastructure changes rarely and holds stateful resources — a D1 form_submissions database, R2 buckets — that an unreviewed apply must never destroy. We need quality gates that protect the craft bar (types, lint, tests, a11y, performance budgets), per-PR previews so reviewers see real output, fast application deploys on merge, and infrastructure changes that are reviewed and applied deliberately rather than on every push.
Decision
Section titled “Decision”CI/CD runs on GitHub Actions as two intentionally decoupled tracks: application deploys on push to main, and infrastructure reconciled through a reviewed, manually gated Terraform workflow.
Pipeline shape
Section titled “Pipeline shape”- Validate (every PR). Install with pnpm (frozen lockfile, cached store), then run:
pnpm typecheck(TypeScript strict +astro check)pnpm lint,pnpm format:checkpnpm test(Vitest unit) andpnpm test:e2e(Playwright) — ADR 0012- axe accessibility checks and Lighthouse CI against the performance/a11y budgets
- token freshness: rebuild tokens and fail if
tokens.cssis stale (ADR 0003) - the docs-site build (
pnpm --filter @quarry/docs build), after the docs-mirror drift check, so a change that breaks the Starlight build — a lockfile bump included — fails before merge and before the push-triggered docs deploy terraform planon PRs that touchinfra/(no apply on PRs)
- Preview deploy (PRs that touch the site or the docs). The preview workflows are path-filtered. Each builds its app in Actions and uploads a Worker version; the version’s preview URL is posted in a sticky PR comment so reviewers inspect the actual deployed result.
- Deploy application (push to
main). A push tomainthat touches the site runs the Deploy Web workflow, which is path-filtered (apps/web,packages/ui, the lockfile, the workflow itself and the post-deploy smoke):pnpm build, thenwrangler deployships the Astro site as a single Cloudflare Worker (static assets + the same-origin/api/contactand/api/careersroutes) and applies any pending D1 migrations. Staging deploys on every push that touches the site, behindSTAGING_DEPLOY_ENABLED; production runs the same job behindDEPLOY_ENABLED. Each deploy ships the Worker’s runtime secrets from its GitHub environment with the new version. A manual run deploys any branch to staging (target: staging), so a change can be exercised on the staging Worker and database before it merges. - Reconcile infrastructure (manual, out of band).
terraform applyruns only by gated manualworkflow_dispatch— never on push — backed byterraform planon infra PRs and a weekly scheduled drift check. Infrastructure is reconciled deliberately, ahead of any deploy that depends on new resources. - Releases (Release Drafter).
release-drafter.ymlruns on push tomainand maintains a rolling draft GitHub Release with categorized notes and a suggested version derived from Conventional PR titles. Publishing the draft tagsvX.Y.Z; releases are communication checkpoints and never gate deploys. See the release runbook.
Gating
Section titled “Gating”- Required status checks (validate suite + budgets, named in the bootstrap runbook) must pass before merge.
- Deploys trigger on the push to
mainitself and run independently of that commit’s CI run. Their correctness rests on branch protection requiring the CI checks before the merge, and on the post-deploy smoke with automatic rollback (ADR 0018). - Production deploys run only from
main, behindDEPLOY_ENABLED, and theproductionenvironment’s deployment branch policy admits onlymain. Staging deploys on every push tomainthat touches the site, behindSTAGING_DEPLOY_ENABLED, and from any branch by manual run.terraform applyruns only by manualworkflow_dispatchfrommain, never automatically — an unreviewed auto-apply could destroy stateful infrastructure. - Releases are deliberate acts: publishing the accumulated draft Release is what cuts a tag. (No tag or Release exists until a maintainer publishes.)
- Secrets/credentials come from GitHub Actions encrypted secrets — repository secrets for CI, environment secrets for the Worker’s runtime — never the repo (ADR 0008,
SECURITY.md).
Consequences
Section titled “Consequences”Positive
- Application and infrastructure changes are each auditable in CI, and their separation keeps a routine app deploy from ever triggering an unreviewed infrastructure change.
- Every PR that touches the site or the docs gets a real preview URL; every PR must clear the full quality bar.
- Performance, a11y, and token-freshness regressions are caught before merge, not after.
Negative / costs
- Splitting deploys across a push-triggered app track and a manually gated infrastructure track means the two must be sequenced by hand when a deploy depends on new infrastructure.
- Full gates add minutes to CI (mitigated by caching).
- Preview deploys consume Cloudflare preview capacity.
Alternatives considered
Section titled “Alternatives considered”- Provider-native CI (Cloudflare Workers Git integration / Wrangler auto-deploy) only. Convenient, but can’t enforce our full unified quality gates or a reviewed Terraform workflow. Rejected as the orchestrator. Previews are built in Actions and served as Worker version preview URLs.
- GitLab CI / CircleCI / Jenkins. Capable, but the repo is on GitHub and Actions is the lowest-friction, best-integrated choice. Rejected.
- Manual deploys. Unauditable and error-prone; defeats the purpose. Rejected.