I forgot I installed my own CI tool. It caught me shipping 4 bugs.

typescript dev.to

Four type errors didn't ship to main last week.

Not because I caught them. I'd already done the ritual — pnpm lint, npx tsc --noEmit -p apps/api/tsconfig.json, pnpm test. Three greens. I was one git push away from landing all four on main.

A pre-push hook I'd forgotten I'd installed stopped the push and said:

typecheck failed — 4 errors

I sat there for a second. I just checked. With my own eyes. Three times.

It was right. I was wrong. Here's exactly what it found, in the order it found them.

Bug 1: coverage enforcement had been silently off for weeks

vitest.config.ts(19,7): error TS2769: No overload matches this call.
  Object literal may only specify known properties,
  and 'lines' does not exist in type 'CoverageOptions'.
Enter fullscreen mode Exit fullscreen mode

Vitest v4 moved coverage thresholds from the top level into a nested thresholds: {} key. My config had lines/functions/branches/statements at the old level:

// before — silently ignored
coverage: {
  provider: 'v8',
  reporter: ['text', 'json', 'html'],
  exclude: [/* ... */],
  lines: 70,
  functions: 70,
  branches: 70,
  statements: 70
}

// after
coverage: {
  provider: 'v8',
  reporter: ['text', 'json', 'html'],
  exclude: [/* ... */],
  thresholds: {
    lines: 70,
    functions: 70,
    branches: 70,
    statements: 70
  }
}
Enter fullscreen mode Exit fullscreen mode

Vitest reads the old key, shrugs, ignores it, enforces nothing. My tests had been "passing coverage" for an indeterminate number of weeks with zero coverage actually checked.

No lint rule catches this. No test catches this. It requires tsc pointed at vitest.config.ts — which nothing in my dev loop ever did, because root-level config files don't live inside any workspace package's tsconfig.json.

Bug 2: a string that forgot to be a union

tests/services/tenant.test.ts(37,41): error TS2345:
  Type 'string' is not assignable to type
  '"us-east-1" | "us-west-2" | "eu-west-1" | "ap-southeast-1"'.
Enter fullscreen mode Exit fullscreen mode

Classic TypeScript widening. config: { region: 'us-east-1' } — TypeScript widens the literal 'us-east-1' to string, which isn't assignable to the Region union my function actually requires. Fix is one as const:

// before
const tenantData = {
  orgId: 'org-123',
  name: 'Acme Corp',
  domain: 'acme.com',
  config: { region: 'us-east-1' }          // widened to string
};

// after
const tenantData = {
  orgId: 'org-123',
  name: 'Acme Corp',
  domain: 'acme.com',
  config: { region: 'us-east-1' as const } // preserves the literal
};
Enter fullscreen mode Exit fullscreen mode

Same structural problem as Bug 1: tests/services/ is a root-level test directory that lives outside any project's tsconfig.json include. My pnpm lint fans out to per-project tsc runs. None of them ever looked at this file.

Bug 3: .avg on a number

tests/services/metrics.test.ts(167,32): error TS2339: Property 'avg' does not exist on type 'number'.
tests/services/metrics.test.ts(168,32): error TS2339: Property 'min' does not exist on type 'number'.
tests/services/metrics.test.ts(169,32): error TS2339: Property 'max' does not exist on type 'number'.
Enter fullscreen mode Exit fullscreen mode

This one embarrassed me most. I'd refactored aggregateMetrics to return number[] at some point and forgotten to update the test. The test was still asserting aggregated[0].cpu.avg, .min, .max — properties that don't exist on a primitive number.

In TypeScript: three explicit errors. In JavaScript: .avg on a number silently returns undefined, then .cpu on undefined throws at runtime, but only for the specific shape of the test data I happened to pass. On slightly different inputs it might silently return nonsense. On the inputs my test used, it happened to look fine.

Same structural problem as Bugs 1 and 2. Same dev-loop blind spot. Same fix: point tsc at the file.

Bug 4: the meta-bug that hid the other three

apps/api/src/app/types.ts(2,6): error TS2304: Cannot find name 'KVNamespace'.
apps/api/src/app/types.ts(3,6): error TS2304: Cannot find name 'R2Bucket'.
apps/api/src/app/types.ts(4,14): error TS2304: Cannot find name 'Queue'.
apps/api/src/app/types.ts(7,17): error TS2304: Cannot find name 'DurableObjectNamespace'.
apps/api/src/app/types.ts(8,6): error TS2552: Cannot find name 'D1Database'.
... 200+ more errors across 50+ files
Enter fullscreen mode Exit fullscreen mode

My root tsconfig.json was the template every monorepo template ships with. No include, no exclude, no types, no baseUrl, no paths:

{"compilerOptions":{"target":"ES2022","module":"ES2022","moduleResolution":"bundler","strict":true,"esModuleInterop":true,"skipLibCheck":true,"forceConsistentCasingInFileNames":true,"resolveJsonModule":true,"isolatedModules":true,"declaration":true,"declarationMap":true,"sourceMap":true}}
Enter fullscreen mode Exit fullscreen mode

Because it had no include, running tsc --noEmit from the repo root pulled in every .ts file in the project, tried to compile them under the root's type set, and exploded with 200+ errors from Cloudflare Worker code that depends on @cloudflare/workers-types — which the root tsconfig doesn't load.

Total noise. Unrunnable. My response, months ago, was to pin my dev-loop tsc command to -p apps/api/tsconfig.json — the one project that worked cleanly. That gave me fast, clean typechecks for apps/api, and a silent blind spot everywhere else.

Bug 4 didn't cause Bugs 1–3. Bug 4 was the condition in which Bugs 1–3 could live forever.

Fixing Bug 4 was the last step. After adding include, exclude, types: [], and workspace paths, bare tsc from the repo root now exits 0. It took four minutes. It could have taken four minutes any time in the last six months. I just never had a reason to do it, because I'd already built a workaround I didn't have to think about.

The line I want you to remember

It's not that my CI catches things my local environment doesn't. It's that my CI runs the commands I stopped running.

That's the whole thesis. Every one of those four bugs was catchable by tsc. The same tsc that ships with TypeScript. The same binary I already have installed. I just couldn't — or wouldn't — type the command that would have caught them, because I'd spent months engineering my dev loop around a broken root config I didn't want to fix.

A pre-push hook that runs your pipeline automatically eliminates that drift by removing the human who "just skips this one this time." That's it. That's the product.

What the tool did, exactly

Here's the part I want to be specific about, because the specifics are the value:

  1. pushci init walked my repo and detected two Node workspace projectsapps/api and apps/web. It read each project's tsconfig.json independently instead of trying to compile the whole tree under one root config. No KVNamespace noise, no avoidance pattern required.
  2. It read my pnpm-lock.yaml and chose pnpm run build over bare npx vite build, so the auto-detected commands actually run in a pnpm workspace. (That one was a recent fix. I was dogfooding the fix when it caught me. Recursion everywhere.)
  3. It generated a pushci.yml pipeline I didn't have to write, with install, build, test, lint, typecheck stages. I edited it later. I didn't need to.
  4. It installed a git pre-push hook I forgot about in about ten minutes, because the best developer tools are the ones you stop noticing. The next push I did — the one with the four bugs in it — the hook fired, ran my pipeline, caught them, blocked the push. None of the four reached main. The commit that did reach main had all four already fixed.

Scoreboard

Metric Value
Bugs about to ship 4
Bugs that shipped 0
Install time 90 seconds
Cost $0
SaaS accounts created 0
YAML files written by hand 0
GitHub webhooks installed 0
Pieces of local state I had to maintain 0

What this is, in one breath

One binary. npm install -g pushci. Auto-detects your stack across 33 languages. Generates pushci.yml from your repo structure. Installs a git pre-push hook that runs the pipeline locally before every push. No SaaS, no dashboard, no account, no credit card, no GitHub App, no .github/workflows/ to maintain. Runs inside Claude Code, Cursor, and Windsurf sandboxes where GitHub Actions can't reach — which is, for what it's worth, what I was originally dogfooding when it caught me.

I wrote it. I forgot I installed it. It caught me anyway.

Ninety seconds to install it and find your own seam:

npm install -g pushci
pushci init
Enter fullscreen mode Exit fullscreen mode

Other ways that work today:

brew install finsavvyai/tap/pushci                # macOS + Linux
curl -fsSL https://pushci.dev/install.sh | sh     # any POSIX shell
npx pushci init                                    # no install
Enter fullscreen mode Exit fullscreen mode

Tomorrow you'll either find zero bugs and forget about it, or you'll find one bug and keep it forever. Those are the only two outcomes.

— Shahar

pushci.dev

Source: dev.to

arrow_back Back to Tutorials