Skip to content

CI/CD with GitHub Actions

  • Status: Accepted
  • Date: 2026-06-06
  • Deciders: Engineering, Platform

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.

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.

  1. Validate (every PR). Install with pnpm (frozen lockfile, cached store), then run:
    • pnpm typecheck (TypeScript strict + astro check)
    • pnpm lint, pnpm format:check
    • pnpm test (Vitest unit) and pnpm test:e2e (Playwright) — ADR 0012
    • axe accessibility checks and Lighthouse CI against the performance/a11y budgets
    • token freshness: rebuild tokens and fail if tokens.css is 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 plan on PRs that touch infra/ (no apply on PRs)
  2. 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.
  3. Deploy application (push to main). A push to main that 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, then wrangler deploy ships the Astro site as a single Cloudflare Worker (static assets + the same-origin /api/contact and /api/careers routes) and applies any pending D1 migrations. Staging deploys on every push that touches the site, behind STAGING_DEPLOY_ENABLED; production runs the same job behind DEPLOY_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.
  4. Reconcile infrastructure (manual, out of band). terraform apply runs only by gated manual workflow_dispatch — never on push — backed by terraform plan on infra PRs and a weekly scheduled drift check. Infrastructure is reconciled deliberately, ahead of any deploy that depends on new resources.
  5. Releases (Release Drafter). release-drafter.yml runs on push to main and maintains a rolling draft GitHub Release with categorized notes and a suggested version derived from Conventional PR titles. Publishing the draft tags vX.Y.Z; releases are communication checkpoints and never gate deploys. See the release runbook.
  • Required status checks (validate suite + budgets, named in the bootstrap runbook) must pass before merge.
  • Deploys trigger on the push to main itself 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, behind DEPLOY_ENABLED, and the production environment’s deployment branch policy admits only main. Staging deploys on every push to main that touches the site, behind STAGING_DEPLOY_ENABLED, and from any branch by manual run. terraform apply runs only by manual workflow_dispatch from main, 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).

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.
  • 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.