Quality Gates in CI Pipelines
Not every failure should block a merge. Learn which quality gates must block pipelines, which should inform, and how to design gates that prevent bad code from reaching production without slowing.
This post is part 1 of the CI/CD Quality series. We’ll explore practical strategies for building quality into continuous integration and delivery pipelines.
Introduction — The Merge Button Is a Decision Point
Every pull request has a merge button. That button represents a decision: is this code ready for production?
In teams without automation, that decision relies on human judgment, code review, and hope. In teams with CI/CD, that decision is informed by automated quality gates.
A quality gate is a checkpoint in your CI pipeline that evaluates whether code meets specific quality criteria. It can pass or fail. When it fails, the pipeline stops, and the merge is blocked.
The critical question is: which checks should block the merge, and which should only inform?
Block too little, and broken code reaches production. Block too much, and your team works around the pipeline to ship faster.
:::warning[Blocking Everything Is Not Quality] If your CI pipeline fails 40% of the time due to flaky tests, developers will stop trusting it. They’ll click “merge anyway” or disable the check. A gate that fails unreliably is worse than no gate at all. :::
This post defines what quality gates are, which ones should block merges, and how to design gates that protect production without creating false bottlenecks.
What Must Block a Merge
Not every quality check deserves to block a merge. The bar for blocking is high: this code will cause production incidents or violate non-negotiable constraints.
Critical Quality Gates
1. Build must succeed
If the code doesn’t compile or bundle, it cannot run. This is non-negotiable.
# .github/workflows/ci.yml
name: CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
Why block: If the build fails, deployment is impossible. Blocking here prevents wasting time on a PR that cannot ship.
2. Linting must pass
Linters catch syntax errors, unused variables, import violations, and style inconsistencies. These are cheap to fix and prevent entire categories of runtime errors.
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
Why block: A linting failure often indicates incomplete refactoring or broken imports. If npm run lint fails locally, the code is not ready.
3. Unit tests must pass
Unit tests validate isolated business logic. If a unit test fails, a developer explicitly broke existing functionality or introduced a regression.
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
Why block: Unit tests fail deterministically when logic is wrong. A failing unit test is a clear signal: this code is not ready.
4. Smoke tests must pass
Smoke tests are a minimal subset of end-to-end tests that verify the application starts, critical pages render, and authentication works. They run in under 5 minutes.
smoke-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e:smoke
Why block: If the homepage doesn’t load or login is broken, the application is not deployable. Smoke tests catch catastrophic failures before they reach production.
5. Security scans must not find critical vulnerabilities
Dependency scanning tools like npm audit, Snyk, or Dependabot flag known CVEs in your dependencies.
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm audit --audit-level=high
Why block: A critical vulnerability is a production incident waiting to happen. Block the merge and fix or suppress it explicitly.
:::tip[Audit Levels]
Use --audit-level=high to block only high and critical vulnerabilities. Blocking on moderate or low findings creates noise and slows teams down without proportional security benefit.
:::
What Should Inform, Not Block
Some checks are valuable but should not block merges. They provide information for decision-making but do not represent a hard constraint.
1. Test coverage percentage
Coverage reports show which lines of code are tested. This is useful context, but a coverage drop does not mean the code is broken.
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test -- --coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Why inform only: Coverage is a proxy metric. High coverage does not guarantee quality; low coverage does not guarantee defects. Use it as a signal, not a gate.
2. Full E2E test suites
Full end-to-end test suites take 15–30 minutes to run. They test realistic user journeys across the full application stack.
e2e-full:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npx playwright install --with-deps
- run: npm run test:e2e
Why inform only: E2E tests are valuable but slow and occasionally flaky. Block on smoke tests; inform with full E2E. Ship fast, catch edge cases in staging.
3. Performance benchmarks
Performance tests flag regressions in response time, memory usage, or bundle size.
performance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npm run perf:benchmark
Why inform only: Performance regressions are often acceptable trade-offs for feature delivery. Flag them, but let the team decide whether to address before or after merge.
The Flaky Gate Anti-Pattern
A flaky gate is a quality check that fails intermittently without code changes. Flaky tests are the single most destructive force in CI/CD pipelines.
Why Flaky Tests Break Teams
- Developers stop trusting the pipeline
- Teams click “merge anyway” or disable checks
- Real failures are ignored because “it’s probably flaky”
- Time is wasted re-running builds
:::danger[Flaky Gates Destroy Trust] If your CI pipeline has a 10% flake rate, and developers run it 10 times per day, they see false failures twice daily. After a week, they’ve stopped paying attention. Your gate is now decoration. :::
How to Handle Flaky Tests
1. Do not block on flaky tests
If a test suite has >2% flake rate, do not make it a required check. Move it to an informational check or disable it until it’s fixed.
e2e-flaky:
runs-on: ubuntu-latest
continue-on-error: true # Does not block merge
steps:
- run: npm run test:e2e
2. Quarantine flaky tests
Tag flaky tests with @flaky and skip them in CI. Track them in a backlog. Fix or delete them.
test.skip('checkout flow completes', async ({ page }) => {
// Flaky due to third-party payment iframe timing
// TODO: Fix or replace with API test
});
3. Fix the root cause
Most flaky E2E tests fail due to:
- Race conditions (missing
await, improper waits) - Hardcoded timeouts (
page.waitForTimeout(5000)) - Shared test data (parallel tests mutating the same DB records)
Playwright’s auto-waiting solves most of these. Use it.
:::tip[Flake-Free E2E Tests]
Use Playwright’s built-in locators and assertions. They auto-wait and auto-retry. Avoid page.waitForTimeout() and manual sleep() calls. Read Part 2 of the Playwright series for details.
:::
Practical Example — GitHub Actions Required Checks
GitHub allows you to mark specific jobs as required before a PR can be merged.
Repository Settings → Branches → Branch protection rules → Require status checks to pass before merging
Mark these as required:
- ✅
build - ✅
lint - ✅
unit-tests - ✅
smoke-tests - ✅
security-scan
Leave these as optional:
- ℹ️
e2e-full - ℹ️
coverage - ℹ️
performance
This configuration protects production without creating false bottlenecks.
Conclusion — Gates That Serve the Team
Quality gates are not about control — they’re about confidence. A well-designed gate tells you: this code will not break production in obvious ways.
The principles:
- Block on failures that make deployment impossible (build, lint, unit tests, smoke tests, critical security issues)
- Inform on failures that provide useful context (coverage, full E2E, performance)
- Never block on flaky tests — they destroy trust faster than they catch bugs
Quality gates should accelerate delivery by catching critical issues early. If your gates slow the team down or get bypassed regularly, the gates are misconfigured.
Action for this week: Review your CI pipeline. Identify one check that blocks merges but fails >5% of the time. Either fix it or move it to informational-only. Trust is built by reliability.
Next in this series: Part 2 — GitHub Actions for Test Automation