On every pull request, BloatBot tells you why your compiled binary changed size — attributing the byte delta to specific dependencies and to your own code — instead of just telling you that it changed.
Existing size tools (size-limit, bundlewatch, cargo-bloat in isolation)
report a number: "binary +47 KB." They stop there. You're left asking the only
question that matters — what did this to me? — and have to go dig.
BloatBot answers it directly:
Your release binary grew +47 KB (+3.1%). 41 KB of that is
serde_json, newly pulled in by your change tosrc/config.rs. The remaining 6 KB is your own code. No existing dependency shrank.
The diffing primitives already exist (cargo bloat, cargo tree, Cargo.lock).
BloatBot's value is the PR workflow, the dependency attribution, and a
comment a human actually reads.
1.48 MiB → 1.53 MiB, gzip 512 KiB → 528 KiB
.text attribution: +41.0 KiB from dependencies, +6.0 KiB from your code.
| Crate | Base | Head | Δ | Δ% |
|---|---|---|---|---|
| 🆕 serde_json | 0 B | 41.0 KiB | +41.0 KiB | new |
| 🔺 myapp (you) | 54.0 KiB | 60.0 KiB | +6.0 KiB | +11.1% |
New dependencies introduced by this PR:
serde_json(+41.0 KiB) — pulled in viamyapp → config-loader → serde_json(added inCargo.lock)
Base a1b2c3d · head e4f5g6h · toolchain 1.97.1.
# .github/workflows/size.yml
name: Binary size
on: pull_request
permissions:
contents: read
pull-requests: write # to post the sticky comment
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # BloatBot builds the base ref, so it needs history
- uses: Asuddle/BloatBot@v1
with:
bin: myapp # which binary to measure (required)
# threshold-percent: "5" # optional: fail the check if it grows >5%That's it. On each PR, BloatBot builds the base and head refs, diffs them per-crate, and posts a single sticky comment that it updates in place on every push.
| Input | Default | Description |
|---|---|---|
bin |
(required) | Binary target to analyze. Workspaces have several, so this is required. |
manifest-path |
Cargo.toml |
Path to the Cargo.toml to analyze. |
cargo-args |
"" |
Extra flags forwarded to both builds, e.g. --features foo. |
noise-floor |
512 |
Ignore per-crate changes below this many bytes (compiler jitter). |
threshold-percent |
"" |
Fail the check if the binary grows by more than this percent. Empty = never fail. |
comment |
true |
Post/patch the sticky PR comment. |
github-token |
${{ github.token }} |
Token used to comment. |
BloatBot's core is a plain CLI — no CI required. If it isn't useful in your terminal, it won't be useful as a bot, so prove it there first:
cargo install cargo-bloat # the underlying analyzer
cargo install --path . # build bloatbot
# Two git refs in, a ranked crate-level diff out:
bloatbot diff main HEAD --bin myappOther subcommands:
# Produce one side's per-crate breakdown as JSON (the intermediate data model):
bloatbot analyze --bin myapp --ref HEAD -o head.json
# Diff two breakdown JSONs — pure, no build, no git (great for tests/CI seams):
bloatbot compare base.json head.json
bloatbot compare base.json head.json --json # machine-readable- Analyze each ref: build the target in release mode, run
cargo bloat --release --crates --message-format json, and record a per-crate.textsize map plus file size, gzip size, and toolchain into aBreakdown. - Compare (pure):
delta = head[crate] − base[crate]for every crate, ranked by absolute delta. New crates (in head, not base) are the interesting case. Workspace-local crates are split out as "your code." A noise floor folds sub-threshold jitter away so we don't cry wolf over 12 bytes. (src/compare.rs— no cargo, git, or GitHub; unit-tested from JSON.) - Attribute the cause — the differentiator. For each newly-appeared crate,
diff
Cargo.lock(newly-locked → "added by this PR") and runcargo tree --invert <crate>to reconstruct the chain that dragged it in:myapp → config-loader → serde_json. (src/attribute.rs) - Render a Markdown comment: headline first, one focused table, the "why"
callout, everything else collapsed behind a
<details>. (src/render.rs) - Post a sticky comment carrying a hidden
<!-- bloatbot -->marker, PATCHing the existing one on re-runs instead of spamming. (scripts/post-comment.sh)
BloatBot rebuilds the base ref in the same job and measures both sides with
the same toolchain, so the diff is real signal rather than compiler noise. That's
two builds per PR; Swatinem/rust-cache
means only changed code recompiles. (A stored-baseline optimization — one build
per PR, diffing against a map recorded on merge — is a documented future option;
correctness first.)
MVP: Rust binaries, per-crate granularity. That's a deliberate, narrow target
where cargo bloat --crates and cargo tree hand you clean data almost for free.
Explicitly not now (written down so scope doesn't creep): JS/wasm/Go, function-level breakdown, multi-artifact repos, a web dashboard, historical trends.
This is a strong OSS tool and a genuinely useful portfolio piece — not a business. The audience ("Rust teams that care about binary size") is real but small. Built because the attribution problem is satisfying to solve.
- Fork PRs get a read-only token and can't comment. BloatBot always writes the report to the job summary too, so a fork PR still shows its size impact.
- Toolchain drift would show up as fake deltas; the toolchain is recorded and a mismatch is flagged in the comment.
- Which binary —
cargo bloatmeasures exactly one artifact, sobinis required.
cargo test --all # 10 unit + 4 CLI integration tests, no toolchain magic needed
cargo clippy --all-targets -- -D warningsBloatBot runs on itself in CI — see .github/workflows/size.yml.
Licensed under either of MIT or Apache-2.0 at your option.