Skip to content

build: move the toolchain to bun 1.4 - #478

Merged
oddharsh merged 7 commits into
mainfrom
bun/toolchain
Aug 20, 2026
Merged

oddharsh merged 7 commits into
mainfrom
bun/toolchain

Conversation

@oddharsh

Copy link
Copy Markdown
Owner

bun-v1.4.0 shipped today, which is the one thing #443 was parked on. This is that branch rebuilt as toolchain-only: 20 files, against 1229.

Everything separable landed on main first — seven restructure slices (#450-#457) and the TypeScript conversion (#459, #460) — so what remains is the package manager and the CI that runs it.

Byte-identical to what main builds today

1505 staged files, verified three ways, because the release notes gave three reasons to doubt it:

build result
node + pnpm (today's main) baseline
bun 1.4 + pnpm's tree identical
bun 1.4 + bun's own resolutions identical

1.4 is a Zig-to-Rust rewrite and switches node:zlib to zlib-ng. This repo's output is content-addressed and almost entirely compression-derived, so one moved byte would re-mint /a/ and /i/ URLs and orphan every committed dictionary. Nothing moved, and oxc-minify, lightningcss and minify-html resolve to the same pinned versions under both installers.

The canary machinery is gone

config/bun-canary.json pinned a SHA-256 because the canary tag rolled daily — it rolled three times on 2026-08-18 alone — and swapping a compiler underneath the artifact graph is exactly what this repo cannot tolerate. A released tag is immutable, so the version string is the guarantee the digest used to provide. .github/actions/setup-bun reads the version from packageManager and installs it: one declaration, no digest to bump, no daily red CI.

It still probes the capability first. Every bun before 1.4 accepts zstdCompressSync's dictionary option and silently ignores it — the frame decodes, nothing throws, and the only signal is a byte count that never shrank. The action runs that control before a build spends 40 seconds discovering it.

Two tests replaced rather than deleted

The workflow-bootstrap test now asserts every workflow that runs bun reaches the shared action and that none names a version inline. The allowBuilds test becomes trustedDependencies, whose entries must appear in bun.lock so a dead approval cannot pose as a reviewed one.

Its first draft demanded a bootstrap from promote-production.yml, which only echoes bun run deploy:promote as advice and runs no bun at all. Detection reads commands rather than strings now — the same sharpening every naive scanner in this repo has needed.

Measured, not carried over

lens-reader needs no --ignore-workspace. pnpm walked up to the workspace root and never created the local node_modules; bun installs that package.json on its own. The comment says so, so nobody adds a flag looking for parity with an era that ended.

routes:check stays on node deliberately: it boots workerd through wrangler's createTestHarness(), which hangs under bun.

Gates, under bun 1.4

325 tests, build, lint, typecheck, routes:check, pages:check, tools:check, perf-budget, photos:check, check-wrangler, the cal suite, lens-reader's 13, and a wrangler deploy --dry-run.

Before this can merge

The dashboard's two deploy commands still say pnpm exec wrangler …, and pnpm refuses to run on a tree whose packageManager says bun. #445 is the bridge that makes one command string work for both, and it should land first. bun/probe-14 is also out there testing whether the build image can install bun@1.4.0 while npm still shows 1.3.14 as latest — that log is worth reading before touching the dashboard.

bun-v1.4.0 shipped today, which is the one thing #443 was parked on. This is
that branch rebuilt as TOOLCHAIN ONLY: 20 files, against 1229. Everything
separable landed on main first as seven restructure slices and the TypeScript
conversion, so what is left is the package manager and the CI that runs it.

BYTE-IDENTICAL TO WHAT MAIN BUILDS TODAY, 1505 staged files, verified three ways
because the release notes gave three reasons to doubt it:

  node + pnpm      the baseline, main as it stands
  bun 1.4 + pnpm   the runtime alone, on pnpm's dependency tree
  bun 1.4 + bun    both, on bun's own resolutions

1.4 is a Zig-to-Rust rewrite and switches node:zlib to zlib-ng, and this repo's
output is content-addressed and almost entirely compression-derived, so a single
moved byte would re-mint /a/ and /i/ URLs and orphan every committed dictionary.
Nothing moved. oxc-minify, lightningcss and minify-html resolve to the same
pinned versions under both installers.

THE CANARY MACHINERY IS GONE, and that is what a released tag buys. config/
bun-canary.json pinned a SHA-256 because the `canary` tag rolled daily, which
would have swapped a compiler underneath the artifact graph. A released tag is
immutable, so .github/actions/setup-bun reads the version from packageManager
and installs it. One declaration, no digest to bump, no daily red CI.

It still probes the capability first. Every bun before 1.4 ACCEPTS
zstdCompressSync's `dictionary` option and silently ignores it: the frame
decodes, nothing throws, and the only signal is a byte count that never shrank.
The action runs that control before any build spends 40 seconds discovering it.

TWO TESTS REPLACED RATHER THAN DELETED. The workflow-bootstrap test now asserts
every workflow that RUNS bun reaches the shared action and that none names a
version inline, and the allowBuilds test becomes trustedDependencies, whose
entries must appear in bun.lock. Its first draft demanded a bootstrap from
promote-production.yml, which only ECHOES "bun run deploy:promote" as advice, so
detection reads commands rather than strings.

Also measured rather than carried over: lens-reader needs NO --ignore-workspace.
pnpm walked up to the workspace root and never created the local node_modules;
bun installs the local package.json on its own. The comment says so, so nobody
adds a flag looking for parity.

routes:check stays on node deliberately: it boots workerd through wrangler's
createTestHarness, which hangs under bun.

Gates under bun 1.4: 325 tests, build, lint, typecheck, routes:check,
pages:check, tools:check, perf-budget, photos:check, check-wrangler, the cal
suite, lens-reader's 13, and a wrangler dry-run.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
aadhar-sh 22aa0f1 Aug 20 2026, 03:09 PM

oddharsh and others added 3 commits August 20, 2026 10:31
`bun test` with no path DISCOVERS every *.test.* in the tree. The CI step ran
that, so it collected 367 tests across 10 files, including cal's vitest specs
and lens-reader's, under a runner neither expects: 20 failures and 4 errors that
say nothing about this change. It calls `bun run test` now, which names the one
file the step means.

And `node --test X` became `bun --test X`, which is not bun's spelling. The
runner is `bun test X`; the flag form silently does something else.

Both came from the same blunt substitution, and neither reproduced locally
because I ran the suite by PATH rather than through the script and the CI step.
Run the command CI runs, not the command that means the same thing.

Also restores test:node, deleted by the same sweep. Running the suite under the
other runtime occasionally is how gotcha 28 found a real defect: bun's
spec-strict Response rejects a body that undici allows, and the test pushing the
same case objects through twice was the bug.

325 tests under both runners, and the whole battery green under bun 1.4.
This job builds TWO commits in one run, and across a package-manager migration
those two want different installers. It ran `bun install --frozen-lockfile`
against the pnpm merge base, which wrote an untracked bun.lock and edited
package.json, and the switch back to HEAD then aborted:

  error: Your local changes to the following files would be overwritten by
  checkout: package.json

So it failed on git bookkeeping rather than on anything it measures. I called
this a frozen-lockfile error when I first read it; the frozen-lockfile problem
is real and upstream of this one, and the actual abort is the dirty tree.

Both steps now pick the installer from the lockfile in the CHECKED-OUT tree, and
the base tree is reset before switching back, so an install can no longer leak
across the boundary. The helper is written to a file the steps SOURCE rather
than exported through BASH_ENV: sourcing is visible where it is used and does
not depend on how Actions invokes bash.

It also watches pnpm-lock.yaml alongside bun.lock when deciding whether the
lockfile moved, since during the swap the answer lives in whichever file exists.

Both branches exercised locally against a real bun tree and a real pnpm tree.
The job still gates nothing; this only stops it reporting a failure that says
nothing about the diff.
@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Wire-size diff vs merge base

4f722a1 → abdfff8

Brotli q11, the quality the edge serves. Every number here is deterministic:
an unchanged file produces no row. Advisory — this check fails on nothing.

Client assets

No change. 22 files, 77.40 KiB Brotli total.

Pages

No change. 48 files, 503.63 KiB Brotli total.

Worker bundle

No change. 235.38 KiB gzip.

Dictionary deltas (pd/)

No change. 144 deltas, 448.83 KiB total.

perf-snapshot spawned "pnpm" as a quoted argument to run the dry-run. That
is gotcha 29's blind spot exactly: every sweep for `pnpm exec` as a phrase
missed it, so it survived the toolchain swap and then failed instantly on a
tree with no pnpm. The runner has no bare pnpm either (only corepack), so it
failed on BOTH sides of the comparison.

The catch turned that into an empty snapshot, and comparing two empty
snapshots renders "No change. 0 files" on every category. The check went
green while measuring nothing, for a full run.

It now runs wrangler's entry through process.execPath, naming no package
manager at all, which this job needs anyway because it builds two commits
that can disagree about which manager exists.

The guard is the other half. A snapshot of nothing is never legitimate here,
so record refuses to write one. It checks MODULES as well as files, because
a failed dry-run in a tree still holding a .build from an earlier run counts
69 files off those stale bytes and reports 0 modules; measured both ways,
and the file-count half alone would have missed it.

Controls: broken wrangler path exits 1 and writes no file; the real path
records 69 files, 85 modules.
@oddharsh
oddharsh merged commit 854d554 into main Aug 20, 2026
6 checks passed
@oddharsh
oddharsh deleted the bun/toolchain branch August 20, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant