Quality Gates
What Is a Quality Gate
A quality gate is an automated threshold that blocks pipeline progression when not met. Not a warning. A hard stop.
Tests pass + Coverage ≥ 90% + No critical vulnerabilities → proceed
Any gate fails → pipeline stops, developer is notified
Quality gates make quality non-negotiable — it cannot be "just this once" skipped.
Test Pass Rate Gate
The simplest gate: 100% of tests must pass (excluding quarantined).
- name: Run tests
run: uv run pytest -n auto --junitxml=test-results/junit.xml
- name: Enforce pass rate
run: |
python scripts/check_pass_rate.py \
--junit test-results/junit.xml \
--min-pass-rate 1.0
A single unexpected failure blocks the pipeline. This is correct behaviour.
Coverage Gate
- name: Test with coverage
run: |
uv run pytest \
--cov=src \
--cov-fail-under=90 \
--cov-report=xml:coverage.xml \
--cov-report=term-missing
--cov-fail-under=90 exits with code 1 if coverage drops below 90%.
The pipeline stops. The developer either adds tests or explains why coverage dropped.
Coverage by Layer
| Layer | Threshold |
|---|---|
| Unit tests | ≥ 95% |
| Integration | ≥ 80% |
| Overall | ≥ 90% |
Track coverage delta per PR to catch gradual erosion:
- name: Coverage comment on PR
uses: py-cov-action/python-coverage-comment-action@v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MINIMUM_GREEN: 90
MINIMUM_ORANGE: 80
Performance Gate
Enforce latency SLOs as a pipeline gate:
# scripts/check_slo.py — runs after load test
from locust.env import Environment
from locust import events
@events.quitting.add_listener
def assert_slo(environment: Environment, **kwargs) -> None:
stats = environment.runner.stats.total
p95 = stats.get_response_time_percentile(0.95)
error_rate = stats.fail_ratio
violations = []
if p95 > 300:
violations.append(f"p95={p95}ms exceeds 300ms SLO")
if error_rate > 0.01:
violations.append(f"error_rate={error_rate:.1%} exceeds 1% SLO")
if violations:
for v in violations:
print(f"SLO BREACH: {v}")
environment.process_exit_code = 1
- name: Load test with SLO gate
run: |
locust -f tests/performance/locustfile.py \
--headless -u 50 -r 5 --run-time 60s \
--host ${{ vars.STAGING_URL }}
Dependency Vulnerability Gate
Block deployment if critical CVEs found in dependencies:
- name: Vulnerability scan
run: uv run pip-audit --fail-on-vuln --severity critical
Or with Trivy for Docker images:
- name: Scan Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
exit-code: '1'
severity: 'CRITICAL,HIGH'
Gate Summary
| Gate | Tool | Failure Action |
|---|---|---|
| Tests pass | pytest | Block merge |
| Coverage ≥ 90% | pytest-cov | Block merge |
| No lint errors | ruff | Block merge |
| Type safety | mypy | Block merge |
| p95 < 300ms | Locust | Block staging→prod |
| No critical CVEs | pip-audit / Trivy | Block deploy |
| Smoke tests pass | pytest | Block production |
Required Status Checks
Enforce gates at the repository level (GitHub branch protection):
Settings → Branches → main → Require status checks:
✓ build
✓ test / unit
✓ test / integration
✓ security-scan
No merge without all checks green. Not optional. Not overridable without admin.