mdashikjs/blog
All posts
A Production-Grade CI Pipeline with GitHub Actions
DevOps

A Production-Grade CI Pipeline with GitHub Actions

DevOps4 min

A Production-Grade CI Pipeline with GitHub Actions

Our CI pipeline runs lint, type-check, tests, build, and security audit in under 3 minutes. Here is the exact workflow file and the decisions behind each step.

GitHub ActionsCI/CDNode.jsTesting
Share:

The full workflow

name: CI
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm lint

      - name: Type check
        run: pnpm tsc --noEmit

      - name: Test
        run: pnpm test -- --coverage

      - name: Build
        run: pnpm build

      - name: Audit
        run: pnpm audit --audit-level=high

Key decisions

Concurrency group: If I push twice to the same branch, the first run is cancelled. No point finishing a stale build.

Frozen lockfile: --frozen-lockfile ensures CI installs exactly what is in the lockfile. If someone forgot to commit pnpm-lock.yaml after adding a dependency, CI fails instead of silently resolving different versions.

Cache: The cache: pnpm option in setup-node caches the pnpm store between runs. First run: 45s install. Subsequent runs: 5s.

Parallelizing steps

For larger projects, split into parallel jobs:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]
  test:
    runs-on: ubuntu-latest
    steps: [...]
  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps: [...]

Lint and test run in parallel. Build only runs if both pass.

Branch protection

The workflow is useless without branch protection rules. Require the quality job to pass before merging. No exceptions. The 30 seconds it adds to every PR saves hours of debugging broken main branches.

What I would add next

  • Playwright e2e tests on a separate job (they are slow and flaky enough to warrant isolation)
  • Bundle size check that comments on the PR with the diff
  • Database migration dry run to catch SQL errors before deploy
MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.