From Broken Builds to Reliable Deployments with Vercel and GH Actions
7 min read

Turn a persistent build failure into a faster, more reliable deployment workflow.
By Nikolina Požega
How and Why I Moved My Vercel Build to GitHub Actions
A problem-solving sprint: why sometimes fixing an error isn’t about patching the log message, it’s about changing where the problem lives.
TL;DR
I couldn’t run rollup-critical-plugin on Vercel because of Node/Chromium/Puppeteer incompatibilities. I spent time trying to patch the environment on Vercel, but the right move was to move the build step to GitHub Actions, let it produce a prebuilt artifact, and have Vercel handle only the deploy. The result: a reliable pipeline that runs critical CSS generation (with Puppeteer) where I control the environment. This post is the investigation arc of that decision — the errors, the experiments, the logic that led me to the pivot, and the final outcome.
Why this post matters
This is not purely a how-to. It’s a story about why you sometimes stop trying to fix the symptom and instead change the context. If you’re a developer who’s seen similar “it worked on my machine” or “it fails in CI but not locally” errors, this will resonate. I’ll walk you through a practical example: the build fails on a remote platform because of deep dependency-environment mismatch, and the pragmatic, maintainable solution is to move the build where you control environment and caching.
The starting point — a broken build
It began innocently: a failed deploy on Vercel. The first error was straightforward and fixable — Vercel announced they were moving away from Node 18, so the project needed Node 22.x. That was an easy upgrade.
Then the next build failed with a different error: missing libraries required by Puppeteer/Chromium. I’d seen this before. My mind went through the checklist:
- libnss3 missing? Add apt packages?
- Puppeteer looking for a chromium executablePath that doesn’t exist in Vercel runtime?
- Use an alternate chromium package?
I tried the usual things: add libnss3 through build config, try alternative chromium packages, adjust executablePath. None of it fixed the failure. Eventually a new error appeared: executablePath points to a path that Vercel’s environment simply doesn’t support.
That’s when the curveball idea appeared: instead of fighting the platform restrictions, run the part that needs special environment somewhere else.
Diagnosis — read the message, then read where it points
A crucial point in the investigation was disciplined error reading. Look not only at what the error says but where it comes from — the file, the dependency chain, and the tool that triggers it.
I traced the error chain:
- The package that fails is rollup-critical-plugin.
- That plugin uses Rollup plugins and ultimately runs Penthouse for critical CSS.
- Penthouse relies on an older Puppeteer (v2.x-ish) / headless Chromium behavior that is brittle on Vercel’s newer Node environment.
- The failing symptoms were a combination of missing system libs (like libnss3) and an unrecognizable executablePath in Vercel.
This was the key insight: the problem isn’t a single missing package, it’s a brittle dependency stack that expects a runtime Vercel can’t guarantee.
Two possible approaches (and why I picked the second)
-
Fix the dependency chain in place
- Fork Penthouse or the upstream package, update Puppeteer and rebuild, or shim the runtime in Vercel.
- Pros: keeps everything on Vercel.
- Cons: high maintenance, fragile (future changes in Vercel or packages could break it again), time sink, too much work.
-
Change where the work is done (the pivot)
- Run the critical CSS generation in GitHub Actions where I control Node, Chromium, Puppeteer, and apt packages.
- Produce a prebuilt artifact (.vercel/output) and let Vercel do the final deploy.
- Pros: pragmatic, maintainable, immediate results. I control the environment and caching.
- Cons: changes the CI/CD architecture a bit — build is off Vercel now.
I chose option 2 because it minimized long-term maintenance and got me reliable results fast.
The implementation arc — what I actually did
This is the condensed, technical summary of how I implemented the pivot.
1. Offload build to GitHub Actions
-
Create a workflow that:
- Checks out repository
- Sets up Node 22
- Ensures Puppeteer & Chromium can install (or uses cached binary)
- Runs npm run build (which includes rollup + penthouse + puppeteer usage)
- Uses Vercel CLI to
vercel pull --environment=,vercel build --prod(or --environment=preview for preview), thenvercel deploy --prebuilt
2. Solve Puppeteer/Chromium reproducibility in Actions
- Cache Puppeteer’s Chromium binary at
~/.cache/puppeteerso we don’t download each run. - Install system deps (libnss3) only if the Puppeteer cache is missing.
- Important Bash gotcha: use -d for directories when checking existence: (
if \[ -d ~/.cache/puppeteer \]).
3. Keep Node install safe
- Cache ~/.npm to speed installs, but always run npm install in the workflow — the npm cache speeds installs, but does not populate node_modules by itself.
4. Vercel prebuilt deploy details
-
Ensure .vercel/output target matches the deploy target:
- Build production with vercel build --prod
- For preview use default preview build (or explicit --environment=preview)
-
Avoid cross-environment pollution from .vercel/cache — I eventually disabled caching for .vercel/cache or made keys environment-specific so preview and production caches never mix.
-
Clean .vercel/output before a production build to be safe:
rm -rf .vercel/output
5. Debug and iterate
- I hit lots of small issues: wrong if syntax in bash, checking -f on directories, incorrect vercel build flags, cache key collisions.
- Each was a small fix. The pattern was always the same: read logs carefully, identify the minimal diffs between expected and actual environment, and fix the smallest possible thing.
The mental model — how I approached this as a problem-solving sprint
I want to highlight the mindset and the steps that mattered most in this sprint. These are the practical habits that got me through the weeds:
- Read the error stack, not just the error line. The source file and dependency path tell you whether the problem is in your code or a third-party binary.
- Compare contexts. Things that work locally but fail on Vercel often mean the runtime differs (Node version, available libraries, binary paths).
- Try minimal, reversible changes. When you try a fix, make it small and confirm results quickly.
- Ask: can I move the work? If a platform is restrictive, sometimes the pragmatic solution is to do the thing somewhere else you control.
- Leverage caching, carefully. Cache speeds everything up but can also hide stale state. Use environment-encoded keys when caches can poison different targets.
- Accept the cost of maintenance tradeoffs. Forking a dependency could work but means ongoing maintenance. Offloading to CI means less future friction.
The result — what changed for me
- The critical CSS generation now runs reliably in GitHub Actions.
- Vercel receives a prebuilt artifact; it handles deployment only.
- Build reliability is high and the CI process is deterministic.
- Build times are much faster (once caches are warmed) and less error prone.
- I gained room to further optimize the workflow (parallel steps, better caching, selective builds).
Short checklist you can apply to similar problems
-
When a build fails in CI:
-
Read full logs; identify the sub-stack that triggered failure.
-
Check runtime differences (Node version, OS libs, binaries).
-
Decide whether to patch the platform or move the step to controlled CI.
-
If moving to CI:
- Ensure you can install required system packages or use cached binaries.
- Make your build produce a deployable artifact.
- Use explicit build flags (e.g., vercel build --prod) to avoid target mismatch.
- Design cache keys so they don’t leak environment metadata across branches.
-
Fixing a failing build doesn’t always mean fixing the failing code. Sometimes you change your deployment model so the code can run where the environment is compatible.
Final thoughts
This was a small example of a bigger discipline: be pragmatic. Fixing one error by patching a platform is sometimes possible, but often the lowest-cost, highest-value move is to shift where the work happens so you can control the runtime. You end up with a cleaner, faster, and more maintainable flow — and that is what sustainable development looks like.
