Stop Letting Code Review Become a Toll Booth: Automation That Keeps Quality High and Delivery Fast
Block on signal, not on vibes. Here’s the paved-road setup that cuts PR cycle time without gambling on quality.
Block on signal, not on vibes.Back to all posts
The day your PRs started moving like syrup
If you’ve ever watched a team “improve quality” by adding 14 required checks and a mandatory two-reviewer policy, you already know how this movie ends. PRs pile up. Engineers babysit flaky CI. Product asks why a three-line config tweak takes two days to merge. Meanwhile, bugs still slip through because humans are rubber-stamping under time pressure.
I’ve seen this fail at unicorns and banks alike. The fix isn’t more gates; it’s better signal and a paved road that makes the right thing the easy thing.
Block on signal, not on vibes.
What actually matters in a healthy review pipeline
Let’s reduce this to brass tacks. Code review serves three outcomes:
- Catch high-risk defects early (security, correctness, data integrity)
- Maintain consistency (style, conventions, architecture boundaries)
- Spread knowledge (context, ownership, change awareness)
You don’t need bespoke bots to achieve that. You need a boring, paved-road setup:
- A single formatter per language enforced locally and in CI.
- A single linter with a conservative rule set.
- Fast unit tests. Slow/e2e tests run post-merge or behind a flag.
- A lightweight policy gate for obviously bad changes (secrets, infra drift, prohibited patterns).
- A merge queue so green changes land without roulette.
- CODEOWNERS plus size/risk heuristics to focus humans where they matter.
Everything else (SAST firehoses, dependency CVE spam, AI nits) starts advisory. Graduate only high-signal findings to “required.”
A paved-road blueprint you can copy
Here’s the minimal setup we deploy at GitPlumbers when a client asks for “fast reviews without quality theater.” It’s GitHub-flavored, with GitLab alternatives noted.
Local: pre-commit as the first line of defense
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.3.3
hooks:
- id: prettier
additional_dependencies: ["prettier@3.3.3"]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/golangci/golangci-lint
rev: v1.61.0
hooks:
- id: golangci-lint- One formatter per language:
prettier,ruff-format,gofmtviagolangci-lint. - Runs fast locally; mirrors in CI so “it worked on my laptop” doesn’t sneak in.
GitLab: same idea with pre-commit and a pre-commit job in .gitlab-ci.yml.
CI: only four required checks
# .github/workflows/ci.yml
name: ci
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci --prefer-offline --no-audit
- run: npx prettier -c "**/*.{js,ts,css,md}"
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci --prefer-offline --no-audit
- run: npx eslint . --max-warnings=0
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci --prefer-offline --no-audit
- run: npm test -- --ci --reporters=default --reporters=github-actions
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: p/ci # curated minimal ruleset
generateSarif: true
publishToken: ${{ secrets.SEMGREP_APP_TOKEN }}- Required checks:
format,lint,test,policy. - Everything else (SAST deep, license scan, e2e) is optional or runs post-merge on
main.
GitLab: mirror with four rules:if $CI_PIPELINE_SOURCE == "merge_request_event" jobs.
Human routing: CODEOWNERS, size labels, and templates
# .github/CODEOWNERS
# Keep it coarse; avoid per-file snowflakes
/apps/** @frontend-team
/services/** @backend-team
/infrastructure @platform-team# .github/labeler.yml (with github/labeler)
size/XS:
- changed-files: '<=5'
size/S:
- changed-files: '6..20'
size/L:
- changed-files: '50..'<!-- .github/pull_request_template.md -->
### What changed
-
### Why
-
### Risk
- [ ] Low (config, docs, tests)
- [ ] Medium (code paths behind flag)
- [ ] High (data model, auth, payments)
### Rollout
- [ ] Canary
- [ ] Feature flag
- [ ] Dark launchThese simple affordances beat any “AI reviewer” at focusing attention.
Merge queue instead of merge roulette
- Enable GitHub Merge Queue on
mainand require green + 1 approval. - For
size/XSwith “Low” risk, auto-add to queue on green. - For
size/Lor marked “High” risk, require@platform-teamreview.
On GitLab, use Merge Trains with Required Pipelines.
Cost/benefit: two real before/afters
- Growth-stage SaaS (Node/Go monorepo on GitHub)
- Before: 13 required checks, flaky Cypress gating merges, two mandatory reviewers. Median PR cycle time 3.1 days. 22% re-review rate. Weekly deploys.
- After (6 weeks): 4 required checks, e2e moved post-merge + canary, merge queue enabled, CODEOWNERS coarse-grained. Median PR cycle time 1.2 days. Re-review 8%. Deploys 3–5/day. Defect rate flat; MTTR down 27% due to faster rollbacks.
- Fintech (Python services on GitLab)
- Before: Bespoke SAST, custom bot commenting nits, 45-minute MR pipeline blocking; security exceptions via email. Engineers waited ~1h for a retry slot.
- After (8 weeks):
black+ruff+pytestas required jobs; SAST downgraded to advisory with Semgrep-curated rules; Merge Trains; security exceptions codified via policy tags. MR lead time from 18h to 6h, pipeline cost down ~38% by cutting parallel heavy scans from MR pipelines.
The theme: fewer, better gates yield faster and safer delivery. Quality didn’t suffer; noise did.
What to block vs. what to advise
If you only dig one hole, dig this one. Block on checks that are:
- Deterministic and fast (<5 minutes cumulative)
- Objective (formatter, linter with zero warnings, unit tests)
- High-signal policy (secrets, obvious vulns, protected file edits)
Advise on checks that are:
- Probabilistic or noisy (broad SAST, transient e2e)
- Context-heavy (architecture nits, micro-optimizations)
- Expensive (long perf tests, fuzzing)
Use reviewdog or native annotations to surface advisory findings without failing the PR.
# Example: ESLint as annotations instead of PR-fail
- name: Lint (advisory)
if: ${{ github.event_name == 'pull_request' }}
run: |
npx eslint . -f json -o eslint.json || true
- uses: reviewdog/action-eslint@v1
with:
reporter: github-pr-review
eslint_input: eslint.json
level: warningPromote a rule to “required” only after you’ve tracked its precision for a sprint and seen >95% signal.
Security and compliance without death-by-scan
Security teams often show up with 600 “Criticals” that block merges. I’ve watched that stall entire quarters. Here’s what actually works:
- Start with Semgrep’s
p/cior a trimmed ruleset aligned to your stack. Tag rules with owners. - Secret scanning is always required (GitHub Advanced Security or
gitleaks). - License checks are advisory unless a hard block is truly necessary; route violations to legal via labels.
- Container/image scans (
trivy) run post-merge on nightly builds; auto-create issues for high severity in active images. - Infra-as-code policy via OPA/Conftest or Checkov: block only on rules you’ve validated in staging.
# Lightweight IaC policy gate (advisory -> promotable)
- name: Checkov (advisory)
uses: bridgecrewio/checkov-action@v12
with:
directory: infrastructure/
soft_fail: true # flip to false for specific rules once vettedThe trick is staged enforcement with clear owners and SLAs, not Big Red Buttons.
Keep it boring: paved-road defaults beat bespoke bots
I’ve ripped out more homegrown “review assistants” than I can count. They rot. People leave. APIs change. Meanwhile, off-the-shelf tools improved dramatically.
Use defaults:
- Formatters:
prettier,black,gofmt - Linters:
eslint,ruff,golangci-lint - Policy:
semgrepcurated,checkov/OPA for IaC - Orchestrator: GitHub Actions or GitLab CI, not an internal Jenkins museum piece
- Dependency updates:
renovatewith automerge for dev/test and patch versions
// renovate.json
{
"extends": ["config:recommended"],
"packageRules": [
{ "matchUpdateTypes": ["minor", "patch"], "automerge": true },
{ "matchManagers": ["npm"], "rangeStrategy": "bump" }
]
}This keeps your review pipeline maintainable by mortals, not just the one staff engineer who wrote The Bot.
Rollout plan and guardrails against check sprawl
Don’t big-bang this. Do it in three sprints:
Sprint 1: Baseline and prune
- Measure current PR cycle time, re-review rate, flake rate.
- Cut required checks to formatter + linter + unit tests + policy. Everything else advisory.
- Enable Merge Queue/Merge Trains.
Sprint 2: Pave the road
- Add
pre-commit. Mirror checks locally and in CI. - Introduce CODEOWNERS and size labels. Add PR template.
- Stand up Semgrep curated rules; soft-fail first.
- Add
Sprint 3: Harden and automate
- Graduate proven policy rules to required.
- Enable Renovate with automerge for safe updates.
- Move slow e2e to post-merge canaries; wire alerts to SLOs.
Governance:
- Quarterly “CI budget” review: every required check needs a metric and an owner. If it’s flaky >1% or adds >3 minutes, fix or demote.
- Keep required checks under five. If someone wants a new one, remove or consolidate another.
- Publish the paved road in your engineering handbook; make opt-outs explicit and time-bound.
AI note: I like AI for generating PR summaries and diff highlights. I don’t let it gate merges. Hallucinated “security issues” at 4pm Friday are not a vibe you want.
Key takeaways
- Block on a tiny set of high-signal, low-noise checks; make everything else advisory.
- Standardize on a paved road: formatter + linter + unit tests + minimal policy checks + merge queue.
- Shift-left with pre-commit and CI mirrors to avoid rework and idle time.
- Use CODEOWNERS and size/risk heuristics to route human attention where it matters.
- Measure PR cycle time, re-review rate, and flake rate; prune checks quarterly to fight sprawl.
- Prefer proven tools and hosted scanners over bespoke bots—reduce blast radius and maintenance.
Implementation checklist
- Pick one formatter and enforce it with pre-commit and CI (e.g., Prettier, Black, gofmt).
- Select a single linter per language (ESLint, golangci-lint, Ruff) and adopt sane configs.
- Add Semgrep with a curated ruleset; start in advisory mode, promote only high-signal rules to block.
- Define branch protection with exactly 2-4 required checks: format, lint, unit tests, policy.
- Configure CODEOWNERS and keep it coarse; add size-based labels to route reviews.
- Adopt a merge queue and require green + approved; ban direct merges to main.
- Automate dependency updates with Renovate and make safe updates auto-merge via the queue.
- Track PR cycle time, flaky test rate, and re-review rate; review and prune every quarter.
Questions we hear from teams
- How do we stop required checks from creeping back in?
- Set a hard cap (e.g., 5). New required checks must replace an existing one and come with an owner, metric, and SLO (precision >95%, runtime <2m). Review quarterly; demote flaky/noisy checks.
- What about monorepos with mixed stacks?
- Scope checks by path. Run only language-relevant jobs for changed files. Use CODEOWNERS by directory and per-path CI matrices. Keep the global required set tiny; everything else is dynamic and optional.
- Can we trust AI to review PRs?
- Use AI for summaries, risk flags, and TODO aggregation. Don’t block merges on AI judgments. Treat it like a helpful intern: great at drafting, not authorized to push the red button.
- Where do e2e and performance tests fit?
- Post-merge on canaries or nightly. Gate promotions, not merges. Use feature flags and a rollback-first posture. If you must run them pre-merge, keep them advisory and sample-based.
- How do we keep security happy without sandbagging delivery?
- Stage enforcement. Start advisory with curated Semgrep and Checkov. Promote a small set of validated rules to required. Route exceptions via code (labels, tags), not email. Publish SLAs and dashboards.
Ready to modernize your codebase?
Let GitPlumbers help you transform AI-generated chaos into clean, scalable applications.
