GitHub Actions is the most accessible CI/CD platform for most teams today. But having a pipeline is not enough. The difference between a pipeline that delivers confidence and one that is just going through the motions is quality gates — hard stops that prevent bad code from advancing.

What a Quality Gate Actually Is

A quality gate is a condition that must be met before a job can proceed. In GitHub Actions, gates are implemented through three mechanisms: job dependencies (needs), status checks on pull requests, and branch protection rules. Together, they form a pipeline that physically cannot deploy unless all quality criteria pass.

Complete Quality Pipeline YAML

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

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

jobs:
  lint:
    name: 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
      - run: npm run lint
      - run: npm run type-check

  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm run test:coverage
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/

  e2e-tests:
    name: E2E Tests (Playwright)
    runs-on: ubuntu-latest
    needs: unit-tests
    strategy:
      matrix:
        browser: [chromium, firefox]
    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 ${{ matrix.browser }}
      - run: npx playwright test --project=${{ matrix.browser }}
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/

  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: [lint, unit-tests, e2e-tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: ./scripts/deploy.sh
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Branch Protection Rules

Workflow files alone are not enough. A developer can bypass them by pushing directly to main. Branch protection rules enforce that all status checks must pass before merging:

  1. Go to your repo → Settings → Branches
  2. Add a rule for the main branch
  3. Enable: Require status checks to pass before merging
  4. Add: lint, unit-tests, e2e-tests (chromium), e2e-tests (firefox)
  5. Enable: Require branches to be up to date before merging
  6. Enable: Do not allow bypassing the above settings
Do not allow bypassing: This is the most commonly skipped setting, and it is the most important one. Even repository admins should not be able to bypass quality gates under normal circumstances.

Making Pull Requests Quality Reviews

Configure your PR template to include quality context:

.github/pull_request_template.md
## What does this PR do?

## How was it tested?
- [ ] Unit tests added/updated
- [ ] E2E tests added/updated  
- [ ] Tested manually on staging
- [ ] Visual snapshots updated (if UI changed)

## Quality checklist
- [ ] No new lint warnings
- [ ] Coverage did not decrease
- [ ] No flaky tests introduced
- [ ] data-testid attributes added for new UI elements

// Key Takeaways