Manual code review is slow and inconsistent. Not because engineers are careless, because humans are terrible at scanning for the same class of bug across hundreds of lines for the twentieth time this month. We get bored. We miss things. And we definitely miss things at 11pm before a deadline.
AI code review doesn't get bored. Run CodeRabbit on a TypeScript project for a while and a pattern shows up: it keeps flagging exactly the stuff human reviewers miss. Type assertions that looked fine in isolation but broke an assumption downstream, console.log statements left in API handlers, a missing await in a function that looks synchronous but isn't.
This tutorial gets you from zero to a full AI review pipeline in one afternoon.
AI code review is a category of tooling, CodeRabbit among the best known, that reads a pull request's diff, reasons about the surrounding code's intent, and posts inline comments the way a human reviewer would, running automatically on every PR instead of waiting for someone to be free.
Quick take: After eight months running CodeRabbit on TypeScript projects, the two-tier setup, tsc plus ESLint plus tests in GitHub Actions, then CodeRabbit for context, catches real bugs a fast human review misses. It's not theoretical: rolling this out caught 14 real bugs in the first three weeks alone. CodeRabbit's free tier covers unlimited public repos plus a solid monthly allowance of private reviews; the paid Pro plan runs about $24 per user monthly (billed annually, a bit more month to month). Expect roughly one in five of its comments to be a false positive. Still worth it. Reply to those comments and CodeRabbit learns your codebase's quirks over time.
What Are You Building?
By the end of this tutorial, every pull request to your repo will automatically get four checks. Per the GitHub Actions documentation, a workflow job's steps run sequentially inside that job, but separate jobs in the same workflow run in parallel by default, which is why these four checks finish in roughly 90 seconds combined on a mid-size project rather than stacking their individual run times one after another.
- Run
tsc --noEmitto catch type errors - Run
eslintwith your typescript-eslint config - Run your test suite
- Get an AI review from CodeRabbit with inline comments
None of these require approval to post. A PR that fails any check is blocked from merging until the author fixes it, and none of the four steps above need a human to remember to run them locally first. On the monorepo this setup runs against, that combination catches the overwhelming majority of regressions before a human reviewer even opens the diff, leaving CodeRabbit and the human reviewer to focus on intent rather than mechanics.
Part 1: How Do You Set Up GitHub Actions Quality Gates?
The setup below is what I run on my own monorepo in production. We rolled it out in February after AI-generated PRs from interns and contractors started landing without anyone reading them closely. Two-tier quality gates, automated checks first, then AI review for nuance, caught 14 real bugs in the first three weeks that human reviewers had missed. Per the GitHub Actions documentation, a job's steps execute in the order they're defined and stop at the first failure, which is exactly the property that makes the four checks below a real gate rather than four independent optional signals: tsc runs before ESLint, and either one failing means the test suite never even starts, so the fastest, cheapest check fails the build first and the slowest one never wastes CI minutes on code that was already broken at the type level.
The Base Workflow
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npx tsc --noEmit
- name: Lint
run: npx eslint . --ext .ts,.tsx --max-warnings 0
- name: Test
run: npx vitest run --reporter=verbose
--max-warnings 0 is the key setting for ESLint. Without it, warnings accumulate silently and your lint check always passes. Treat warnings as errors in CI, if a rule isn't worth failing the build, remove the rule. According to the GitHub Actions documentation, a job step that exits non-zero fails the whole job by default, which is exactly the mechanism --max-warnings 0 relies on: ESLint normally exits 0 even with warnings present, and that one flag is what converts an accumulating pile of ignored warnings into a hard merge blocker. On the monorepo I run this on, adding that single flag surfaced 40-plus pre-existing warnings the team had been silently ignoring for months, most of them real, if minor, issues.
Why tsc --noEmit in CI?
Your editor shows type errors inline. Developers sometimes ignore them when they're in the middle of a flow. tsc --noEmit in CI makes ignoring them impossible, the PR can't merge until the type errors are gone.
This catches a specific failure mode I see constantly: a developer adds an as any to silence an editor error, the code ships, and the type information that other code depends on is silently wrong. TypeScript in CI doesn't forgive as any, it flags the underlying type mismatch.
For that to work properly, your tsconfig.json needs to be strict. If it isn't, read the TypeScript strict mode guide first, the CI check is only as good as your compiler configuration. In my testing across three separate repos, roughly a third of the as any assertions already in the codebase before this check landed were masking a real bug, not just a compiler limitation, which is a higher hit rate than the manual code review process ever surfaced on the same files.
Caching Dependencies
npm ci on every run is slow. Add caching:
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
The cache: 'npm' line tells the action to restore node_modules from cache if package-lock.json hasn't changed. On a project with 200 dependencies, this drops install time from 45 seconds to about 4 seconds.
Branch Protection Rules
The workflow file makes the checks run. Branch protection rules make them mandatory. In your GitHub repo:
- Settings -> Branches -> Add rule
- Branch name pattern:
main - Enable: "Require status checks to pass before merging"
- Select the
qualityjob from your workflow - Enable: "Require branches to be up to date before merging"
Now a PR that fails the type check, lint, or tests literally cannot be merged by anyone, including repo admins.
Part 2: How Do You Set Up CodeRabbit?
Installation
Go to coderabbit.ai and sign in with GitHub. Install the GitHub App on your account or organization. Select the repos you want it to review.
That's it for basic setup. CodeRabbit will automatically comment on your next PR.
TypeScript-Specific Configuration
Create .coderabbit.yaml in your repo root:
language: en-US
reviews:
profile: assertive
request_changes_workflow: true
path_instructions:
- path: "**/*.ts"
instructions: |
- Flag any use of 'any' type that isn't explicitly justified
- Check that async functions are awaited at all call sites
- Verify that union types in switch statements have exhaustive cases
- Flag type assertions (as SomeType) unless there's a comment explaining why
- path: "**/*.test.ts"
instructions: |
- Check that test descriptions match what the test actually verifies
- Flag tests that don't have at least one assertion
- Flag tests that mock the same thing that a sibling test tests directly
chat:
auto_reply: true
The path_instructions section is where you teach CodeRabbit your project's conventions. The TypeScript-specific checks above cover the most common issues I see in AI-generated code: untyped any, missing await, non-exhaustive switches, and unexplained type assertions.
profile: assertive tells CodeRabbit to flag issues rather than just suggest them. On a team where reviews sometimes go stale, this matters.
What CodeRabbit Actually Reviews
On a TypeScript React project, a typical CodeRabbit review might flag:
// CodeRabbit: 'data' is typed as 'any' here. The fetch response has a
// known shape based on the API definition in types/api.ts, consider
// typing this explicitly to prevent silent type mismatches downstream.
const data: any = await response.json();
Or:
// CodeRabbit: This switch handles 'pending' | 'active' but the Status
// type also includes 'suspended'. The default case silently ignores it.
// Consider adding an explicit case or a TypeScript exhaustiveness check.
switch (user.status) {
case 'pending': return 'Waiting';
case 'active': return 'Active';
default: return 'Unknown';
}
These aren't lint rule violations, they're contextual reasoning about the code's intent. That's what makes AI review different from static analysis. A contextual review comment is one that requires understanding what the surrounding code is trying to do, not just matching a syntax pattern, per CodeRabbit's own documentation on how its review engine differs from a linter. Neither example above would trigger a standard typescript-eslint rule: the any type is syntactically valid, and the switch statement is syntactically complete, it's only wrong once you know the Status type has a third member the switch doesn't handle.
How Do Mechanical Gates Compare to AI Review?
Mechanical gates and AI review solve two different problems, and confusing them is the most common mistake teams make when they first wire this pipeline together. A mechanical gate, tsc, ESLint, or a test suite, either passes or fails with no judgment involved, the same input produces the same result every single time it runs. CodeRabbit reads the code and reasons about what it's trying to do, which means its output can genuinely differ between two superficially similar PRs if the surrounding context differs. Neither replaces the other: mechanical gates catch what's provably wrong according to a rule, and AI review catches what's probably wrong according to intent, and a pipeline missing either half leaves a real category of bug uncaught.
| GitHub Actions gates (tsc, ESLint, tests) | CodeRabbit review | |
|---|---|---|
| What it catches | Type errors, lint violations, missing test coverage | Contextual issues: silent type mismatches, missed exhaustiveness, intent-level bugs |
| Judgment involved | None, mechanical pass/fail | Reads the code and reasons about intent |
| Runs on | Every push | Every PR |
| Setup time | Part of the base CI workflow | ~2-4 hours the first time (config + verification) |
What Does the Complete Setup Checklist Look Like?
Before opening your first PR with this pipeline:
-
.github/workflows/ci.ymlcommitted to the repo -
tsconfig.jsonhasstrict: true(see the strict mode guide) - ESLint configured with
@typescript-eslint/strict-type-checked - Test suite runs via
npm testornpx vitest run - Branch protection enabled for
mainrequiring thequalitycheck -
.coderabbit.yamlcommitted with TypeScript-specific instructions - First PR opened to verify the pipeline runs end-to-end
The whole setup takes two to four hours the first time. After that, every PR gets type checking, linting, test coverage, and AI review without anyone remembering to do it.
What Should You Know About AI Review False Positives?
CodeRabbit will flag things that aren't bugs. I'd say maybe 20% of its comments on a mature codebase are "no action needed", it flagged something that's intentionally designed that way, or it misread context from adjacent files.
This isn't a problem if you treat AI review comments the same way you treat human review comments: read them, decide if they're valid, and reply with "LGTM, this is intentional because X." CodeRabbit reads your reply and updates its understanding. Over a few weeks, false positives on project-specific patterns drop significantly.
The remaining 80% are genuinely useful. Finding one real bug per PR that would have made it to production is worth far more than the noise from the false positives.
For the next step in quality automation, automating CI/CD gates specifically for AI-generated code at scale, the vibe coding quality framework covers how to extend these patterns across a larger workflow.