Skip to main content

Pre-Merge Quality Gates

A CI pipeline that runs tests is only useful if it can block bad code from reaching the main branch. Quality gates are enforced checks that must pass before a pull request can be merged. Without them, developers can ignore failing tests and merge anyway.

Branch Protection Rules

GitHub lets you protect branches with rules that enforce your team's standards. Navigate to your repository's Settings > Branches > Add branch protection rule.

Key settings for the main branch:

  • Require a pull request before merging: No direct pushes to main. All changes go through PRs.
  • Require status checks to pass before merging: Select the CI jobs that must succeed.
  • Require branches to be up to date before merging: The PR branch must include the latest changes from main.
  • Require review approval: At least one team member must approve the PR.
  • Do not allow bypassing the above settings: Even admins must follow the rules.

Required Status Checks

Status checks are the CI jobs that must pass. When you configure them in branch protection, GitHub shows each check on the PR page with a green checkmark or red X.

Select specific job names from your workflow:

jobs:
  lint-and-typecheck:
    name: Lint & Type Check      # This name appears as a status check
    # ...

  unit-tests:
    name: Unit Tests (18)        # Matrix jobs get the matrix value appended
    name: Unit Tests (20)
    # ...

  e2e-tests:
    name: E2E Tests
    # ...

In branch protection settings, mark these as required:

  • "Lint & Type Check"
  • "Unit Tests (20)"
  • "E2E Tests"

The merge button stays disabled until all required checks pass.

PR-Level Quality Checks

Beyond test results, you can enforce other quality standards in your pipeline:

Lint Check

Catch code style issues before review:

lint:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: 'npm'
    - run: npm ci
    - run: npx eslint . --max-warnings 0

The --max-warnings 0 flag treats warnings as errors. This prevents lint warnings from accumulating over time.

Type Check

Ensure TypeScript compiles without errors:

typecheck:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: 'npm'
    - run: npm ci
    - run: npx tsc --noEmit

Bundle Size Check

Prevent bundle size regressions:

bundle-size:
  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 build
    - run: npx bundlesize

Commit Message Format

Enforce conventional commits using a GitHub Action:

commit-lint:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: 'npm'
    - run: npm ci
    - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}

Blocking Merge on Failure

When a required check fails, the PR page shows clearly what went wrong:

 Lint & Type Check           passed
 Unit Tests (20)             failed
 E2E Tests                   skipped (depends on Unit Tests)

The merge button is disabled with a message: "Required status checks must pass before merging." Developers must fix the failing tests, push a new commit, and wait for the pipeline to pass.

Handling Flaky Tests

Flaky tests that randomly fail create a serious problem for quality gates. Developers lose trust and start retrying until they get green, or worse, asking to remove the check.

Strategies for handling flakiness:

# Allow retries in the test configuration
e2e-tests:
  runs-on: ubuntu-latest
  steps:
    - run: npx playwright test --retries=2

Additionally, track flaky tests over time:

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results.json' }],
  ],
});

Review the JSON results to identify tests that needed retries. Fix flaky tests as a priority — they erode confidence in the entire pipeline.

CODEOWNERS for Critical Paths

GitHub's CODEOWNERS file can require specific reviewers for sensitive areas:

# .github/CODEOWNERS
# CI configuration requires DevOps review
.github/workflows/    @devops-team

# Security-sensitive code requires security review
src/auth/             @security-team

# Database migrations require DBA review
migrations/           @dba-team

This ensures domain experts review changes in their area, adding a human quality gate alongside the automated ones.

Key Takeaways

  • Branch protection rules prevent merging code that fails required status checks.
  • Mark your critical CI jobs (lint, type check, tests) as required status checks.
  • Use --max-warnings 0 with ESLint and --noEmit with TypeScript for strict checks.
  • Handle flaky tests with retries and tracking — do not remove the quality gate.
  • Use CODEOWNERS to require expert review for sensitive code paths.