PR Checks That Matter — Shift-Left Without Noise
Fast feedback on pull requests accelerates delivery. Learn how to design PR checks that catch real issues early, use path filters to skip irrelevant checks, and balance required vs. optional checks.
This post is part 5 of the CI/CD Quality series. Part 4 covered reports and failure triage — making CI failures actionable.
Introduction — Shift-Left Is About Speed, Not Volume
“Shift-left” means catching issues earlier in the development cycle. The earlier you find a defect, the cheaper it is to fix.
But shift-left is not “run every check on every PR.” That’s how you get:
- 20-minute CI runs that block merges
- Developers waiting for unrelated checks to pass
- False positives from checks that don’t apply to the changed code
Smart PR checks are fast, relevant, and blocking only when necessary.
This post covers how to design PR checks that provide fast feedback without creating bottlenecks, use path filters to skip irrelevant checks, and balance required vs. optional checks.
Fast Feedback vs. Slow Suites
The goal of PR checks is fast feedback. If a developer waits 20 minutes for CI, they context-switch to another task. When CI completes, they’ve already moved on.
Target CI Times
| Check Type | Target Time | Purpose |
|---|---|---|
| Lint | < 30 seconds | Catch style violations |
| Build | < 2 minutes | Ensure code compiles |
| Unit tests | < 3 minutes | Validate business logic |
| Smoke tests | < 5 minutes | Catch critical regressions |
| Full E2E suite | 10–20 minutes | Comprehensive validation (run post-merge) |
Principle: Block merges on fast checks (< 5 minutes). Run slow suites post-merge or on schedules.
:::warning[The 5-Minute Rule] If your required PR checks take longer than 5 minutes, developers will start bypassing them. Fast feedback keeps CI trusted. :::
Path Filters — Run Only Relevant Checks
Not every PR needs every check. If you change documentation, you don’t need to run E2E tests.
GitHub Actions supports path filters to trigger workflows only when specific files change.
Example: Skip E2E Tests for Docs Changes
name: E2E Tests
on:
pull_request:
paths:
- 'src/**'
- 'tests/**'
- 'package.json'
- 'playwright.config.ts'
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e
Result: E2E tests run only when src/, tests/, or test config changes. Docs PRs skip this workflow entirely.
Example: Separate Workflows for Frontend and Backend
# .github/workflows/frontend-ci.yml
name: Frontend CI
on:
pull_request:
paths:
- 'frontend/**'
- 'package.json'
jobs:
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
# .github/workflows/backend-ci.yml
name: Backend CI
on:
pull_request:
paths:
- 'backend/**'
- 'requirements.txt'
jobs:
backend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest
Result: Frontend changes only run frontend tests. Backend changes only run backend tests. No wasted CI minutes.
:::tip[Combining Path Filters]
Use paths-ignore to exclude specific files. Example: paths-ignore: ['docs/**', '*.md'] skips workflows for documentation changes.
:::
Required vs. Optional Checks
GitHub allows you to mark specific checks as required before a PR can be merged.
Repository Settings → Branches → Branch protection rules → Require status checks to pass before merging
Recommended Required Checks
✅ Build — Code must compile
✅ Lint — Code style must pass
✅ Unit tests — Business logic must be correct
✅ Smoke tests — Critical paths must work
Recommended Optional Checks
ℹ️ Full E2E suite — Informational; failures investigated post-merge
ℹ️ Coverage report — Trends tracked; no hard threshold
ℹ️ Performance benchmarks — Regressions flagged but not blocked
Rationale: Required checks are fast, deterministic, and block obvious defects. Optional checks provide context without blocking delivery.
Conditional Checks Based on Labels
Use GitHub labels to trigger optional checks on-demand.
Example: Run Full E2E Suite When Labeled
name: Full E2E (On-Demand)
on:
pull_request:
types: [labeled]
jobs:
e2e-full:
if: contains(github.event.pull_request.labels.*.name, 'run-e2e-full')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:e2e
Usage: Add the run-e2e-full label to a PR to trigger the full E2E suite. Remove the label to skip it.
Why this works: Full E2E suites are slow. Run them only when needed — high-risk changes, pre-release validation, or investigating regressions.
Failing Fast — Stop on First Failure
If the build fails, there’s no point running tests. If lint fails, there’s no point running E2E.
Use job dependencies to fail fast.
Example: Sequential Checks with Dependencies
name: CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
lint:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
unit-tests:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
smoke-tests:
runs-on: ubuntu-latest
needs: [build, lint, unit-tests]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e:smoke
Flow:
buildruns first- If
buildsucceeds,lintandunit-testsrun in parallel - If all succeed,
smoke-testsruns - If any job fails, dependent jobs are skipped
Result: Fast failure. If the build fails in 1 minute, CI stops immediately. No wasted time running tests against broken code.
Practical Example — Optimised PR Checks
Combine path filters, required checks, optional checks, and fast failure.
name: PR Checks
on:
pull_request:
paths:
- 'src/**'
- 'tests/**'
- 'package.json'
- 'playwright.config.ts'
jobs:
build:
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
lint:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
unit-tests:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test
smoke-tests:
runs-on: ubuntu-latest
needs: [build, lint, unit-tests]
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 chromium
- run: npm run test:e2e:smoke
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: smoke-test-report
path: playwright-report/
e2e-full:
if: contains(github.event.pull_request.labels.*.name, 'run-e2e-full')
runs-on: ubuntu-latest
needs: [build, lint, unit-tests]
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
- run: npm run test:e2e
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-full-report
path: playwright-report/
Features:
- Path filters skip workflow for docs changes
buildruns first; other jobs depend on itsmoke-testsis required and runs in < 5 minutese2e-fullis optional and runs only when labeled- All reports uploaded as artifacts
Typical PR: 3–4 minutes to complete required checks. Full E2E runs only when explicitly requested.
Auto-Merge for Trusted PRs
For low-risk PRs (dependency updates, automated refactors), enable auto-merge when all checks pass.
Dependabot Auto-Merge
name: Auto-Merge Dependabot
on:
pull_request_target:
jobs:
auto-merge:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable auto-merge
run: gh pr merge --auto --squash "${{ github.event.pull_request.html_url }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Result: Dependabot PRs auto-merge when all checks pass. No manual approval required for low-risk dependency updates.
:::warning[Security Consideration] Only auto-merge PRs from trusted sources (Dependabot, renovate). Never auto-merge external contributor PRs without review. :::
Conclusion — Fast, Relevant, Trusted
PR checks are the front line of quality. They catch issues before merge, provide fast feedback, and keep the main branch stable.
The principles:
- Run fast checks (< 5 minutes) as required checks
- Use path filters to skip irrelevant workflows
- Fail fast with job dependencies
- Make slow suites optional or label-triggered
- Auto-merge trusted PRs to reduce manual overhead
When PR checks are fast and relevant, developers trust them. When developers trust CI, they ship faster.
Action for this week: Measure the P50 and P95 times for your PR checks. If P95 > 10 minutes, identify the slowest job and either optimise it, move it to post-merge, or make it optional.
Next in this series: Part 6 — Continuous Testing in Continuous Delivery