Fail CI when a model weight, bundle, or packaged dataset grows past a size you declared on purpose, before it reaches production.
In July 2026 a deploy on my own site quietly broke for twelve days. A 3.6 MB token library had slipped into the server bundle, pushing the Cloudflare Worker past its 3 MiB gzip limit on the free plan. Nothing crashed loudly: the build kept succeeding, the deploy command kept exiting zero, and production just kept serving the last version that fit under the limit. I only found out because I went looking for something unrelated. The full account is in the post-mortem.
The bug was never really about tokenizers or Cloudflare specifically. It was that nothing in CI knew a size limit existed, so nothing could tell me I had crossed it. This action is that missing check, generalized: declare a budget for any file or glob pattern, model weights, JS bundles, packaged datasets, serverless handlers, and CI fails with a clear table the moment something exceeds it, instead of two weeks later.
- uses: actions/checkout@v4
- uses: jmweb-org/model-budget-action@v1
with:
config: .github/model-budgets.json[
{ "pattern": "models/*.onnx", "max-size": "50MB", "gzip": false },
{ "pattern": ".open-next/**/handler.mjs", "max-size": "2.5MB", "gzip": true }
]| Input | Default | Meaning |
|---|---|---|
config |
.github/model-budgets.json |
Path to the budgets file (JSON or YAML), relative to the repository root. |
fail-on-missing |
false |
Fail the run if a declared pattern matches zero files, instead of skipping it. |
warn-at |
85 |
Percentage of a budget at which a file that still passes gets a ::warning:: annotation. |
| Output | Meaning |
|---|---|
report |
JSON: every checked file with its size, budget, margin, and status, plus a summary count. Use it in a later step to post a PR comment, push a metric, or whatever else you need. |
A list of entries. Each one needs pattern and max-size; gzip is
optional and defaults to false.
| Field | Meaning |
|---|---|
pattern |
Glob relative to the repository root. Supports * (within one path segment), ** (across segments, zero or more), and ? (one character). No brace expansion, no character classes. |
max-size |
A number of bytes, or a number followed by KB, MB, or GB. Sizes are binary: 1 KB = 1024 B, the same convention ls -lh and the platform limits this action targets both use. |
gzip |
If true, the file is gzip-compressed in memory before measuring, and the budget applies to that compressed size. Use this for anything a server or CDN ships gzipped, since that is the size that actually counts against a platform limit. |
JSON works out of the box. YAML is supported through a small hand-written parser that covers exactly the shape above, a flat list of scalar key/value maps, not a general YAML parser. Reach for JSON if you need anything more nested.
For every file a pattern matches, the action compares its size (raw or
gzipped, per the gzip flag) against max-size:
- over budget: the run fails, an
::error::is attached to the file, and it shows up in the job summary table. - at or above
warn-at% of the budget, still under it: the run passes, but a::warning::is attached so it shows up as an annotation on the pull request. - comfortably under: nothing extra happens.
Every run writes a markdown table to the job summary
(GITHUB_STEP_SUMMARY) with one row per checked file: name, size, budget,
margin, and status, so you don't have to open the raw logs to see what
happened.
A pattern that matches no files is reported but does not fail the run by
default; set fail-on-missing: true if a missing artifact is itself a
problem worth catching (for example, a build step that should always
produce a file this pattern expects).
The budget that would have caught the incident above:
[
{ "pattern": ".open-next/server-functions/default/handler.mjs", "max-size": "2.5MB", "gzip": true }
]2.5 MB gzip leaves headroom under Cloudflare's 3 MiB free-plan ceiling for the rest of what the Worker bundle ships. A CI run against the commit that broke the deploy would have failed on this line instead of shipping it.
$ node --testNo install step, the action is plain Node built-ins plus bash, nothing to
npm install before it runs, so it starts fast as a CI step.
MIT. See LICENSE.