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
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:
- Go to your repo → Settings → Branches
- Add a rule for the
mainbranch - Enable: Require status checks to pass before merging
- Add:
lint,unit-tests,e2e-tests (chromium),e2e-tests (firefox) - Enable: Require branches to be up to date before merging
- Enable: Do not allow bypassing the above settings
Making Pull Requests Quality Reviews
Configure your PR template to include quality context:
## 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
- Use
needsdependencies to create a hard quality gate chain in your pipeline. - Branch protection rules are the enforcement layer — without them, gates can be bypassed.
- Always upload test reports and coverage as CI artifacts for every run.
- A PR template with quality checkboxes creates accountability and documents what was tested.