Skip to content

Make Bun workflows idiomatic for Mill and Scala.js - #13

Merged
arcaputo3 merged 35 commits into
mainfrom
agent/idiomatic-scalajs-mill-workflows
Aug 27, 2026
Merged

Make Bun workflows idiomatic for Mill and Scala.js#13
arcaputo3 merged 35 commits into
mainfrom
agent/idiomatic-scalajs-mill-workflows

Conversation

@arcaputo3

@arcaputo3 arcaputo3 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The complete 0.3.0 release: the original API surface plus a thirteen-PR review sweep
(#14#26, all merged into this branch) that fixed every correctness bug the review found and
made CI prove the shipped defaults.

What 0.3.0 is

  • Managed toolchain: checksum-verified Bun 1.4.0 on macOS, Linux (incl. musl, auto-detected),
    and Windows, x64 (incl. -baseline opt-in) and arm64 — 12 release assets, all modeled. A
    shared download cache (~/.cache/mill-bun, checksum-keyed) survives eviction, publishes
    atomically even across filesystems, and is shared by every module and CI run. System Bun stays
    one env var away; bunDoctor diagnoses resolution.
  • Reproducible installs: strict source-controlled bun.lock by default, frozen installs,
    actionable missing-lock and lockfile-skew errors (a lock written by a newer Bun says
    "regenerate with bunLock", not UnknownLockfileVersion). Local packages via unmanagedDeps
    are staged under vendor/ as file: deps, so locks stay checkout-independent.
  • One vocabulary across Scala.js and TypeScript: bunInstall, bundle/bundleFast,
    compileExecutable(s), testForked, bunToolEnv/bunRuntimeEnv, typed npm*Deps +
    npmOverrides, bunBundleFormat: Option[String] on both. Deprecated aliases cover every
    rename except the one documented type break; docs/MIGRATING-0.3.md has the full table.
  • Web modules, workspaces, publishing: paired Bun*WebModules, BunWorkspaceModule (one
    install + lock for mixed graphs), schema-v2 published manifests with vendored-runtime opt-in,
    workers and SQLite mixins.

What the sweep fixed (each with a test that fails on its parent commit)

  1. Web staging flattened the node_modules symlink into an empty directory — Scala.js web
    bundles could not resolve any npm dep (Preserve symlinks when staging, and link node_modules into web builds #15).
  2. TypeScript test modules resolved the outer module's lockfile through Scala outer-scope —
    frozen test installs always failed (Give TypeScript test modules their own lockfile identity #16); a second instance of the same defect class made the
    install-reuse path unreachable (Make bare test modules actually reuse the outer install #20).
  3. The bun"…" interpolator validated with a second, weaker parser — bun"react@" compiled and
    then failed at install time (Make the bun"..." interpolator use the one npm-spec parser #17).
  4. Vendored runtimes merged through a symlink into the shared workspace install, corrupting a
    sibling task's output (Stop vendored runtimes leaking into a workspace's shared node_modules #18).
  5. The download cache never recovered from eviction and could be permanently poisoned by a
    mid-copy crash on cross-filesystem publishes (Harden the shared Bun download cache against eviction and torn publishes #19).
  6. unmanagedDeps was positional-arg bun add — unconditionally rejected by frozen installs,
    and locks recorded absolute checkout paths (Deliver unmanagedDeps through package.json, not positional install args #21).
  7. dev() mutated the cached stage that production bundle builds from (Make bare test modules actually reuse the outer install #20).
  8. publishVersion cached PUBLISH_VERSION until a manual clean, letting stale local jars
    silently shadow fixture resolution (Make the examples runnable, wire them into CI, finish the scaffolding #26).

What CI now proves

Seven legs: build (compile + unit + full integration, system Bun) and managed-bun (download/
cache path) on ubuntu, macos, and windows, plus an examples leg that publishes the plugin
locally at the version the examples pin and runs both quick-starts end to end. The integration
suite runs the SHIPPED defaults — strict lockfiles against 14 committed fixture locks
(regenerable with one command), managed toolchain — instead of the relaxed env that previously
made every green misleading. Windows surfaced three real issues (native mill launcher, NTFS
permissions, POSIX-only fixture proxy); all fixed, all 19 legs green at merge time.

Validation at tip

  • unit tests and full integration green on ubuntu/macos/windows CI and locally
  • both example-* projects verified end to end (lock → run → testForked)
  • committed fixture locks reproduce byte-identically (RegenerateFixtureLocks re-run: zero diffs)

Before tagging 0.3.0 (deliberately deferred)

  • tree-wide scalafmt + format check (as the final commit, to avoid drowning review diffs)
  • the dependencyPairs specifier-equality design question (^19 vs ^19.0.0 is a hard
    conflict; combined with the latest default, an unversioned dep in a published library
    conflicts with every pinning consumer)
  • dev() end-to-end test and a cross-compile fixture

See CHANGELOG.md ([0.3.0]) and docs/MIGRATING-0.3.md.

arcaputo3 and others added 2 commits August 7, 2026 14:33
Bun 1.4.0 shipped six days before #13 pinned 1.3.14 in eleven places, and
the pin was hard to move: `bundledChecksum` matched the version with a
literal, and `bunArchiveSha256` was rejected unless `bunArchiveUrl` was set
too -- contradicting its own scaladoc.

- Replace the single-version checksum map and the `version == "1.3.14"`
  guard with a version -> asset -> sha256 table holding 1.3.14 and 1.4.0.
  All 24 entries verified against the official SHASUMS256.txt.
- Compose asset names from (os, arch, musl, baseline) instead of
  enumerating, and reject combinations Bun does not publish. This adds the
  musl (Alpine) and -baseline (pre-AVX2 x64) builds the plugin previously
  could not download at all: 12 assets per version, not 6. musl is detected
  from the dynamic loader; baseline is opt-in via `bunUseBaseline` because
  the JVM cannot see CPU feature flags.
- `bunArchiveSha256` now works alone, deriving the URL from `bunVersion`, so
  running an unbundled Bun is a two-line override. A custom
  `bunArchiveUrl` still requires a checksum so a mirror stays verified.
- Add a checksum-keyed shared download cache. `downloadedBunExecutable` is a
  task on the trait, so a build with N Bun modules previously downloaded the
  ~35MB archive N times into N task dests.
- `BunToolchainModule.DefaultBunVersion` is the single pin; `bunVersion` and
  `bunTypesVersion` both derive from it. `@types/bun` is published in
  lockstep with Bun, so the stale 1.3.11 pin can no longer drift.
- Point the integration suite at the managed toolchain by default. It ran
  with MILL_BUN_USE_SYSTEM=true, so it tested whatever Bun the developer had
  rather than the version the plugin pins -- and broke outright once the pin
  moved ahead of the local Bun. The shared cache makes this affordable: the
  whole suite now uses one download.

Unit 57 passing; integration 37 passing against managed Bun 1.4.0 with a
single cached archive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
arcaputo3 and others added 14 commits August 26, 2026 14:44
`BunScalaJSWebModule` could not resolve any npm dependency. Every tree copy
in the plugin walked with `os.walk` (followLinks=false) but branched on
`os.isDir`/`os.copy` (followLinks=true), so the `node_modules` symlink that
`ensureLinkedWorkspace` plants in the link report was misclassified:

- `BunWebSupport.copyContents` turned it into an empty directory, so
  `bun build index.html` had nothing to resolve imports against.
- `BunToolchainModule.copyWorkspace` deep-copied the entire resolved tree --
  O(node_modules) on every compileExecutable, bundle, and test workspace --
  and threw outright on any broken link, routine for the .bin shims of
  skipped optional dependencies.

Add `BunToolchainModule.copyTree`, which recreates links instead of
resolving them, and route copyWorkspace, copyPathRefs, and both
BunWebSupport copiers through it.

`BunScalaJSWebModule` now stages `node_modules` explicitly, mirroring
`BunTypeScriptWebModule.prepareWebStage`, rather than hoping the link
survives the copy.

Staging tasks that consume the linked tree now declare the install they
symlink into. Mill's filesystem checker rejects reading a dest the task does
not depend on, and the previous deep-copy only slipped past it because
`os.copy` checks the source path rather than the resolved target -- the
dependency was always real, just undeclared.

Split `htmlEntries` into a pure resolver and `materializeHtmlEntries`, so
`dev()` and `bundle` stop writing `index.html` into a staging task's
already-cached output. Collapse `BunTypeScriptWebModule`'s two byte-identical
staging tasks into one `webStage`.

The scalajs-web fixture had no npm dependencies, which is exactly why CI was
blind to this. It now imports lodash through @jsimport; on the parent commit
that fixture fails with a bun build error, and both web tests fail.

Unit 67 passing (was 57), integration 37 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`BunTypeScriptTests.npmInstall` builds a package.json that is a strict
superset of the outer module's, but `bunLockfile` is not a member of Mill's
`TypeScriptTests`, so the unqualified reference resolved through Scala's
outer scope to the *enclosing* module's bun.lock. Installing a superset
package.json against that lock under --frozen-lockfile fails with:

    error: lockfile had changes, but lockfile is frozen

and there was no `test.bunLock` to generate a matching one. Any TypeScript
test module declaring a dependency the outer module lacks was broken under
the shipped default.

- `bunTestPackageJson` is now one task shared by `npmInstall` and `bunLock`,
  so the install and the lockfile cannot describe different dependency sets.
- The test trait declares its own `bunLockfile` at `<test module>/bun.lock`
  and its own `bunLock` command. Defined directly rather than by mixing in
  BunToolchainModule, which would fork bunVersion/bunExecutable into separate
  task instances and re-download managed Bun per test module.
- A test module that adds nothing now delegates to the outer install instead
  of demanding a second lockfile, so bare test modules are unaffected.
- Every Bun member in the test trait is qualified `outer.` or `this.`. The
  outer-vs-inherited split is what caused this, and only `bunLockfile` was
  ever wrong -- `transitiveUnmanagedDeps` and friends are declared by Mill's
  TypeScriptTests and correctly bind to the test module.
- `requireBunLockfile` takes the expected path, so the error names the test
  module's lockfile rather than the outer one.

The typescript-test-deps fixture (outer is-even, test is-odd) now pins
bunRequireLockfile itself, following the typescript-lock precedent, so the
strict path is exercised regardless of the suite's env. On the parent commit
both new tests fail.

Unit 67 passing, integration 39 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`validateLiteral` hand-rolled a second, weaker parser that only inspected the
package-name half. So `bun"react@"` compiled cleanly and then threw from
`BunToolchainModule.parseDependency` during the install -- defeating the
entire purpose of a compile-time validator.

- `validateLiteral` now delegates to `parseDependency`, so the compile-time
  and task-time rules cannot diverge again. This also removes a dead `val
  name` binding whose two branches both evaluated to `dep` and whose result
  was discarded.
- Interpolated forms like `bun"react@$version"` are not knowable at compile
  time; the macro previously skipped validation entirely for them. They now
  route through `BunDep.validate`, which fails when the build evaluates the
  dependency list rather than inside `bun install`.

New `invalid-bun-specifier` fixture covers the literal case; on the parent
commit it compiles and `app.bunDeps` succeeds, returning Seq("react@").

Unit 69 passing, integration 41 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`BunScalaJSModule.bunInstall` merged vendored node_modules into
`dest/node_modules` unconditionally -- including after the workspace branch
had symlinked that path to the shared workspace install's Task.dest. The
merge therefore wrote into another task's output, shared by every member of
the workspace.

- `mergeFromClasspathEntry` refuses a symlinked destination root and names
  the task the directory belongs to. On its own this turns a silent
  cross-task write into a loud failure.
- The merge moved into the non-workspace branch, and workspace members with a
  vendored-runtime dependency now fail with an actionable message instead.
  Sharing one node_modules across members is the point of a workspace, so
  merging per-member was never going to be right; hoisting vendored trees to
  the workspace install is a feature, not a bug fix, and is left for later.

Only reachable with a published vendored-runtime dependency -- mergeFrom-
ClasspathEntry no-ops unless the entry carries META-INF/bun/node_modules --
which is why no fixture caught it.

Unit 70 passing, integration 41 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An evicted cache entry was never repopulated: downloadedBunExecutable
returned a Revalidate.Never PathRef to a file outside every Task.dest,
so deleting ~/.cache/mill-bun left every build trusting a path that no
longer exists until the user guessed the right `clean`. Cache refs now
carry quick (mtime+size) signatures with Revalidate.Always — recovery
costs one stat per evaluation.

The cross-filesystem publish fallback copied straight to the
checksum-keyed final path, so a concurrent reader could execute a
partially written binary and a build killed mid-copy poisoned the cache
permanently. The fallback now stages next to the final path and renames
— the publish is always a same-filesystem atomic move — and preserves
the executable bit the old os.copy.over silently dropped. Losing a
publish race is judged by the outcome (cached exists), not the
exception type, since Windows reports a sharing violation rather than
FileAlreadyExistsException.

Also: copyTree preserves relative symlink targets verbatim instead of
absolutizing them into the source tree; a relative MILL_BUN_CACHE_DIR
resolves against the workspace root instead of crashing; the CI build
job pins MILL_BUN_USE_SYSTEM=true so the system-Bun path keeps coverage
now that the suite defaults to managed; and pull_request CI is no
longer restricted to PRs targeting main, so stacked PRs get checks.

Every fix was confirmed to fail on the parent commit before trusting
its pass: the eviction integration test (forked evals with a private
MILL_BUN_CACHE_DIR) fails there with a successful eval pointing at a
missing file, and the relative-symlink test fails with an absolutized
target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BunTypeScriptTests extends upstream Mill's TypeScriptTests, so the
unqualified this.tsDeps() inside bunTestPackageJson resolved to the
Node toolchain defaults (ts-node, tsconfig-paths, @types/node) that the
outer trait deliberately replaced. Those names always survived the
outer-name filter, so a bare test module's package.json never equaled
the outer's and the reusesOuterInstall branch was unreachable — with
bunRequireLockfile on, every bare test module demanded its own
lockfile, contradicting the documented contract. The trait now pins
tsDeps to the outer module's.

The guard test passed anyway because both of its assertions also hold
in the broken standalone branch (the suite relaxes the lockfile
requirement, and installs never write into the source tree). It now
also asserts the returned install path is the outer module's
npmInstall.dest — confirmed failing before this fix and passing after.

Also: dev() on BunTypeScriptWebModule served straight out of the cached
webStage output while its sync thread mirrored live edits (but never
deletions) into it, and bundle builds from that same stage — a file
created and deleted during a dev session would ship in the next
production bundle. dev() now serves from a private copy in its own
command dest; copyTree preserves the node_modules symlink, so the copy
is cheap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pin Bun 1.4.0, compose the asset matrix, share the download cache
Give TypeScript test modules their own lockfile identity
Stop vendored runtimes leaking into a workspace's shared node_modules
Make bare test modules actually reuse the outer install
Preserve symlinks when staging, and link node_modules into web builds
Make the bun"..." interpolator use the one npm-spec parser
Harden the shared Bun download cache against eviction and torn publishes
Local paths were passed as positional arguments to bun install, which
turns the invocation into bun add — unconditionally rejected by
--frozen-lockfile. unmanagedDeps therefore never worked against a
lockfile, in any module type, and a lock produced by bunLock recorded
the absolute checkout path.

Each entry is now staged into vendor/ beside the generated package.json
and declared as file:./vendor/<name>, so the lock records the
checkout-independent file:vendor/<name>. Validated against real Bun
1.4.0: the lock survives relocating the repository and frozen installs
resolve the package. The staging excludes the local package's own
node_modules; bun resolves its declared dependencies through the
lockfile instead.

Applied uniformly: Scala.js and TypeScript installs and bunLock, the
TypeScript test trait (union of outer and test-side deps, preserving
the bare-module reuse equality), the publish module's vendored runtime
install, and Bun workspaces — where vendor trees are staged in the
layout beside each member's package.json. A name declared both as an
npm dependency and an unmanaged dependency is rejected, as are two
unmanaged directories claiming the same package name. Published
manifests are unaffected: they are built from the npm dependency lists,
so file: specifiers cannot leak to consumers.

This is the first coverage unmanagedDeps has ever had. The new
typescript-unmanaged fixture locks, frozen-installs, bundles, and runs
against a local package with bunRequireLockfile enabled; the
mixed-workspace fixture asserts the member manifest and staged vendor
tree and that no positional args reach the stubbed install. Both fail
on the parent commit — the TypeScript lock never contains
file:vendor/local-lib, and the workspace member manifest has no
shared-local entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arcaputo3 and others added 8 commits August 26, 2026 19:22
…v hooks

The TypeScript install task is canonically bunInstall; Mill's inherited
npmInstall delegates to it and stays usable. All internal call sites use
the canonical name, so install artifacts live in bunInstall.dest.

Both module kinds share one test entrypoint name: testForked. The
TypeScript test command `test` is a deprecated alias, and the Scala.js
bunTest — which duplicated the inherited testForked exactly, since the
overridden jsEnvConfig and testLinkTask already put every test run on
Bun — is deprecated in its favor.

TypeScript bunBundleFormat is Option[String], matching Scala.js. The
type disagreement was the one rename no alias could bridge; 0.3.0 takes
the one-time break, and None now lets `bun build` infer the format.
BunWorkersModule.workerBundleFormat follows it directly.

Environment hooks share one vocabulary: bunToolEnv (install, lock,
build subprocesses) is defined once on BunToolchainModule and used by
every runBun call site — Scala.js, TypeScript, workspace, publish, and
workers modules previously mixed bunEnv() and bunToolEnv() — and the
TypeScript bunRuntimeEnv is public instead of protected.

Integration tests exercise the canonical names throughout, with
explicit alias regression coverage: npmInstall must resolve to
bunInstall.dest, and the deprecated test / bunTest commands must keep
working until their removal at 1.0. MIGRATING-0.3.md documents the new
rows and the bunBundleFormat break with a before/after example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build.mill forced MILL_BUN_REQUIRE_LOCKFILE=false across the entire
integration suite, so the strict-lockfile default — the plugin's
headline 0.3.0 behavior — was exercised by almost nothing. Every
install-path bug fixed earlier in this stack lived in exactly that gap.

All 13 real-bun dependency-bearing fixtures now commit a bun.lock
generated by the pinned Bun 1.4.0 and install frozen in CI. The
RegenerateFixtureLocks entry point re-locks every fixture in one
command (MILL_BUN_REGENERATE_LOCKS=1 gates it; without the env it lists
what it would do and touches nothing). The forkEnv default is removed;
scalajs-dependency-manifests opts out in its own build.mill because its
stub cannot produce a real lock.

scalajs-transitive is restructured: lib and app both claimed the
workspace root as moduleDir, so both resolved the same bun.lock and
only one dependency set could be recorded there. lib now lives in lib/
with its own lockfile identity.

Committed locks make the Bun pin load-bearing — bun.lock is forward-
but not backward-compatible, and the raw UnknownLockfileVersion error
from an older Bun never mentions how to recover. A frozen install
against a lock the pinned Bun cannot read now fails before bun runs,
telling the user to regenerate with bunLock or raise bunVersion. The
supported-lockfile-version table lives beside the checksum table, and a
unit test forces every bundled Bun to declare its entry.

The new typescript-stale-lock fixture proves the guard fires before any
bun subprocess is reached, and fails with the guard neutralized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The plugin carries Windows-specific code — bun.exe extraction,
executableCandidates, the mill.bat fork path in the integration
harness — with zero coverage. Both jobs now run on windows-latest with
fail-fast off, using bash as the step shell so one ./mill invocation
works on all three platforms. The official Mill 1.1.5 mill.bat launcher
is committed; build.mill already selects it on Windows, but it never
existed, so the integration suite could not have forked a fixture build
there at all.

Both jobs also cache ~/.cache/mill-bun keyed on the toolchain source,
so a managed-Bun run downloads each pinned archive once per platform
rather than once per CI run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sh wrapper fetches Mill's native launcher, which has no Windows
build — every Windows step died instantly with "This native mill
launcher supports only Linux and macOS." A launcher-selection step now
points Windows at the committed mill.bat and everything else at ./mill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All five fixture suites copy-pasted the same tester/outputPath/
commandLogPath helpers and each re-encoded Mill's out/<module>/<task>
layout; BunIntegrationSuite now owns both, and the regenerator uses it
too.

bundle and bundleFast differed only in linker and --bytecode across 30
duplicated lines of args assembly; both now delegate to one Task.Anon
body. bundleFast never bytecode-compiles — it exists for iteration
speed — and its scaladoc says so.

First-ever coverage for three shipped features:
- BunSQLiteModule: typescript-compile mixes it in with a discovered
  data/app.db and asserts it lands in the compile workspace. The
  fixture chains bunCompileResources through super — an unqualified
  override silently drops the mixin's contribution, which is exactly
  the mistake a user would make, so the fixture models the right form.
- npmOverrides end-to-end: the new typescript-overrides fixture has lib
  and app pinning conflicting is-odd specifiers; the override resolves
  the install against a committed lock, and the generated package.json
  carries both the resolved specifier and the overrides block.
- bunDoctor: evaluated in the managed-toolchain suite.

Also fixes the two scaladoc examples that still used Mill 0.x's
millSourcePath (BunSQLiteModule, BunWorkersModule) — neither compiled.

Re-running RegenerateFixtureLocks against the committed locks produces
zero diffs, confirming lock generation is deterministic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t there

os.perms throws on NTFS, and bun.exe needs no executable bit — the
permission half of the cross-filesystem publish test now runs only on
POSIX. First genuine Windows finding; the managed-bun leg (download,
extraction, shared cache, eviction recovery, mill.bat forking) already
passes on windows-latest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
example-scalajs and example-typescript contained only a build.mill —
the TypeScript one still using the pre-break bunBundleFormat: String
form, so it did not even compile — while the README called them
runnable. Both are now the README quick-starts made real: sources,
tests, committed bun.lock, and their own mill/mill.bat wrappers. Both
verified end to end (lock, run, testForked) against a locally published
0.3.0. The examples/ showcase needed locks for its two
dependency-bearing modules — the strict default caught them with
exactly the intended error — and now compiles fully.

A new examples CI job publishes the plugin locally at whatever version
the examples pin (extracted from the build header, so a release-version
sweep cannot desynchronize the leg), runs both quick-starts, and
compiles examples/.

publishVersion is Task.Input: a plain Task captured PUBLISH_VERSION
once and served the stale value until a manual clean — the runbook
documented the workaround, and this session hit it for real when a
0.3.0 publishLocal left publishLocalTestRepo without the 0.0.0-NIGHTLY
that every fixture pins. The fixtures then silently resolved a
five-month-old jar from ~/.ivy2/local and 32 integration tests failed
mysteriously. The fixture repo fallback also drops ivy2Local, so a
stale local jar can never shadow the test repo again; the runbook note
about cleaning cached publish metadata is gone because the footgun is.

Scaffolding: CHANGELOG promoted to [0.3.0]; README's development
section shows the shipped-defaults suite and one-command lock
regeneration; RELEASING.md notes that mill-bun_mill1_3 is Mill's Scala
3 artifact mangling rather than a typo; the tracked-but-ignored
.mill-jvm-version gitignore entry and the never-referenced
millExecutable object are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arcaputo3 and others added 3 commits August 26, 2026 20:33
The fixture's bun-proxy is a POSIX sh script, which CreateProcess
cannot execute — the only failure in the entire Windows integration
suite. The fixture now carries bun-proxy.cmd with identical semantics
(log first-arg:marker, delegate to PATH bun, propagate the exit code)
and picks the right one per platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deliver unmanagedDeps through package.json, not positional install args
Finish the 0.3.0 renames: bunInstall, testForked, bunBundleFormat, env hooks
Run the integration suite on shipped defaults with committed lockfiles
Add Windows to the CI matrix and cache the managed Bun download
Share the integration harness and cover the last untested features
Make the examples runnable, wire them into CI, finish the scaffolding
@arcaputo3
arcaputo3 marked this pull request as ready for review August 27, 2026 14:39
bunUseSystem and bunRequireLockfile read Task.env inside plain cached
Tasks, so the documented workflows — toggling MILL_BUN_USE_SYSTEM, or
MIGRATING-0.3's mid-migration MILL_BUN_REQUIRE_LOCKFILE=false — were
silently ignored on a warm out/ until a manual clean. Both are
Task.Input now, with a forked-eval integration test that toggles the
requirement between runs on one warm out/ tree (confirmed failing with
plain Task).

mergeFromJar accepted `..` segments from jar entries: a dependency jar
carrying the vendored marker could write arbitrary files during
bunInstall without executing any code (zip-slip). Entries are
backslash-normalized and refused when they climb out of the bundle
root; a unit test builds a hostile jar and asserts refusal plus zero
leakage.

compileExecutable on Windows recorded a PathRef without the .exe bun
actually appends — CreateProcess masked it in CI while downstream
copies would fail and a missing path's constant signature broke
invalidation. Both module kinds record the .exe name on Windows, and
both integration tests now assert the recorded file exists.

The TypeScript test trait gains bunTestEnv (default: outer
bunRuntimeEnv), the test-side env lever mirroring bunTestJsEnv —
overriding forkEnv on a test object compiles and does nothing. All six
test-execution commands use it; the typescript-tests fixture asserts
the variable reaches bun test.

Generated member manifests now carry bunWorkspacePackageName (user
packageJson.name still wins on TypeScript): overriding the name — the
documented fix for colliding module names — previously satisfied Mill's
duplicate guard while bun still saw the colliding defaults. The
mixed-workspace fixture renames a member and asserts directory,
manifest name, and vendor staging all follow.

bunPublishedRuntimeInstall declared its .npmrc read as a Task.Source;
the raw workspace read tripped Mill's filesystem checker the moment the
file existed and never invalidated on change.

Docs and CI: the README Scala.js quick start compiles now (missing
scalalib import and utest dependency); the workspace snippet's imports
are complete; the examples-CI version grep survives prerelease pins;
RELEASING.md records why the release integration step deliberately
omits PUBLISH_VERSION; MIGRATING-0.3 documents that deterministic
conflict failure applies to every install, with npmOverrides as the
escape hatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final-review fixes: stale env toggles, zip-slip, Windows exe, test env
@arcaputo3
arcaputo3 merged commit a13c94a into main Aug 27, 2026
7 checks passed
@arcaputo3
arcaputo3 deleted the agent/idiomatic-scalajs-mill-workflows branch August 27, 2026 16:11
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