Tooling, CI/CD & DX

CI/CD pipelines

Designing a pipeline for a Nuxt app — stages and their order, caching installs and build outputs, parallel jobs and sharded tests, building once and testing the artifact, preview deployments per pull request, environment promotion, release strategies and rollbacks, secrets and OIDC, flaky-test policy, and the metrics that tell you the pipeline is healthy.

"Walk me through your CI" is a question about judgement: what you gate on, what you parallelise, how you get a preview to a reviewer, how you ship without fear and roll back in a minute. The pipeline below is the shape most Nuxt teams converge on; the quality gates page covers the test side, this page the delivery side.

Know

  • Stages: install (cached) → static checks (lint, typecheck, content validation) → unit and component tests → build once → end-to-end and smoke tests against the built server (nuxt preview or the .output artifact) → performance and accessibility budgets → preview deploy for pull requests → on the main branch: migrations, deploy, post-deploy smoke, release notes. Fast, cheap jobs first so failures return in minutes.
  • Build once, promote the artifact: the same .output (or container image) moves from preview to staging to production, with runtimeConfig supplying per-environment values through NUXT_* variables; rebuilding per environment is a classic source of "worked in staging".
  • Caching: the pnpm store keyed by the lockfile (actions/setup-node with cache: pnpm), Turborepo remote cache for task outputs, Playwright browsers cached by version, .nuxt/node_modules/.cache for Vite's dependency cache where safe.
  • Parallelism: independent jobs for lint, typecheck and unit tests; Playwright sharded across runners (--shard=1/4) with merged reports; matrix builds only where the runtime matters (Node 24 and 26 for a library, one runtime for an app).
  • Preview deployments: every pull request gets a URL (Cloudflare Workers preview URLs or Pages, Vercel and Netlify previews, or a namespaced environment on your own infra), posted as a PR comment, protected by basic auth or an allow-list when it holds real data, and torn down on merge. Reviewers and designers click instead of running the branch.
  • Environment promotion and protection: staging deploys automatically from main; production requires an approval or a tag, uses environment-scoped secrets and concurrency groups so two deploys cannot interleave.
  • Release strategies: rolling deploys with health checks (the deployment page), blue-green when you can afford two fleets, canaries by percentage on platforms that support gradual rollouts (Cloudflare gradual deployments, Vercel skew protection to keep old clients talking to matching servers), feature flags to decouple deploy from release.
  • Database migrations in the pipeline: expand-only migrations run as a step before the deploy, from the artifact's migration files, against the target environment, with a lock; the deploy proceeds only if they succeed; contractions ship in a later release.
  • Rollback: redeploy the previous artifact (one command or one click), never "revert and rebuild under pressure"; keep the last N artifacts; route rules and feature flags provide instant partial rollbacks; static sites roll back by re-pointing the CDN at the previous build.
  • Secrets: environment secrets in the CI provider, short-lived credentials via OIDC (GitHub → cloud provider or npm trusted publishing) instead of long-lived tokens, no secrets in build args or NUXT_PUBLIC_*, secret scanning on pushes, rotation runbooks.
  • Flaky tests: quarantine with an owner and a deadline instead of retrying forever; retries only for known-flaky end-to-end tests (Playwright retries: 2 in CI) with the flake rate tracked; a test that fails on main blocks merging until fixed or quarantined.
  • Health metrics: pipeline duration (target under ten minutes to merge signal), failure rate by stage, flake rate, deploy frequency, lead time, change failure rate and time to restore (DORA), and cost per run.
  • Security in the pipeline: pinned action versions (by SHA for third-party actions), minimal permissions, dependency review on PRs, SBOM generation, container image scanning, provenance attestations for artifacts.

How it works

A complete GitHub Actions workflow for a Nuxt app deployed to Cloudflare Workers with preview URLs:

.github/workflows/ci.yml
name: CI
on:
  pull_request:
  push: { branches: [main] }
concurrency: { group: ci-${{ github.ref }}, cancel-in-progress: true }
permissions: { contents: read }

jobs:
  checks:
    runs-on: ubuntu-latest
    strategy: { matrix: { task: [lint, typecheck, test] } }
    steps:
      - uses: actions/checkout@v7
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v7
        with: { node-version-file: .nvmrc, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm ${{ matrix.task }}

  build:
    needs: checks
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v7
        with: { node-version-file: .nvmrc, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
        env: { NUXT_PUBLIC_SITE_URL: https://preview.example.com }
      - uses: actions/upload-artifact@v7
        with: { name: output, path: .output, retention-days: 7 }

  e2e:
    needs: build
    runs-on: ubuntu-latest
    strategy: { fail-fast: false, matrix: { shard: [1, 2, 3, 4] } }
    steps:
      - uses: actions/checkout@v7
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v7
        with: { node-version-file: .nvmrc, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - uses: actions/download-artifact@v8
        with: { name: output, path: .output }
      - run: pnpm exec playwright install --with-deps chromium
      - run: pnpm exec playwright test --shard=${{ matrix.shard }}/4   # webServer runs `node .output/server/index.mjs`
      - uses: actions/upload-artifact@v7
        if: failure()
        with: { name: playwright-report-${{ matrix.shard }}, path: playwright-report }

  preview:
    if: github.event_name == 'pull_request'
    needs: build
    runs-on: ubuntu-latest
    permissions: { contents: read, pull-requests: write, id-token: write }
    steps:
      - uses: actions/checkout@v7
      - uses: actions/download-artifact@v8
        with: { name: output, path: .output }
      - uses: cloudflare/wrangler-action@v4
        id: deploy
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: versions upload --preview-alias pr-${{ github.event.number }}
      - run: gh pr comment ${{ github.event.number }} --body "Preview: ${{ steps.deploy.outputs.deployment-url }}"
        env: { GH_TOKEN: ${{ github.token }} }

  deploy:
    if: github.ref == 'refs/heads/main'
    needs: [build, e2e]
    runs-on: ubuntu-latest
    environment: production        # requires reviewer approval in repo settings
    concurrency: { group: deploy-production, cancel-in-progress: false }
    steps:
      - uses: actions/checkout@v7
      - uses: actions/download-artifact@v8
        with: { name: output, path: .output }
      - run: pnpm dlx drizzle-kit migrate      # expand-only migrations, from the same commit
        env: { DATABASE_URL: ${{ secrets.DATABASE_URL }} }
      - uses: cloudflare/wrangler-action@v4
        with: { apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}, command: deploy }
      - run: curl --fail --retry 5 --retry-delay 3 https://example.com/api/ready

Rollback as a runbook, not an improvisation:

# Cloudflare Workers: list versions, roll back to the previous one (instant)
wrangler versions list
wrangler rollback <previous-version-id>

# Node/container platforms: redeploy the previous image or artifact
docker service update --image ghcr.io/team/shop:<previous-sha> shop

Decoupling deploy from release with a flag:

app/composables/useFeature.ts
export function useFeature(name: string) {
  const flags = useState<Record<string, boolean>>('flags', () => ({}))
  return computed(() => flags.value[name] === true)
}
// flags come from runtimeConfig or a flag service, loaded once in a plugin; a bad release is turned off, not redeployed
Gotcha· Rebuilding per environment

NUXT_PUBLIC_API_BASE changes between staging and production, so someone rebuilds for each and tests one artifact while shipping another. Public runtime config is read from the environment at server start (and injected into the payload), so one build serves every environment; rebuild only for genuinely build-time differences.

Gotcha· Secrets in preview deployments

Previews built from forked pull requests must not receive production secrets or database access; CI providers withhold secrets from forks by default, and previews should run against seeded staging data with basic auth in front.

Verify before the interview:

Current Wrangler commands for preview aliases and rollbacks, actions/setup-node cache options and the current major versions of the actions used, and Playwright's sharding and report-merging commands.

docs ↗

Exercise

Exercise
  • Split a serial pipeline into parallel checks plus a single build; measure wall-clock time to merge signal before and after.
  • Add preview deployments with a PR comment; make a UI change and have a colleague review from the URL alone.
  • Rehearse a rollback in staging: deploy a deliberate error, roll back with one command, record how long it took.

Be able to say

Be able to say· Walk me through your CI/CD pipeline for a Nuxt app.

"Fast, cheap gates first: install from cache, then lint, type check and unit tests as parallel jobs. Then one build whose .output is uploaded as the artifact everything else uses: Playwright sharded across four runners against the built server, performance and accessibility budgets, and a preview deployment per pull request with the URL commented on the PR so reviewers click instead of checking out. On main, the same artifact is promoted: expand-only migrations run first, the deploy follows with health checks and a concurrency group so deploys never interleave, and a readiness probe confirms it. Rollback is redeploying the previous version with one command, and feature flags decouple releasing from deploying. Secrets are environment-scoped with short-lived OIDC credentials, actions are pinned, and I watch pipeline duration, flake rate and the DORA metrics to keep it healthy."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.