Good code review automation catches issues early without annoying developers. Bad automation slows everything down and gets ignored. Here’s how to build the good kind.
The Goal: Fast, Useful Feedback Code Review Automation
: Automated systems that analyze pull requests for issues including bugs, security vulnerabilities, style violations, and test coverage—providing feedback before human reviewers engage.
Automation should:
Run in under 5 minutes for most PRs Catch real issues, not noise Provide actionable feedback Not block urgent changes unnecessarily Layer 1: Branch Protection Start here. This is built into GitHub.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Settings → Branches → Branch protection rules
branch_protection :
required_status_checks :
strict : true
contexts :
- "lint"
- "test"
- "security"
required_pull_request_reviews :
required_approving_review_count : 1
dismiss_stale_reviews : true
require_code_owner_reviews : true
restrictions :
users : []
teams : [ "maintainers" ]
CODEOWNERS for Automatic Assignment 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# .github/CODEOWNERS
# Default owners for everything
* @your-org/core-team
# Security-sensitive files need security review
/auth/ @your-org/security
/api/ @your-org/security
*.env* @your-org/security
# Frontend changes
/src/components/ @your-org/frontend
/src/styles/ @your-org/frontend
# Infrastructure
/terraform/ @your-org/platform
/k8s/ @your-org/platform
Dockerfile @your-org/platform
Layer 2: Basic Checks Fast, deterministic checks that run on every PR.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# .github/workflows/lint.yml
name : Lint
on :
pull_request :
jobs :
lint :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Setup Node
uses : actions/setup-node@v4
with :
node-version : '20'
cache : 'npm'
- name : Install
run : npm ci
- name : Lint
run : npm run lint
- name : Format Check
run : npm run format:check
- name : Type Check
run : npm run typecheck
Best practice: Run lint/format checks first. They’re fast and catch the obvious stuff.
Test Suite 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# .github/workflows/test.yml
name : Test
on :
pull_request :
jobs :
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Setup
uses : actions/setup-node@v4
with :
node-version : '20'
cache : 'npm'
- name : Install
run : npm ci
- name : Test
run : npm test -- --coverage
- name : Upload Coverage
uses : codecov/codecov-action@v4
with :
fail_ci_if_error : false # Don't block on coverage service issues
Best practice: Parallelize tests when possible. Use test splitting for large suites.
1
2
3
4
5
6
7
8
9
jobs :
test :
runs-on : ubuntu-latest
strategy :
matrix :
shard : [ 1 , 2 , 3 , 4 ]
steps :
- name : Test
run : npm test -- --shard=${{ matrix.shard }}/4
Layer 3: Security Scanning Security checks that don’t slow down development.
Secrets Detection (Pre-Commit) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# .github/workflows/security.yml
name : Security
on :
pull_request :
jobs :
secrets :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- name : Gitleaks
uses : gitleaks/gitleaks-action@v2
env :
GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
Dependency Scanning 1
2
3
4
5
6
7
8
9
10
dependencies :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Dependency Review
uses : actions/dependency-review-action@v4
with :
fail-on-severity : high
allow-licenses : MIT, Apache-2.0, BSD-3-Clause
SAST (Static Analysis) 1
2
3
4
5
6
7
8
9
10
11
sast :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Semgrep
uses : returntocorp/semgrep-action@v1
with :
config : >-
p/security-audit
p/secrets
Best practice: Set appropriate severity thresholds. Don’t block on low-severity issues.
Layer 4: AI-Powered Review Add AI analysis for nuanced feedback.
CodeRabbit Integration 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# .coderabbit.yaml
reviews :
request_changes_workflow : false
high_level_summary : true
ignore :
paths :
- "**/*.test.*"
- "**/fixtures/**"
- "docs/**"
auto_review :
enabled : true
drafts : false
chat :
auto_reply : true
Custom AI Review 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# .github/workflows/ai-review.yml
name : AI Review
on :
pull_request :
types : [ opened, synchronize]
jobs :
ai-review :
runs-on : ubuntu-latest
if : github.event.pull_request.draft == false
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- name : Get Changed Files
id : files
run : |
echo "files=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | head -20 | tr '\n' ' ')" >> $GITHUB_OUTPUT
- name : AI Analysis
if : steps.files.outputs.files != ''
uses : your-org/ai-review-action@v1
with :
files : ${{ steps.files.outputs.files }}
severity_threshold : medium
env :
OPENAI_API_KEY : ${{ secrets.OPENAI_API_KEY }}
GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
Layer 5: Merge Requirements Configure what’s required to merge.
Required Checks 1
2
3
4
5
6
7
8
9
10
11
# Reasonable requirements for most teams
required_checks :
- lint # Fast, always run
- test # Essential
- security # Non-negotiable
- build # Verify it compiles
optional_checks :
- ai-review # Helpful but not blocking
- coverage # Informational
- performance # Run on specific paths only
Conditional Requirements 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# .github/workflows/conditional.yml
name : Conditional Checks
on :
pull_request :
jobs :
detect-changes :
runs-on : ubuntu-latest
outputs :
backend : ${{ steps.filter.outputs.backend }}
frontend : ${{ steps.filter.outputs.frontend }}
infra : ${{ steps.filter.outputs.infra }}
steps :
- uses : actions/checkout@v4
- uses : dorny/paths-filter@v2
id : filter
with :
filters : |
backend:
- 'src/api/**'
- 'src/services/**'
frontend:
- 'src/components/**'
- 'src/pages/**'
infra:
- 'terraform/**'
- 'k8s/**'
backend-tests :
needs : detect-changes
if : needs.detect-changes.outputs.backend == 'true'
runs-on : ubuntu-latest
steps :
- name : Run backend tests
run : npm run test:backend
frontend-tests :
needs : detect-changes
if : needs.detect-changes.outputs.frontend == 'true'
runs-on : ubuntu-latest
steps :
- name : Run frontend tests
run : npm run test:frontend
infra-validation :
needs : detect-changes
if : needs.detect-changes.outputs.infra == 'true'
runs-on : ubuntu-latest
steps :
- name : Validate Terraform
run : terraform validate
Speed Optimization Caching 1
2
3
4
5
6
7
8
9
- name : Cache Dependencies
uses : actions/cache@v4
with :
path : |
~/.npm
node_modules
key : ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys : |
${{ runner.os }}-npm-
Parallel Jobs 1
2
3
4
5
6
7
8
9
10
11
12
13
14
jobs :
lint :
runs-on : ubuntu-latest
# ... lint steps
test :
runs-on : ubuntu-latest
# ... test steps
security :
runs-on : ubuntu-latest
# ... security steps
# These all run in parallel
Skip Unnecessary Work 1
2
3
4
5
6
7
8
9
10
11
12
- name : Check if build needed
id : check
run : |
if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -qE '\.(ts|tsx|js|jsx)$'; then
echo "needed=true" >> $GITHUB_OUTPUT
else
echo "needed=false" >> $GITHUB_OUTPUT
fi
- name : Build
if : steps.check.outputs.needed == 'true'
run : npm run build
Handling Edge Cases Draft PRs 1
2
3
4
5
6
7
8
9
10
11
12
on :
pull_request :
types : [ opened, synchronize, ready_for_review]
jobs :
full-check :
if : github.event.pull_request.draft == false
# ... full checks
quick-check :
if : github.event.pull_request.draft == true
# ... just lint and type check
Emergency Bypasses 1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Allow bypass for emergencies (with audit trail)
jobs :
check :
if : "!contains(github.event.pull_request.labels.*.name, 'emergency')"
# ... normal checks
audit-bypass :
if : contains(github.event.pull_request.labels.*.name, 'emergency')
runs-on : ubuntu-latest
steps :
- name : Log Bypass
run : |
echo "Emergency bypass by ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
# Send to audit log
Large PRs 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- name : Check PR Size
uses : actions/github-script@v7
with :
script : |
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
const additions = files.reduce((sum, f) => sum + f.additions, 0);
const deletions = files.reduce((sum, f) => sum + f.deletions, 0);
if (additions + deletions > 1000) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '> This PR has over 1000 lines changed. Consider splitting it for easier review.'
});
}
Notification Best Practices Slack Integration 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- name : Notify on Failure
if : failure()
uses : slackapi/slack-github-action@v1
with :
payload : |
{
"text": "PR Check Failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*<${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}>* failed checks"
}
}
]
}
env :
SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name : Post Summary
uses : actions/github-script@v7
with :
script : |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## Check Summary
- Lint: Passed
- Tests: 142 passed, 0 failed
- Coverage: 87%
- Security: No issues found`
});
FAQ How long should CI take? Target under 10 minutes for the full suite, under 5 minutes for basic checks. Anything over 15 minutes will frustrate developers and reduce usage.
Should AI review be a required check? Usually no. AI review is helpful but can have false positives. Make it informational, not blocking. Let humans decide whether to act on AI suggestions.
How do I handle flaky tests? Quarantine them. Move flaky tests to a separate job that doesn’t block merges. Fix them as a priority, but don’t let them slow everyone down.
What about monorepos? Use path-based triggering to only run relevant checks. Implement affected project detection. Consider tools like Nx or Turborepo for intelligent caching.
Conclusion Key Takeaways Start with branch protection and CODEOWNERS Run fast checks (lint, type) first Parallelize independent jobs Use path-based filtering to skip unnecessary work Cache dependencies aggressively Make security checks non-blocking but visible Add AI review as informational, not required Provide emergency bypass with audit trail Target under 10 minutes for full CI Notify on failures, not every run