Build log · week ending 5 October 2026
Two CI changes that made the main branch fail
By Tim
Each week I write up what we built on Noisy Brood: what broke, how we fixed it, and what I learned along the way.
The main branch went red after I changed how the checks run.
Until this week the gate ran its checks one after another: formatting, types, tests, then the build. A gate is the single command that decides whether a change may merge. I changed it to run checks at the same time wherever they don’t depend on each other. CI runs on a small machine with two processor cores, so the type check, the build and the tests now shared two cores.
One test failed. It reads every source file in the app and checks that each page and server function asks who is signed in. Alone, on my laptop, it takes about half a second. On the shared runner it took over five.
Vitest, the test runner the app uses, fails a test that runs longer than five seconds by default. The test was not stuck. It had more work than the time it was given, because it reads more code as the app grows, and now it was competing for the processor. A timeout that fires on a slow machine looks the same as a hang, so I measured before deciding which it was.
The fix is a longer limit for that one test, with a comment saying why. Look at the number at the end of the call:
// It parses every file under src/, so it grows with the app, and the gate
// runs it beside type-check and the build on a 2-core runner: half a
// second alone took over five there, Vitest's default, and failed main.
it("finds no page, layout, route method or server function without auth.protect()", () => {
expect(
sources.flatMap((s) =>
findUnprotected(s.path, s.text, { routed: s.routed }),
),
).toEqual([]);
}, 60_000);
I did not raise the limit for every test. A short default is how a real hang gets noticed, and one slow test is not a reason to lose that.
The other half
The same week I made the test suites run more files at once. Both runners, Vitest and Node’s built-in test runner, default to one fewer worker than the machine has cores. On a two-core runner that is one file at a time. Most of our tests spend their time waiting on a subprocess or on the database, not on the processor, so one at a time wastes the machine.
I put the number in one small module that both runners read:
// At least four test files at once, or the cores less one if that is more.
import { availableParallelism } from "node:os";
export const TEST_CONCURRENCY = Math.max(4, availableParallelism() - 1);
Vitest takes it as maxWorkers, and the root test script passes it to Node as --test-concurrency. One source means the two runners cannot drift apart.
Doing more at once and giving each piece enough time go together. I made the first change and found the second one only because main failed.
Why it matters
A faster check means a fix reaches the app sooner, and a check that fails for the right reason means a red build is worth reading. Both make it cheaper to ship the small repairs that keep Brood reliable.
Noisy Brood files away the mail that doesn’t need you, so the mail that does can find you. We write up what we build each week because every fix like this one is meant to make that a little more reliable. It isn’t open to everyone yet. If you’d like to hear when it can take you, join the list.