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, 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 different problem for a different day, tracked and deliberately backlogged. This week was just about getting the pipeline green again without six people’s worth of “works on my machine.”