CI/CD & Quality Gates
GitHub Actions pipelines, automated quality gates, Lighthouse audits, and deployment workflows.
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