Skip to content

feat(mill): add Mill plugin#2042

Merged
hugo-vrijswijk merged 1 commit into
masterfrom
mill-plugin
Jun 17, 2026
Merged

feat(mill): add Mill plugin#2042
hugo-vrijswijk merged 1 commit into
masterfrom
mill-plugin

Conversation

@hugo-vrijswijk

@hugo-vrijswijk hugo-vrijswijk commented Jun 16, 2026

Copy link
Copy Markdown
Member

Closes #1860

Adds a plugin for Mill, similar to the sbt plugin. The plugin is published as a regular library, so can be built from sbt (hooray). Reuses the sbt-testrunner (renamed to stryker4s-testrunner) to start a process for running mutants. The process-testrunner related code has been moved to core.

Configuration is done through defs in the Stryker4sModule. Logging is done natively through a Task.log passthrough. Supports test discovery, code coverage, and compile error rollback

@hugo-vrijswijk hugo-vrijswijk force-pushed the mill-plugin branch 9 times, most recently from 2e1d873 to 0a3203e Compare June 17, 2026 14:01
@hugo-vrijswijk hugo-vrijswijk marked this pull request as ready for review June 17, 2026 14:08
@hugo-vrijswijk hugo-vrijswijk force-pushed the mill-plugin branch 6 times, most recently from a0c9ec0 to d15f564 Compare June 17, 2026 14:52
@hugo-vrijswijk hugo-vrijswijk enabled auto-merge (squash) June 17, 2026 15:30
@hugo-vrijswijk hugo-vrijswijk merged commit d18cbf7 into master Jun 17, 2026
13 checks passed
@hugo-vrijswijk hugo-vrijswijk deleted the mill-plugin branch June 17, 2026 15:35
@hugo-vrijswijk hugo-vrijswijk requested a review from Copilot June 17, 2026 15:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Mill build tool support by introducing a Mill plugin module and refactoring the existing sbt-specific process test-runner into build-tool-agnostic core/testRunner components.

Changes:

  • Introduce modules/mill plugin (Stryker4sModule) with Mill-native config/logging, test discovery, and compile/rollback flow.
  • Rename/generalize the sbt test-runner artifact to stryker4s-testrunner and move process test-runner wiring into modules/core.
  • Add Mill integration testing (scripted-style) and CI wiring, plus docs/README updates.

Reviewed changes

Copilot reviewed 43 out of 45 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
README.md Document Mill plugin usage and CLI/config patterns
project/Settings.scala Add Mill plugin settings; rename test-runner module
project/MillScripted.scala Add sbt task to run Mill integration fixtures
project/Dependencies.scala Add Mill dependencies + Scala version for Mill plugin
modules/testRunner/src/main/scala/stryker4s/testrunner/TestRunnerMain.scala Rename main entrypoint/package for generalized test-runner
modules/testRunner/src/main/scala/stryker4s/testrunner/TestRunner.scala Update package to build-tool-agnostic namespace
modules/testRunner/src/main/scala/stryker4s/testrunner/TestInterfaceMapper.scala Update package for new test-runner artifact
modules/testRunner/src/main/scala/stryker4s/testrunner/server/UnixSocketServer.scala Update imports/packages to new test-runner namespace
modules/testRunner/src/main/scala/stryker4s/testrunner/server/TcpSocketServer.scala Update imports/packages to new test-runner namespace
modules/testRunner/src/main/scala/stryker4s/testrunner/MessageHandler.scala Update package for new test-runner artifact
modules/testRunner/src/main/scala/stryker4s/testrunner/interface/fingerprint.scala Update package for new test-runner artifact
modules/testRunner/src/main/scala/stryker4s/testrunner/Context.scala Update package for new test-runner artifact
modules/testRunner/src/main/scala/stryker4s/package.scala Update test-runner mapper import to new package
modules/testkit/src/main/scala/stryker4s/testkit/FileUtil.scala Use Java 8-friendly resource-to-path conversion
modules/sbt/src/sbt-test/sbt-stryker4s/test-subproject/build.sbt Bump Scala 3 version in scripted fixture
modules/sbt/src/main/scala/stryker4s/sbt/Stryker4sSbtRunner.scala Switch sbt runner to core ProcessTestRunner + new context
modules/sbt/src/main/scala/stryker4s/sbt/Stryker4sPlugin.scala Construct and pass sbt runner context
modules/sbt/src/main/scala/stryker4s/sbt/runner/SbtTestRunner.scala Remove sbt-specific wrapper (replaced by core runner)
modules/sbt/src/main/scala/stryker4s/model/TestInterfaceMapper.scala Expose mapper as an object for reuse by sbt runner
modules/mill/src/test/scala/stryker4s/mill/StrykerModuleTest.scala Unit tests for Mill module defaults/behavior
modules/mill/src/test/scala/stryker4s/mill/runner/MillTestDiscoveryTest.scala Unit tests for Mill test discovery
modules/mill/src/test/scala/stryker4s/mill/MillConfigSourceTest.scala Unit tests for Mill config source mapping
modules/mill/src/test/resources/unit-test-project/foo/test/src/foo/CalcTest.scala Mill unit-test-project fixture
modules/mill/src/test/resources/unit-test-project/foo/src/foo/Calc.scala Mill unit-test-project fixture
modules/mill/src/test/resources/no-test-module-project/bar/src/bar/Bar.scala Fixture for “no test module” scenario
modules/mill/src/mill-test/mutate/mill.bat Windows Mill launcher fixture for integration test
modules/mill/src/mill-test/mutate/mill *nix Mill launcher fixture for integration test
modules/mill/src/mill-test/mutate/foo/test/src/foo/CalcTest.scala Integration fixture producing deterministic mutation score
modules/mill/src/mill-test/mutate/foo/src/foo/Calc.scala Integration fixture mutation target
modules/mill/src/mill-test/mutate/build.mill Integration fixture build using the Mill plugin
modules/mill/src/mill-test/mutate/.mill-version Pin Mill version for integration fixture
modules/mill/src/main/scala/stryker4s/mill/StrykerModule.scala Mill plugin trait exposing stryker command + config defs
modules/mill/src/main/scala/stryker4s/mill/runner/Stryker4sMillRunner.scala Mill runner: compile mutated sources + start process test-runners
modules/mill/src/main/scala/stryker4s/mill/runner/MillTestDiscovery.scala Discover tests via Mill testkit discovery and map to API model
modules/mill/src/main/scala/stryker4s/mill/MillConfigSource.scala Mill build config source implementation
modules/mill/src/main/scala/stryker4s/log/MillLogger.scala Logger adapter to Mill daemon logger
modules/mill/src/main/scala/mill/api/daemon/package.scala Helper to expose Mill logger color setting
modules/core/src/main/scala/stryker4s/run/testrunner/TestRunnerConnection.scala Move package to core run.testrunner namespace
modules/core/src/main/scala/stryker4s/run/testrunner/ProcessTestRunner.scala Generalize process test-runner to accept API test groups
modules/core/src/main/scala/stryker4s/model/TestRunnerId.scala Introduce shared TestRunnerId model in core
docs/getting-started.md Mention Mill as an alternative entrypoint
docs/configuration.md Add Mill build-tool config section and logging note
build.sbt Add Mill plugin module, new publish aliases, update aggregates
.scalafmt.conf Exclude Mill integration fixtures; set Scala 3 dialect override
.github/workflows/ci.yml Add Mill integration job; update sbt integration jobs

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread project/MillScripted.scala
Comment thread README.md
Comment on lines +56 to +57
//| mvnDeps:
//| - io.stryker-mutator:::mill-stryker4s:<version>
Comment thread README.md
Comment thread project/MillScripted.scala
Comment thread docs/configuration.md
MercurieVV added a commit to MercurieVV/ScalaSemantic that referenced this pull request Jul 4, 2026
* docs: catalog sbt build surface for Mill migration

* build: draft Mill build mirroring sbt feature set

* build: working Mill 1.1.7 draft — full graph compiles (__.compile)

* build: Mill draft passes full test suite (__.test 530/530)

Dogfood tests needed three fixes beyond compile:
- emit SemanticDB during compile via -Xsemanticdb + -sourceroot (Mill's
  semanticDbEnabled only feeds the IDE target; -sourceroot keeps files in
  out/ instead of polluting src/)
- run forked tests from repo root (forkWorkingDir) so fromProject(".") sees out/
- disable Mill's per-test sandbox (testSandboxWorkingDir=false) which otherwise
  overrides the working dir with a temp cwd

* fix: correct Mill SemanticDB enable guidance (semanticDbEnabled alone yields empty index)

Mill's semanticDbEnabled only feeds the on-demand semanticDbData target; a
plain 'mill __.compile' emits no *.semanticdb, so ScalaSemantic saw an empty
index for Mill users. Recommend the compiler flag (-Xsemanticdb -sourceroot
<build root>) so a normal compile emits it, matching the Gradle/scalac guidance.
Fixes both the auto-detect script snippet and the integration doc.

* build: fix Mill assembly merge rules; fat jar verified end-to-end

Append(scala/tools/asm) concatenated .class bytecode — wrong port of sbt's
MergeStrategy.first. Mill already keeps first on duplicate paths, so drop the
Append rules and only keep the discards (module-info + jar signatures).

Verified: mcp.assembly builds (99MB), stainless bundled, manifest main-class
correct, no duplicate jar paths, asm classes intact (cafebabe), and the jar
answers initialize/tools/list/find_symbol over stdio JSON-RPC.

* build: port ProGuard shrink + testShrunk to Mill

Replace the stubbed mcp.proguard (was aliased to the un-shrunk assembly) with a
real ProGuard run, and port testShrunk. proguard-base:7.9.1 is added to the
meta-build classpath via the //| mvnDeps header so proguard.* is referenced
directly. The -keep array is ported verbatim from build.sbt.

testShrunk strips mcp's runtime classpath (everything already bundled in the fat
jar) from the test classpath, adds the shrunk jar, and forks JUnitCore on the
PC/analysis/mcp suites from the repo root (dogfood cwd).

Verified on Mill 1.1.7 + Java 26: shrink 99.6MB->79MB (36386->24263 entries),
stdio JSON-RPC round-trips, testShrunk green (40 tests).

* build: use mill-contrib BuildInfo instead of hand-generated source

Replace the mcp module's hand-written generatedSources BuildInfo.scala with the
mill-contrib BuildInfo plugin (sbt-buildinfo parity):

- add com.lihaoyi::mill-contrib-buildinfo:$MILL_VERSION to the //| mvnDeps header
- mix BuildInfo into object mcp; buildInfoPackageName + buildInfoMembers(version)
- buildInfoStaticCompiled = true so version bakes as a compiled val (not a runtime
  resource lookup), robust from the ProGuard-shrunk fat jar
- override resources = super[BuildInfo].resources to break the MavenModule/BuildInfo
  diamond (adds the generated resource rather than replacing it)

Verified on Mill 1.1.7 + Java 26: generates object BuildInfo { val version: String =
"0.3.10" }; stdio initialize returns serverInfo.version 0.3.10.

* build: CI cache, script/doc updates for Mill dev workflow

Add explicit coursier+Mill actions/cache to ci.yml, verify docs.run/mdoc
end-to-end, switch scripts/agent-run.sh, token-live-metrics.sh, and
gen-release-notes.sh from stale sbt invocations to Mill (git hooks already
run Mill), and update CLAUDE.md/development.md/releasing.md/integration.md
accordingly ahead of full sbt cutover.

* docs: confirm scala-steward Mill compatibility, no config needed

Researched scala-steward-action's Mill support: it delegates dependency
extraction to a companion mill-plugin loaded into the target build rather
than parsing build.mill itself, so the build.sc->build.mill rename and
ivy/mvn interpolator syntax don't matter, and it respects our
.mill-version. Closes the last "doable" item in the migration TODO; only
upstream-blocked items (publish/scalafix/mutation) and final cutover
remain.

* docs: stryker4s Mill plugin merged upstream, awaiting v0.22.0 release

Rechecked mill-scalafix and stryker4s upstream status: mill-scalafix
still has no mill1.x build. stryker4s did merge Mill support to master
(stryker-mutator/stryker4s#2042, 2026-06-17) but it's only queued in the
still-open release-please PR for v0.22.0 - nothing published to Maven
Central yet (latest release stays v0.21.0). Close enough to actively
watch for the next release.

* build: unblock scalafix on Mill via scalafix-interfaces; investigate stryker4s

mill-scalafix has no Mill 1.x build and breaks build.mill entirely if
imported. Bypass it: call ch.epfl.scala:scalafix-interfaces (Scalafix's
own stable, Maven-Central-published embedding API for exactly this case)
directly from a new Common.scalafixCheck task. Verified end-to-end -
passes clean on all 4 modules, and a smoke-test import-order violation
produced the expected fix diff and failed the check. Wired into prePush
and a new CI step.

Also investigated stryker4s the same way: its Mill plugin merged upstream
but isn't released yet, so proved it out via a locally-published snapshot
(same commit the existing sbt integration already pins) - confirmed
working end-to-end in an isolated throwaway project. Left OUT of the main
build.mill though, since Mill's plugin-header mvnDeps resolve statically
on every invocation (unlike sbt's conditional addSbtPlugin gating) and
would break CI/other devs' builds the moment the local-only jar is
missing. Documented as the next step once isolated into run-stryker.sh's
existing throwaway worktree, or once stryker4s v0.22.0 ships.

* build: commit the ./mill launcher script

CI and git hooks already invoke ./mill directly with no download step,
but the script itself was never actually tracked in git - this branch
had never been pushed, so nothing caught it. Without this, every CI job
and a fresh clone's pre-push hook would fail with "no such file".

* fix: CI silently skipped 3 of 4 modules in scalafix/test steps

Confirmed a Mill 1.1.7 CLI bug: a space-separated list of explicit
per-module targets in one invocation (e.g. `core.compile pc.compile`,
or `core.test.testForked pc.test.testForked ...`) silently executes
only the FIRST target and reports overall SUCCESS regardless - verified
by checking out/<module>/ never gets created for the later targets, and
by injecting a deliberately failing test/scalafix violation and watching
it get swallowed. This was already the shape of the CI Test step before
this session's changes, and my own "Check scalafix" step added the same
way - caught only by reading the actual PR #210 CI log output instead of
trusting the green checkmark.

Fix: use a single `__`-prefixed wildcard selector instead (__.scalafixCheck,
__.test.testForked) - verified these resolve to exactly the 4 intended
modules and correctly run/fail on all of them, in combined invocations.
build.mill's prePush command was never affected (it calls each module
sequentially from Scala code, not via CLI multi-target parsing) and was
independently re-verified to still correctly fail on an injected test
failure.

* fix: split test-compile from test-run in CI to remove a real race

Confirmed on the live PR CI run: AnalyzerStructureSuite saw "core
layer=1" instead of the correct "0" - a genuine flake, not a code bug.
Root cause: dogfood suites call SemanticIndex.fromProject(".") and walk
the ENTIRE out/ tree for *.semanticdb from every module (main and test
sources). Mill runs independent modules' compile/test-compile/test tasks
concurrently, so one module's test could start scanning that tree while
another module's test-compile was still concurrently writing its own
*.semanticdb files, seeing a transiently incomplete graph.

Fix: a separate "Compile all test sources" step (./mill __.test.compile)
runs to completion before the "Test" step starts, removing the
concurrent write/read window. Test execution itself stays parallel.

* fix: sort SemanticIndex's file walk for deterministic module attribution

Files.walkFileTree's visit order mirrors raw OS directory-entry order and
differs between filesystems (ext4 on Linux CI vs APFS on macOS). Duplicate
global symbol IDs exist in this index (synthetic local<N> compiler counters
are reused per-file, not globally unique), and definitionUri/moduleOf tie-break
on walk order, so the whole structural graph was filesystem-order-dependent.
This is what caused AnalyzerStructureSuite's core layer=1 failure, deterministic
on CI but never reproducing locally - not the compile/test-execution race
previously suspected (that fix didn't change the outcome).

* build: final sbt cutover — delete build.sbt/project, disable publish + mutation CI

Deletes build.sbt, project/*.sbt, project/build.properties, and the two
meta-build helper sources (already ported into build.mill). Nothing in the
Mill build depended on them — ./mill prePush stays 663/663 green.

Sonatype publish and stryker4s mutation testing are both sbt-bound with no
working Mill 1.x path yet, so their CI jobs are gated `if: false` (kept
in place, not deleted) rather than ported:
- publish: no mill1.x build of mill-ci-release exists; deferred per request.
- mutation: a local snapshot of the unreleased Mill stryker4s plugin compiles
  and mutates but hits a reproducible InitialTestRunFailedException inside
  its own test-runner glue — traced as far as ruling out our wiring
  (test-filter, timeout config) as the cause. scripts/generate-stryker-overlay.py
  kept as the validated, not-yet-usable overlay design for when this ships.

docs/MILL_MIGRATION.md, CLAUDE.md, docs/project/development.md updated to
match the new state.

* test: dogfood SemanticIndex against Mill meta-build's own build.mill

Adds mill-build/build.mill override (-Xsemanticdb on MillBuildRootModule)
so the meta-build compile emits SemanticDB for a .mill file, and a test
proving the index actually loads it. Known limitation noted: emitted uri
is absolute (no matching -sourceroot), so relative-path lookup doesn't
resolve yet.

* build: lint the meta-build files, add out-of-Mill check for build.mill

- mill-build/build.mill: add -Wunused:all and wartremover to the
  meta-build override (mirrors Common trait's app-module settings).
- Add scripts/check-build-mill-fmt.sh + a CI step to scalafmt-check
  build.mill. Deliberately NOT a Mill task: a Task.Command that shells
  out to `./mill --meta-level N ...` deadlocks against its own daemon
  (Mill serializes all commands through one daemon per project) —
  confirmed by a 15+ minute hang. Must stay a standalone shell script.
- Fix contributor name in LICENSE/build.mill.
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.

Add support for Mill

2 participants