The Step Nothing Local Runs

A finished content library, generated in a private sibling repo and mounted into this site at build time, had been sitting ready for days behind one open human gate: a repo secret that had to exist before the deploy workflow could even check out the private sibling repo it reads from. I created it, told the agent to push, and expected the rest to be mechanical; it had already passed locally.

It wasn’t mechanical. The push kicked off CI, and CI died.

Green all the way to the door

The deploy workflow runs four things in order: pnpm check, the test suite, pnpm build, then a Cloudflare Pages upload via cloudflare/wrangler-action. The first three passed, same as they’d passed locally before the push. The fourth one failed, and it failed in a place nothing before it had ever touched: wrangler-action doesn’t use a wrangler that’s already in the lockfile, it runs pnpm add wrangler fresh, in CI, as its own first step. That pulls in workerd, wrangler’s runtime dependency, which ships a build script. pnpm 11 treats an unapproved build script as fatal (not a warning, a hard ERR_PNPM_IGNORED_BUILDS), and workerd had never been named in this repo’s allowlist, because nothing local had ever installed it. check, tests, and build all being green had proven precisely nothing about whether the deploy step would work, because the deploy step exercises a dependency install that only exists inside that one CI job.

The fix was one line: add workerd: true to the allowBuilds block in pnpm-workspace.yaml, with a comment explaining why it’s safe to approve (its postinstall just unpacks a prebuilt runtime binary: no network, no arbitrary code beyond the package itself). Redeploy, green, the library mount verified live.

Not the first time

A month earlier, the same gate had already bitten this repo once, from a different direction. pnpm 11 moved build-script approval out of package.json and into pnpm-workspace.yaml, and made an unapproved script fail any pnpm exec or pnpm run, including the a11y test suite’s own call to pnpm exec astro build. That first time, the culprits were esbuild and sharp, both ordinary transitive deps of Astro, and the failure blocked the whole local test suite for anyone working in the repo, not just CI. The fix then was the same shape: name the two packages, write down why each is safe, approve them.

Two incidents, a month apart, same underlying mechanism, two completely different symptoms: one blocked local dev, the other blocked a CI-only deploy step neither pnpm check nor pnpm build would ever reach. That’s the part worth sitting with: this isn’t a bug that gets fixed once. allowBuilds is a named-innocent allowlist, and every new dependency with a build script is guilty until specifically approved, which means the next new dependency with a build script, wherever it enters the graph, will trip the same wall again, and it will trip it in whatever pipeline stage happens to install it first. There’s no version of “approve the ones we’ve seen” that generalizes to “approved forever.”

What I’d rather keep than fix

I could weaken the gate: approve build scripts by default, or add a wildcard. I don’t want to. The whole reason pnpm 11 made this fatal instead of silent is that an unreviewed build script is exactly the kind of thing a supply-chain compromise rides in on; making it loud is the feature, not the friction. The discipline that’s actually worked twice now isn’t “make the wall go away,” it’s “when the wall stops you, name precisely what you’re letting through and say why, in a comment that outlives the person who wrote it.” That’s slower than a blanket allow. It’s also the only version of this gate that’s still doing its job the day something shows up that shouldn’t be approved.

The more durable lesson is about what “green” actually promises. A pipeline that passes check, tests, and build locally has proven those three things and nothing past them. The deploy step here installs a dependency no local command ever installs, which means this specific failure class can only ever be discovered for the first time in CI, and it’ll tend to surface on the day something you’ve actually been waiting on is finally ready to ship, because that’s the day you push. I’m not trying to make that day boring by eliminating the possibility. I’m trying to make sure that when it happens, the fix is still “name the thing, say why it’s safe” (five minutes) instead of “loosen the rule that’s supposed to catch the one that matters.”


← all writing