I recently spent an embarrassing amount of time on what turned out to be a very
simple problem. I code on Windows and deploy to Linux, so I should have
suspected line endings earlier. The culprit was a single invisible character:
the carriage return (\r).
The Symptoms: "Failed to upload build artifact"
I was working on a Deno 2 project using the Fresh framework for my personal portfolio. My CI/CD pipeline was set up in GitLab CI, configured to run tests, build the static assets, and then push the result to Deno Deploy using the CLI.
Everything worked fine on my local Windows machine. But the moment my GitLab pipeline hit the deployment stage, it choked and threw a vague error:
Failed to upload build artifactIn Deno Deploy, this error usually means the uploaded zip exceeds the 50MB limit.
Since node_modules grows quickly with npm dependencies, it can easily exceed
100MB. I assumed the Deno CLI in my pipeline was accidentally packaging it and
sending it to the edge network.
The Brute-Force Attempt
My first instinct was to just delete the folder before deploying, rather than dig into ignore rules:
deploy:
stage: deploy
script:
- deno task build
- rm -rf node_modules # Nuke it from orbit
- deno deploy --app="my-app" --prodThe CI logs reported a 7.5MB total directory size.
The deployment still failed with the same error. A 7.5MB directory shouldn't exceed any limit. I had no idea what was going on.
The Plot Twist: Edge Builders
That's when I realized I had misunderstood how edge platforms work. When I used the Deno Deploy CLI on my Fresh project, it didn't just upload my compiled files. It uploaded source code and triggered a remote build on Deno Deploy's edge servers.
- My CI runner successfully deleted
node_moduleslocally. - It uploaded the 7.5MB source code to the Deno Deploy Edge Builder.
- The Edge Builder (running Linux) executed
deno task build. - This re-downloaded all
npm:dependencies, re-creating a 100MB+node_modulesfolder on the remote server. - The Edge Builder then prepared the final package to distribute to the edge
network. To decide what to exclude, it read my
.denoignorefile.
My .denoignore file clearly had node_modules/ listed in it. So why was the
remote builder still packaging it?
The Invisible Culprit
I looked at the .denoignore file more carefully and noticed the problem: Git
line endings.
Because I created the .denoignore file in VSCode on Windows, and because Git
on my machine defaults to checking out files with CRLF (\r\n), the file was
pushed with carriage returns.
When the Linux-based Deno Deploy Edge Builder read my .denoignore file, it
split the lines by \n. It read my exclusion rule not as node_modules/, but
as node_modules/\r.
Because node_modules/\r did not exactly match the generated node_modules/
directory, the rule was completely bypassed. The remote builder happily packaged
the 107MB directory, hit its hard limit, and crashed, returning "Failed to
upload build artifact."
The Fix
The fix was simple. I just needed to convert the file to Linux line endings
(LF).
So I don't hit this again, I created a .gitattributes file at the root of my
project:
* text=auto
*.denoignore text eol=lf
*.gitignore text eol=lf
deno.json text eol=lfThis tells Git to always check out these config files with LF line endings,
regardless of the OS.
Conclusion
The whole debugging session took way longer than it should have, and the fix was
a one-line .gitattributes entry. When a cross-platform pipeline fails in a way
that makes no sense, check line endings early.