Skip to content

CI/CD Quality Gates - AI-Generated TypeScript Tests

AI coding tools introduce any types and skipped edge cases faster than humans can review. These 4 GitHub Actions quality gates catch them automatically.

· · 9 min read

Updated: October 13, 2026

Team working at a shared bank of monitors

Quick Take

CodeRabbit's December 2025 study of 470 open source pull requests found about 1.7x more issues in AI-co-authored ones than in human-only ones. Four mechanical GitHub Actions gates catch most of it before merge: tsc --noEmit with noUncheckedIndexedAccess, ESLint at --max-warnings 0, an 80% Vitest coverage floor, and CodeRabbit for judgment calls. I haven't measured a catch rate for the pipeline as a whole, so treat it as a filter to try, not a guarantee.

CI/CD quality gates, sometimes written CICD gates, are automated checks wired into your continuous integration pipeline that fail a pull request the moment generated code violates a defined standard, before a human reviewer ever looks at the diff. CI/CD quality gates exist because AI-generated TypeScript code doesn't look obviously wrong. It follows patterns, uses the right library APIs, compiles. That's exactly the problem. A gate only helps if it actually inspects the files you think it does, which is what the lint coverage audit measures.

What it also does: introduce any types when inference gets hard, skip the error case in unit tests, generate as SomeType casts instead of proper validation, and assume array indexes are always safe. None of this fails a casual review. All of it causes production bugs. I can't hand you a war story with a client name on it, so here's something you can check yourself instead: the array-index case below fails to compile in six lines.

A CI pipeline that treats AI commits exactly like human commits, no exceptions, is the only reliable defense. Not code review, not pair programming, not "being careful." Mechanical gates that run on every push.

This is the GitHub Actions workflow I'd start from. Four gates, roughly 30 lines of YAML total.

TL;DR:

  • Gate 1: tsc --noEmit catches unsafe casts and implicit any
  • Gate 2: ESLint with --max-warnings 0 makes lint violations build-breaking
  • Gate 3: Vitest coverage threshold (80%) forces edge case tests
  • Gate 4: CodeRabbit on every PR catches design issues the mechanical gates miss

For the broader quality strategy these gates fit into, see Vibe Coding TypeScript.

Why Does AI Code Need Automated Gates?

CodeRabbit's December 2025 report analysed 470 open source pull requests, 320 AI-co-authored and 150 human-only, and found AI-co-authored ones averaged 10.83 issues each against 6.45, about 1.7x. Logic and correctness issues rose by roughly 75%. That's not a knock on the tools, it's a structural reality. AI models predict plausible tokens. They don't run your code mentally and verify edge cases.

Speed makes this worse, not better. A developer who writes 10 lines slowly is also thinking about error handling. A developer who reviews 200 AI-generated lines is pattern-matching, not auditing. The defects that slip through are exactly the ones that look fine on a quick scan: a missing null check, a function typed as any, a test that never calls the failure branch.

Automated gates don't get tired. They don't skim. They run the same checks on every push regardless of who wrote the code or how much coffee you've had.

Gate 1, What Does the TypeScript Strict Compile Check Catch?

The fastest gate is also the most effective. tsc --noEmit runs the TypeScript compiler over your entire codebase without emitting output files. No setup beyond a tsconfig.json. (TypeScript Compiler Options, 2024)

In GitHub Actions:

- name: TypeScript check
  run: npx tsc --noEmit

This catches unsafe type assertions, implicit any under strict, and the as SomeType casts that AI tools generate when they can't infer the correct type. For this gate to do real work, your tsconfig.json needs strict: true plus noUncheckedIndexedAccess:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true
  }
}

I won't put a percentage on how many bugs this gate stops, because I haven't measured one. Here's what I did measure. I compiled a six-line function, const first = items[0]; return first.name.toUpperCase();, with TypeScript 6.0.3. Under plain --strict it compiled clean. Adding --noUncheckedIndexedAccess produced error TS18048: 'first' is possibly 'undefined'. That flag is the one I'd argue does the most work against AI-generated code, because it forces items[0] to be treated as T | undefined instead of T, which catches the "assume the array isn't empty" pattern that generated code leans on.

See the strict TypeScript config for the full breakdown of what each strict flag catches.

Cardboard boxes moving along parallel conveyor belts in a warehouse
Photo by Getty Images on Unsplash

Gate 2, What Do TypeScript-Specific ESLint Rules Catch?

tsc checks types. ESLint catches patterns that compile fine but signal AI shortcuts. The critical flag here is --max-warnings 0: warnings become errors in CI. No exceptions. (GitHub Actions Documentation, 2024)

- name: ESLint
  run: npx eslint . --max-warnings 0

Three rules do the most work against AI-generated TypeScript. Add these to your .eslintrc:

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-non-null-assertion": "error",
    "@typescript-eslint/explicit-function-return-type": "error"
  }
}

Here's the opinionated take: @typescript-eslint/no-explicit-any should be "error", not "warn". AI tools use any the moment a stricter type requires more reasoning. A warning lets it accumulate across 50 files before someone notices. An error means the PR doesn't merge. That's the only version of this rule that works.

@typescript-eslint/no-explicit-any should be "error", not "warn" - a warning lets it accumulate across 50 files before someone notices.

Share this Post on X Bluesky

no-non-null-assertion blocks the ! operator. explicit-function-return-type forces every function to declare what it returns. AI tools skip return types constantly, this rule makes that a build failure.

Gate 3, Why Set a Test Coverage Threshold?

AI coding tools generate tests. That's not the same as generating good tests. What they generate is happy-path coverage: call the function with valid inputs, assert it returns the expected output. The error cases, null input, network failure, empty array, malformed data, don't exist. If you're specifically testing AI-generated React components, the React component testing strategies guide covers the patterns that catch the gaps coverage thresholds alone won't expose.

Vitest's coverage thresholds fix this indirectly. Set a minimum and the build fails when coverage drops below it:

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
      },
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.d.ts'],
    },
  },
});

GitHub Actions step:

- name: Test coverage
  run: npx vitest run --coverage

80% line coverage is a reasonable floor. It's not the number that matters, it's what happens when the number drops. The developer (or AI tool) has to write the missing tests. That's when the error paths get covered.

Is 80% the right threshold for every project? Probably not. But it's a defensible starting point, and you can raise it once the baseline is established.

How do you roll a new coverage threshold into an existing repo without breaking every open PR overnight? Follow this order.

  1. Run npx vitest run --coverage once against main to record the current baseline, don't guess at the starting number.
  2. Set the threshold a few points below that baseline so the gate passes today and only blocks regressions, not the entire history.
  3. Raise the threshold by 5 points every couple of weeks until you reach 80%, giving the team time to backfill tests gradually.
  4. Once at 80%, treat any further increase as a deliberate team decision, not a default you bump just because you can.
An automated factory with multiple levels of conveyor lines routing parcels
Photo by Hyundai Motor Group on Unsplash

Gate 4, What Does Automated AI Code Review Catch?

The first three gates are mechanical. They catch specific patterns with no judgment. This gate is different, it reads the code and flags design-level issues. For TypeScript projects on GitHub, CodeRabbit setup is the fastest path to get this running.

A minimal .coderabbit.yaml that focuses on TypeScript-specific patterns:

reviews:
  profile: "assertive"
  instructions: |
    Flag any use of `any` type or non-null assertion operators.
    Check that functions have explicit return types.
    Flag tests that don't include at least one assertion on an error path.
    Note missing input validation on function parameters that accept external data.

This gate runs on PRs only, not on every push. It's qualitative, not a hard pass/fail, but it surfaces the issues the compiler can't: a function that technically type-checks but has no input validation, a test that covers the success path but assumes the happy path is all that matters.

I haven't measured how often a review bot like CodeRabbit catches something the three mechanical gates miss, so I won't quote a rate. The class of problem is easy to describe, though: a missing null check on external data, or a test that only runs with valid inputs. That's where I'd expect the gate to earn its slot, and where I'd look first if you want to check it on your own PRs.

What Do the Four Gates Look Like at a Glance?

GateWhat it catchesRuns on
1. Strict compile checkType errors, implicit anyEvery push
2. TypeScript-specific ESLint rulesUnsafe patterns the compiler allowsEvery push
3. Test coverage thresholdUntested code pathsEvery push
4. Automated AI code reviewDesign-level issues: missing null checks, happy-path-only testsPRs only, qualitative not pass/fail

How Do You Put the Full Workflow Together?

All four gates as a single GitHub Actions workflow. Save this as .github/workflows/quality.yml:

name: Quality Gates

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - name: TypeScript check
        run: npx tsc --noEmit

  lint:
    runs-on: ubuntu-latest
    needs: typecheck
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - name: ESLint
        run: npx eslint . --max-warnings 0

  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - name: Test coverage
        run: npx vitest run --coverage

The jobs run in sequence: typecheck first, then lint, then test. Set up branch protection in GitHub Settings > Branches to require all three to pass before merge. CodeRabbit runs automatically on PRs once installed, no additional workflow configuration needed.

The whole thing is about 40 lines of YAML. It runs in under 2 minutes on a typical TypeScript project.

These gates don't slow down AI-assisted development, they make it sustainable. The alternative is accumulated technical debt: any types spreading through the codebase, untested error paths waiting to fail in production, and type assertions that mask real bugs. No AI tool reverses that easily once it's established. A 2-minute CI check on every push does.

Frequently Asked Questions

What is a CI/CD quality gate?
A quality gate is an automated check in your CI pipeline that fails the build when code doesn't meet a defined standard. For TypeScript projects, that means type errors, lint violations, or test coverage drops below a threshold. The GitHub Actions docs describe jobs and steps as the unit of CI automation, each gate runs as a separate job so you get precise failure messages.
Why do AI coding tools need stricter CI gates than human developers?
AI tools generate code fast, which means defects accumulate fast too. CodeRabbit's December 2025 report, which compared 320 AI-co-authored pull requests with 150 human-only ones, found about 1.7x more issues per AI PR. More importantly, AI tools reach for any, non-null assertions, and happy-path-only tests the moment a stricter approach requires more tokens to generate. Mechanical gates in CI catch these patterns every single time, human reviewers don't.
What coverage threshold should I set for AI-generated code?
80% line coverage is a reasonable floor when AI tools are writing tests alongside code. The real issue isn't the number, it's that AI-generated tests almost exclusively cover the happy path. A threshold forces the question: does this test suite actually try the error case? Vitest's coverage thresholds fail the build when the number drops, which surfaces untested paths immediately.
Does adding these gates slow down AI-assisted development?
No, they shift friction earlier. A TypeScript compile check and an ESLint run are the cheap end of a pipeline, and their cost scales with your codebase, so time them on your own. What they prevent is a production bug that costs half a day to trace. That's not overhead. That's the pipeline doing its job.