Make Bun workflows idiomatic for Mill and Scala.js - #13
Merged
Conversation
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>
`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>
…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>
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
and Windows, x64 (incl.
-baselineopt-in) and arm64 — 12 release assets, all modeled. Ashared download cache (
~/.cache/mill-bun, checksum-keyed) survives eviction, publishesatomically even across filesystems, and is shared by every module and CI run. System Bun stays
one env var away;
bunDoctordiagnoses resolution.bun.lockby 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 viaunmanagedDepsare staged under
vendor/asfile:deps, so locks stay checkout-independent.bunInstall,bundle/bundleFast,compileExecutable(s),testForked,bunToolEnv/bunRuntimeEnv, typednpm*Deps+npmOverrides,bunBundleFormat: Option[String]on both. Deprecated aliases cover everyrename except the one documented type break;
docs/MIGRATING-0.3.mdhas the full table.Bun*WebModules,BunWorkspaceModule(oneinstall + 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)
node_modulessymlink into an empty directory — Scala.js webbundles could not resolve any npm dep (Preserve symlinks when staging, and link node_modules into web builds #15).
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).
bun"…"interpolator validated with a second, weaker parser —bun"react@"compiled andthen failed at install time (Make the bun"..." interpolator use the one npm-spec parser #17).
sibling task's output (Stop vendored runtimes leaking into a workspace's shared node_modules #18).
mid-copy crash on cross-filesystem publishes (Harden the shared Bun download cache against eviction and torn publishes #19).
unmanagedDepswas positional-argbun add— unconditionally rejected by frozen installs,and locks recorded absolute checkout paths (Deliver unmanagedDeps through package.json, not positional install args #21).
dev()mutated the cached stage that productionbundlebuilds from (Make bare test modules actually reuse the outer install #20).publishVersioncachedPUBLISH_VERSIONuntil a manual clean, letting stale local jarssilently 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
example-*projects verified end to end (lock → run → testForked)RegenerateFixtureLocksre-run: zero diffs)Before tagging 0.3.0 (deliberately deferred)
dependencyPairsspecifier-equality design question (^19vs^19.0.0is a hardconflict; combined with the
latestdefault, an unversioned dep in a published libraryconflicts with every pinning consumer)
dev()end-to-end test and a cross-compile fixtureSee
CHANGELOG.md([0.3.0]) anddocs/MIGRATING-0.3.md.