CI/CD Pipeline
Purpose
Every PR runs type-check, lint, test, and build automatically. Failures block merge. Pipeline completes in under 10 minutes (ideally under 3) so developers don't abandon discipline.
Universal concept, platform-bound Procedure — the pipeline structure (parallel type-check / lint / test / build, dependency + framework-output caching, branch protection, preview deploys) applies to any stack. But the Procedure below is written concretely for GitHub Actions + Vercel; the job-DAG and caching concepts port to GitLab CI / CircleCI / others (see Other stacks), where the YAML and platform UI differ.
Procedure
Define the workflow stages (parallel where possible)
- Triggers:
on: pull_request + push to main; add paths-ignore for docs-only changes so a README edit doesn't run the full build
- Concurrency:
concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true } — a new push to a PR cancels the stale in-progress run (saves minutes + CI cost)
- Pin the Node version to what you deploy on (
.nvmrc / engines)
- Job 1: dependency install (cached)
- Job 2 (parallel):
tsc --noEmit
- Job 3 (parallel):
eslint .
- Job 4 (parallel):
vitest run
- Job 5 (depends on 2-4):
next build
- Stages 2-4 in parallel ⇒ pipeline = max(typecheck, lint, test) + build, not sum
Configure caching
- npm cache:
actions/setup-node@v4 with cache: 'npm'
- Next.js build cache:
actions/cache@v4 for ~/.npm and .next/cache
- Use the canonical cache key from Next.js docs (the
restore-keys fallback is what delivers the warm-cache speedup):- uses: actions/cache@v4
with:
path: |
~/.npm
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- Result: expect 2-3x speedup on warm cache (exact numbers depend on project size)
Vercel Preview integration
- Connect repo in Vercel dashboard
- Every PR gets a preview URL
- Comment the URL on the PR for stakeholder review
- Production: promote from a verified preview; run a post-deploy smoke check; keep instant rollback available (Vercel retains prior deployments)
Branch protection on main
- GitHub → Settings → Branches → Add rule
- Require status checks: type-check, lint, test, build
- Require PR review (≥ 1 approval)
- Block force push, block direct push to main
Environment variable management
- Separate Vercel envs: Development / Preview / Production
- GitHub Secrets for CI-only values
- Validate via Zod schema (see
developer-experience skill)
5b. Harden the CI itself (it runs with your secrets)
- Least privilege: set
permissions: contents: read at the workflow top, widen per-job only as needed — the default GITHUB_TOKEN is over-permissioned
- Pin third-party actions to a full commit SHA, not a moving tag (
@v4 can be re-pointed to malicious code); let Dependabot bump them
- Never expose secrets to untrusted fork PRs —
pull_request (the safe default) has no secret access; reach for pull_request_target only with extreme care (it runs with secrets against the PR's code)
- Use OIDC for cloud deploys instead of long-lived access keys stored in secrets
- (coordinate with
security-audit)
Optional integrations
- Lighthouse CI for performance regression detection
- axe-core for a11y regression (coordinate with
accessibility-audit)
- Sentry release tagging (coordinate with
observability-setup)
- Upload artifacts on failure (Playwright traces/screenshots, coverage) so red runs are debuggable
- Wire a new gate as non-blocking first (
continue-on-error), then flip it to required once it's stable
Verify pipeline performance (validation loop)
- Trigger a real PR; measure total runtime
- If > 10 min: identify the long pole job (usually
next build or npm install), expand cache scope or parallelize further, and re-trigger; loop until ≤ 10 min
- If warm-cache runs don't show ≥ 2x speedup over cold: cache key probably too narrow — broaden the
restore-keys fallback and re-measure
Completion Criteria
Output
- CI workflow file:
.github/workflows/ci.yml with parallel jobs (type-check / lint / test / build) + caching block
- Branch protection: rule on
main requiring all CI status checks to pass + PR review ≥ 1
- Preview deploys: Vercel/Netlify/Cloudflare Pages project linked to repo, commenting URL per PR
- Pipeline metrics (paste into PR description): cold build time / warm cache time / cache hit rate
- Commit format:
chore(ci): <change> for workflow updates; chore(infra): <change> for branch protection / env config
Implementation
React + Next.js (default — GitHub Actions + Vercel)
- Workflow:
actions/checkout + actions/setup-node + actions/cache, each pinned to a commit SHA (Dependabot github-actions ecosystem bumps them)
- Concurrency:
concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }
- Permissions:
permissions: contents: read at the top; widen per-job as needed
- Build cache key:
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.{js,jsx,ts,tsx}') }} + restore-keys fallback
- Preview: Vercel project auto-deploys every PR, comments URL on PR
- Deploy auth: Vercel/cloud OIDC over static tokens; instant rollback via prior deployments
- Branch protection: require type-check + lint + test + build status checks
Other stacks
- Vue / Nuxt: cache
.nuxt/cache and node_modules/.cache/nuxt; deploy preview via Vercel/Netlify (both auto-detect Nuxt)
- SvelteKit: cache
.svelte-kit/output; deploy preview via Vercel/Netlify/Cloudflare Pages (built-in adapters)
- Angular: cache
.angular/cache; deploy preview via Firebase Hosting or Vercel
- Other CI platforms: GitLab CI, CircleCI, Buildkite — same parallel structure (jobs DAG), different YAML syntax
- Other deploy platforms: Netlify deploy previews, Cloudflare Pages, Render preview environments — all support PR-triggered builds with unique URLs
- Universal: parallel job DAG > sequential steps; caching keyed on lockfile + source hash is the universal pattern; branch protection rules are platform-specific but conceptually identical
Related skills
test-strategy — defines what gets gated in CI (and keeps required checks non-flaky)
accessibility-audit — axe-core wired here as a gate
security-audit — least-privilege tokens, SHA-pinned actions, npm audit as a CI gate
observability-setup — Sentry release tagging coordinated with CI
developer-experience — husky + lint-staged complement CI by gating locally
Reference
- Key insight encoded: Cache
~/.npm + .next/cache keyed on package-lock.json + source hashes — this is the single biggest pipeline speedup. Run type-check / lint / test as parallel jobs (not sequential steps) to hit sub-3-minute warm pipelines. The next build step is usually the long pole; its caching benefit compounds across PRs. Two senior must-haves often missed: concurrency: cancel-in-progress (kill stale runs on a new push) and treating the workflow as a security boundary — least-privilege permissions:, SHA-pinned actions, and no secrets for fork PRs.
1---2name: cicd-pipeline-23description: Set up GitHub Actions for Next.js with parallel type-check / lint / test / build, dependency + Next.js caching, concurrency cancellation, Vercel Preview, branch protection, and least-privilege CI security. Use at project start, when manual deploys cause errors, or when the team grows. Coordinates with test-strategy for what gets gated and developer-experience for local pre-commit gating.4license: MIT5---67# CI/CD Pipeline89## Purpose10Every PR runs type-check, lint, test, and build automatically. Failures block merge. Pipeline completes in under 10 minutes (ideally under 3) so developers don't abandon discipline.1112**Universal concept, platform-bound Procedure** — the pipeline *structure* (parallel type-check / lint / test / build, dependency + framework-output caching, branch protection, preview deploys) applies to any stack. But the Procedure below is written concretely for **GitHub Actions + Vercel**; the job-DAG and caching concepts port to GitLab CI / CircleCI / others (see Other stacks), where the YAML and platform UI differ.1314## Procedure15161. **Define the workflow stages (parallel where possible)**17 - Triggers: `on: pull_request` + `push` to `main`; add `paths-ignore` for docs-only changes so a README edit doesn't run the full build18 - **Concurrency**: `concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }` — a new push to a PR cancels the stale in-progress run (saves minutes + CI cost)19 - Pin the Node version to what you deploy on (`.nvmrc` / `engines`)20 - **Job 1**: dependency install (cached)21 - **Job 2** (parallel): `tsc --noEmit`22 - **Job 3** (parallel): `eslint .`23 - **Job 4** (parallel): `vitest run`24 - **Job 5** (depends on 2-4): `next build`25 - Stages 2-4 in parallel ⇒ pipeline = max(typecheck, lint, test) + build, not sum26272. **Configure caching**28 - npm cache: `actions/setup-node@v4` with `cache: 'npm'`29 - Next.js build cache: `actions/cache@v4` for `~/.npm` and `.next/cache`30 - Use the canonical cache key from Next.js docs (the `restore-keys` fallback is what delivers the warm-cache speedup):31 ```yaml32 - uses: actions/cache@v433 with:34 path: |35 ~/.npm36 ${{ github.workspace }}/.next/cache37 key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}38 restore-keys: |39 ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-40 ```41 - Result: expect 2-3x speedup on warm cache (exact numbers depend on project size)42433. **Vercel Preview integration**44 - Connect repo in Vercel dashboard45 - Every PR gets a preview URL46 - Comment the URL on the PR for stakeholder review47 - Production: promote from a verified preview; run a post-deploy smoke check; keep instant rollback available (Vercel retains prior deployments)48494. **Branch protection on `main`**50 - GitHub → Settings → Branches → Add rule51 - Require status checks: type-check, lint, test, build52 - Require PR review (≥ 1 approval)53 - Block force push, block direct push to main54555. **Environment variable management**56 - Separate Vercel envs: Development / Preview / Production57 - GitHub Secrets for CI-only values58 - Validate via Zod schema (see `developer-experience` skill)59605b. **Harden the CI itself (it runs with your secrets)**61 - **Least privilege**: set `permissions: contents: read` at the workflow top, widen per-job only as needed — the default `GITHUB_TOKEN` is over-permissioned62 - **Pin third-party actions to a full commit SHA**, not a moving tag (`@v4` can be re-pointed to malicious code); let Dependabot bump them63 - **Never expose secrets to untrusted fork PRs** — `pull_request` (the safe default) has no secret access; reach for `pull_request_target` only with extreme care (it runs with secrets against the PR's code)64 - **Use OIDC** for cloud deploys instead of long-lived access keys stored in secrets65 - (coordinate with `security-audit`)66676. **Optional integrations**68 - Lighthouse CI for performance regression detection69 - axe-core for a11y regression (coordinate with `accessibility-audit`)70 - Sentry release tagging (coordinate with `observability-setup`)71 - Upload artifacts on failure (Playwright traces/screenshots, coverage) so red runs are debuggable72 - Wire a new gate as non-blocking first (`continue-on-error`), then flip it to required once it's stable73747. **Verify pipeline performance (validation loop)**75 - Trigger a real PR; measure total runtime76 - If > 10 min: identify the long pole job (usually `next build` or `npm install`), expand cache scope or parallelize further, and re-trigger; loop until ≤ 10 min77 - If warm-cache runs don't show ≥ 2x speedup over cold: cache key probably too narrow — broaden the `restore-keys` fallback and re-measure7879## Completion Criteria80- [ ] Pipeline runs on every PR open + push81- [ ] Failure blocks merge (branch protection enforced)82- [ ] Type-check + lint + test run in parallel83- [ ] Concurrency cancels stale in-progress runs (`cancel-in-progress`)84- [ ] Cold build < 10 min, warm cache delivers ≥ 2x speedup85- [ ] Vercel Preview URL on every PR86- [ ] Environment variables split by env (dev / preview / production)87- [ ] Least-privilege `permissions:`; third-party actions pinned to SHA; no secrets exposed to fork PRs8889## Output90- **CI workflow file**: `.github/workflows/ci.yml` with parallel jobs (type-check / lint / test / build) + caching block91- **Branch protection**: rule on `main` requiring all CI status checks to pass + PR review ≥ 192- **Preview deploys**: Vercel/Netlify/Cloudflare Pages project linked to repo, commenting URL per PR93- **Pipeline metrics** (paste into PR description): cold build time / warm cache time / cache hit rate94- **Commit format**: `chore(ci): <change>` for workflow updates; `chore(infra): <change>` for branch protection / env config9596## Implementation9798### React + Next.js (default — GitHub Actions + Vercel)99- Workflow: `actions/checkout` + `actions/setup-node` + `actions/cache`, each pinned to a commit SHA (Dependabot `github-actions` ecosystem bumps them)100- Concurrency: `concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }`101- Permissions: `permissions: contents: read` at the top; widen per-job as needed102- Build cache key: `${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.{js,jsx,ts,tsx}') }}` + restore-keys fallback103- Preview: Vercel project auto-deploys every PR, comments URL on PR104- Deploy auth: Vercel/cloud OIDC over static tokens; instant rollback via prior deployments105- Branch protection: require type-check + lint + test + build status checks106107### Other stacks108- **Vue / Nuxt**: cache `.nuxt/cache` and `node_modules/.cache/nuxt`; deploy preview via Vercel/Netlify (both auto-detect Nuxt)109- **SvelteKit**: cache `.svelte-kit/output`; deploy preview via Vercel/Netlify/Cloudflare Pages (built-in adapters)110- **Angular**: cache `.angular/cache`; deploy preview via Firebase Hosting or Vercel111- **Other CI platforms**: GitLab CI, CircleCI, Buildkite — same parallel structure (jobs DAG), different YAML syntax112- **Other deploy platforms**: Netlify deploy previews, Cloudflare Pages, Render preview environments — all support PR-triggered builds with unique URLs113- **Universal**: parallel job DAG > sequential steps; caching keyed on lockfile + source hash is the universal pattern; branch protection rules are platform-specific but conceptually identical114115## Related skills116- `test-strategy` — defines what gets gated in CI (and keeps required checks non-flaky)117- `accessibility-audit` — axe-core wired here as a gate118- `security-audit` — least-privilege tokens, SHA-pinned actions, `npm audit` as a CI gate119- `observability-setup` — Sentry release tagging coordinated with CI120- `developer-experience` — husky + lint-staged complement CI by gating locally121122## Reference123- **Key insight encoded**: Cache `~/.npm` + `.next/cache` keyed on `package-lock.json` + source hashes — this is the single biggest pipeline speedup. Run type-check / lint / test as parallel jobs (not sequential steps) to hit sub-3-minute warm pipelines. The `next build` step is usually the long pole; its caching benefit compounds across PRs. Two senior must-haves often missed: `concurrency: cancel-in-progress` (kill stale runs on a new push) and treating the workflow as a security boundary — least-privilege `permissions:`, SHA-pinned actions, and no secrets for fork PRs.