Gate on Tests, Not on Typechecks_
A CI setup that blocks merges on what actually catches regressions and keeps a few hundred known type errors visible without letting them stop the work.
There is a version of engineering discipline that is really just theater. A red build nobody can fix, so everyone learns to merge around it. I would rather have a green build that means something.
On Birixia, a business broker CRM I built as a Bun monorepo, the type checker reports a few hundred errors in one workspace. CI is green anyway, and that is on purpose.
What is actually true
The backend carries roughly 267 pre-existing tsc errors, almost all of it systemic friction between Pothos and Drizzle at the type level rather than anything that misbehaves at runtime.
The thing that makes this tolerable is a property of the runtime: Bun executes TypeScript without typechecking it. The types are a development tool, not a build step. The application runs, the tests pass, and the product works.
So there are two honest options. Stop the world and fix several hundred type errors in generated-ish GraphQL and ORM plumbing before shipping anything else. Or ship, and be precise about what the build guarantees.
I picked the second, and the important part is the precision.
Two jobs, different jobs
CI runs on every pull request and every push to main, split in two.
The blocking job installs with a frozen lockfile, runs the test suites in both backend workspaces, and builds the frontend. Four steps. All four are green, and all four gate merges into main. If that job is red, something is genuinely broken.
The advisory job runs tsc --noEmit across the backend workspaces with continue-on-error set. It can never fail the run. Its entire purpose is to keep the number visible.
That split is the whole idea. The blocking job answers “did we break the product.” The advisory job answers “how much type debt is there today.” Those are different questions and collapsing them into one red or green light destroys both answers.
Why not just delete the check
Because then the debt becomes invisible, and invisible debt only ever grows. The advisory job means anybody can look at a run and see whether the number is going up. It also means the day the count reaches zero, promoting it to blocking is a one line change rather than a project.
The rule I would state plainly: a check you cannot act on today should still run, but it must not be able to block a merge. The moment a red build is something people are expected to ignore, every red build becomes something people ignore.
The unglamorous details that make it work
Tests are mocked or point at deliberately unreachable hosts and set their own dummy connection strings, so CI needs no Postgres service container and no repository secrets. A test suite that requires real infrastructure is a test suite that will be flaky, slow, and eventually skipped.
Branch protection requires the build and test check and specifically does not require the advisory one, so the intent is enforced by configuration rather than by everyone remembering it.
The Bun version is pinned in both jobs and matches local. A CI environment that drifts from the development environment produces the worst class of failure: the one that only reproduces in the place you cannot debug.
The general point
Continuous integration is not a purity test. It is a contract about what a green check means.
Make that contract narrow enough to always be true and it becomes worth something. Make it aspirational and you have built an elaborate way to train your team to ignore warnings.
Ship on a green build that means the product works. Keep the debt on screen where it can be argued about. Those are not in tension.