A Week of CI Whack-a-Mole
Every fix in this post unblocked the next failure. That’s the honest shape of a CI week: you don’t find six bugs, folks, you find one bug wearing five disguises.

It started with GitHub Actions deprecating Node 20 on its runners, a warning on every job, while all four CI jobs still hardcoded node-version: 20 against a local/prod toolchain already on Node 24 via .nvmrc. Switched every setup-node step to node-version-file: .nvmrc so CI can’t drift from the one file that’s supposed to be the source of truth, again.
That unstuck the next one: husky‘s prepare script runs on every npm install, including Vercel’s production install, where there’s no .git directory and husky exits non-zero. Vercel deploys had been failing on that alone. Gated it behind a .git existence check.
Past that, tsc failed with about 250 errors, all downstream of one thing: nobody had run prisma generate before it. Vercel caches node_modules and skips regeneration by default. GitHub CI only worked because its build job ran prisma generate explicitly first. Added it as a postinstall and prepended it to build, so the client exists before tsc touches it in any environment. prisma generate only reads schema.prisma, no DATABASE_URL required, so it’s safe in a DB-less build too.
Then the deployed function crashed at import: winston’s File transport calls mkdirSync('logs') on construction, and Vercel’s filesystem is read-only. Gated the file transports on a serverless check (VERCEL/AWS_LAMBDA_FUNCTION_NAME) and wrapped construction in try/catch. Console transport is enough there, Vercel captures stdout anyway.
None of this makes the API actually happy running on Vercel, that’s a persistent Fastify server with node-cron and a live Redis connection, a mole I’m choosing not to whack this week, tracked and deliberately backlogged. This week was just about getting the pipeline green again without six people’s worth of “works on my machine,” and there’s always another mole, that’s the whole game.