Frequent use 2+ years

CI/CD & Quality Gates

GitHub Actions pipelines, automated quality gates, Lighthouse audits, and deployment workflows.

GitHub ActionsCI/CDLighthouseGitDocker

How I use it

CI/CD is the backbone of a trustworthy test suite. A passing test on a developer's machine means nothing if it never runs on a clean environment against the real build. I wire Playwright, Cucumber, and API tests into GitHub Actions pipelines so every pull request is validated before it touches main — and deployments only happen when all quality gates pass.

Pipeline: pull request validation

This is the core PR pipeline. It runs in parallel — unit/API tests and E2E tests on separate jobs so fast feedback arrives before the slower browser tests finish. The deploy job is blocked until both pass.

name: CI

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

env:
  NODE_VERSION: '20'

jobs:
  api-tests:
    name: API & Unit Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci

      - name: Run API tests
        run: npm run test:api
        env:
          API_BASE_URL: ${{ secrets.STAGING_API_URL }}
          API_TOKEN:    ${{ secrets.STAGING_API_TOKEN }}

      - name: Upload Cucumber report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cucumber-report
          path: reports/cucumber/

  e2e-tests:
    name: E2E Tests (Playwright)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npx playwright install --with-deps chromium

      - name: Run E2E tests
        run: npx playwright test --project=chromium
        env:
          BASE_URL:  ${{ secrets.STAGING_BASE_URL }}
          AUTH_TOKEN: ${{ secrets.STAGING_AUTH_TOKEN }}

      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/

  deploy:
    name: Deploy to Staging
    needs: [api-tests, e2e-tests]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - name: Deploy
        run: ./scripts/deploy.sh staging
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Quality gate job

A dedicated quality gate job runs after deployment and blocks promotion to production if any check fails. It runs Lighthouse against the live staging URL and fails the pipeline if scores drop below thresholds — catching regressions introduced by the build itself, not just the code.

name: Quality Gate

on:
  workflow_run:
    workflows: [CI]
    types: [completed]
    branches: [main]

jobs:
  lighthouse:
    name: Lighthouse Audit
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - uses: actions/checkout@v4

      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          urls: |
            ${{ secrets.STAGING_BASE_URL }}
            ${{ secrets.STAGING_BASE_URL }}/dashboard
          budgetPath: .github/lighthouse-budget.json
          uploadArtifacts: true

  smoke-tests:
    name: Smoke Tests
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci
      - run: npx playwright install --with-deps chromium

      - name: Run smoke suite
        run: npx playwright test --grep @smoke
        env:
          BASE_URL: ${{ secrets.STAGING_BASE_URL }}

  promote:
    name: Promote to Production
    needs: [lighthouse, smoke-tests]
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh production
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Lighthouse budget config

Score thresholds are committed as code so every contributor knows the bar. If a PR introduces a heavy dependency that tanks performance, the pipeline catches it before merge — not after a user complains.

[
  {
    "path": "/*",
    "assertions": {
      "categories:performance":    { "minScore": 0.9,  "aggregationMethod": "pessimistic" },
      "categories:accessibility":  { "minScore": 0.95, "aggregationMethod": "pessimistic" },
      "categories:best-practices": { "minScore": 0.9,  "aggregationMethod": "pessimistic" },
      "categories:seo":            { "minScore": 0.9,  "aggregationMethod": "pessimistic" },
      "first-contentful-paint":    { "maxNumericValue": 2000 },
      "largest-contentful-paint":  { "maxNumericValue": 3000 },
      "total-blocking-time":       { "maxNumericValue": 300  },
      "cumulative-layout-shift":   { "maxNumericValue": 0.1  }
    }
  }
]

Handling test artifacts on failure

Artifacts are uploaded unconditionally with if: always() — so when a test fails in CI and the run is gone by morning, the trace, screenshots, and HTML report are still there. This alone has saved hours of "I can't reproduce it locally" debugging.

      - name: Upload Playwright trace on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-traces-${{ github.run_id }}
          path: |
            test-results/
            playwright-report/
          retention-days: 7

      - name: Notify on failure
        if: failure() && github.ref == 'refs/heads/main'
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": ":red_circle: E2E tests failed on `main` — <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

What I focus on

  • Secrets never in code — all credentials go through GitHub Secrets or environment variables; the workflow YAML is always safe to commit
  • Parallel jobs — fast and slow tests run concurrently; developers get feedback on API tests in ~2 min while E2E tests are still running
  • Artifacts on every failure — traces, screenshots, and reports are uploaded unconditionally so flaky failures leave forensic evidence
  • Quality gates as code — Lighthouse budgets and smoke test thresholds are versioned alongside the application, not configured in a dashboard
  • Environment promotion gates — staging and production are separate GitHub Environments; promotion requires explicit quality gate passage, not manual approval alone