A CI/CD pipeline without quality gates is just a fast way to ship broken software. The pipeline is the last line of automated defence between your code and your users — and if it is not enforcing quality at every stage, you are effectively deploying on hope.

In this article, I will walk you through building a quality-first CI/CD pipeline from scratch using GitHub Actions, covering every stage from code commit to production deployment. This is the pipeline architecture I have refined across multiple teams and products.

The Quality Gate Philosophy

A quality gate is a checkpoint that code must pass before it can proceed to the next stage. Gates should be fast, automated, and non-negotiable. If a gate fails, the pipeline stops. No exceptions. No "we'll fix it later." A failing gate is a signal, and ignoring that signal is how you accumulate technical debt and production incidents.

The gates I will cover, in order of execution:

  1. Static analysis (lint + type check)
  2. Unit tests with coverage threshold
  3. Integration tests
  4. E2E tests with Playwright
  5. Visual regression (snapshot tests)
  6. Performance budget check
  7. Security scan

Stage 1 — Static Analysis Gate

This is the cheapest gate to run and it catches a surprising number of issues. Lint rules enforce code style and catch obvious errors. TypeScript compilation catches type errors before runtime. This gate should run in under 60 seconds.

.github/workflows/quality.yml
name: Quality Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  static-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Lint check
        run: npm run lint
      - name: Type check
        run: npm run type-check

Stage 2 — Unit Test Gate with Coverage

Unit tests verify individual functions and components in isolation. More importantly, the coverage threshold ensures your team cannot merge code without adequate test coverage. I recommend starting at 70% and increasing to 80%+ over time.

.github/workflows/quality.yml (continued)
  unit-tests:
    runs-on: ubuntu-latest
    needs: static-analysis
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - name: Run unit tests with coverage
        run: npm run test:coverage
      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

In your jest.config.ts, enforce coverage thresholds:

jest.config.ts
export default {
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

Stage 3 — E2E Tests with Playwright

E2E tests are the most expensive gate to run but the most representative of real user behaviour. I run them in parallel across Chromium and Firefox to save time. Critical user flows — login, checkout, key workflows — must all pass before deployment proceeds.

.github/workflows/quality.yml (continued)
  e2e-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium firefox
      - name: Run E2E tests
        run: npx playwright test
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/

Stage 4 — Visual Regression Gate

Visual regression tests catch unintended UI changes — a CSS change that accidentally breaks a layout, a font that loaded differently, a component that shifted position. These are the bugs that pass all functional tests but still make users confused or angry.

Important: Always upload failing visual diffs as CI artifacts. Your team needs to see exactly what changed visually to decide whether it is an intentional change or a regression.

Stage 5 — Deploy Gate (Production Only)

Only after all previous gates pass does the pipeline proceed to deployment. The deploy job uses needs to depend on all quality jobs, creating a hard dependency chain:

.github/workflows/quality.yml (continued)
  deploy:
    runs-on: ubuntu-latest
    needs: [static-analysis, unit-tests, e2e-tests]
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: ./scripts/deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Making the Pipeline Fast

The biggest resistance to quality gates is speed. If the pipeline takes 45 minutes, developers will start bypassing it. Keep it fast by:

Target: Your full quality pipeline should complete in under 15 minutes for most projects. If it takes longer, optimise parallelism and caching before reducing coverage.

// Key Takeaways