One of uika's build-tool integrations.
Mill 1.1.8 or newer. Declare one UikaModule object in build.mill. It holds
every setting and carries both commands, which find every non-test
JavaModule in the build themselves. Only JFR collection needs a mixin on the
test modules, because forkArgs is a task on the test module itself.
//| mvnDeps: ["net.exoego.uika::mill-uika::VERSION_PLACEHOLDER"]
package build
import mill.*, javalib.*
object uika extends net.exoego.uika.mill.UikaModule {
def failOn = "reachable"
def excludeFiles = Seq("uika-exclude.toml")
}An empty object uika extends net.exoego.uika.mill.UikaModule works too. Every
setting has a default. In a build.mill.yaml build the plugin goes under
mill-build:, and the settings are keys of the object. A complete Java project:
mill-version: 1.1.8
mill-build:
mvnDeps: ["net.exoego.uika::mill-uika::VERSION_PLACEHOLDER"]
extends: JavaModule
object uika:
extends: net.exoego.uika.mill.UikaModule
failOn: reachable
excludeFiles: ["uika-exclude.toml"]$ ./mill uika.dumpClasspath # writes out/uika/classpath.json
$ ./mill uika.dumpClasspath --output /tmp/after.json
$ ./mill uika.upgradeCheck --before /tmp/before.json --after /tmp/after.jsonThe commands take the object's name, so object linkage gives
./mill linkage.dumpClasspath. Relative paths, in settings and arguments alike,
resolve against the workspace root.
The dump command compiles as a side effect, so the PR-side dump needs no extra step.
upgradeCheck fetches the CLI as the jvm jar of
net.exoego.uika:uika-cli:<version> and runs it on the JVM that runs Mill. So it
works wherever that JVM does.
The linkage-check job dumps the PR's classpath and a baseline from the PR's
base branch, and fails on broken references between the two.
# .github/workflows/linkage-check.yml
name: linkage-check
on:
pull_request:
paths:
- '**.mill'
- '**.sc'
- .github/workflows/linkage-check.yml
# a PR update supersedes the running check
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
linkage-check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
# ... You may need to setup Java/Mill here ....
- name: Dump baseline classpath
# ahead of the PR dump, so the PR's compile replaces the base's classes
id: baseline
# a PR whose base cannot produce a baseline skips the check instead
# of failing it
continue-on-error: true
run: |
git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
git checkout ${{ github.event.pull_request.base.sha }}
if ./mill uika.dumpClasspath --output /tmp/before.json; then
status=0
else
status=1
fi
git checkout -
exit $status
- name: Dump PR classpath
# Mill compiles as a side effect of evaluating the dump command
run: ./mill uika.dumpClasspath --output /tmp/after.json
- name: Check broken references
if: steps.baseline.outcome == 'success'
run: >
./mill uika.upgradeCheck
--before /tmp/before.json --after /tmp/after.jsonIf the PR workflow already compiles the project and later jobs can reuse that
work, through a remote build cache or by running on the same runner, give this
job a needs: on those jobs, in the same workflow file. The PR dump then
takes its compiled classes from them instead of compiling again.
paths: keeps the check off PRs that touch no dependency declaration. A
required status check cannot use it, because a skipped workflow creates no
check run and the PR waits on it forever. For a required check, trigger on
every PR and skip the job from a job-level if: that reads the changed files.
A skipped job counts as passed.
The baseline dump is a second Mill invocation on the PR runner. Dumping it
once per push to the base branch instead, as an artifact named by SHA that the
PR job fetches by base.sha, moves that invocation off the PR's critical path
at the price of a job on every push, so the example does not do it.
Where that trade is worth it, save the coursier cache with actions/cache
under the same SHA and restore it before the check, because a fetched dump
names JARs by absolute path and the old versions are not on the PR runner.
Restore it for same-repo PRs only, since a fork PR's build code could read
private dependencies out of it. A compared-pair JAR uika cannot open exits 2
rather than degrading to a warning.
Settings are tasks on the UikaModule object, stated once and read by both
commands. The commands take no options beyond --output on dumpClasspath and
--before and --after on upgradeCheck.
def failOnis the gate threshold. The default is"any".def excludeFilesis aSeq[String]of exclude files. The default is none.def jdkReleaseoverrides the release derived fromjavacOptionsandscalacOptions(their mandatory halves included, since Mill compiles with both). The dump records a module that declares no release as running on the JDK that compiles it, which is itsjvmVersionwhen set and the JVM running Mill otherwise. A positive value is also what the dump records as the release every module runs on, for a build whose runtime is not what it compiles against. Set 0 to disable the check's API layer. The dump then keeps its derived values. The default, -1, derives both.def mergedClasspath = truechecks the union of every module's classpath once instead of each module against its own resolution. Per-module checking scans once per module, so a large build may want the union; the trade is that a break only one module's resolution shows can hide behind another module's version of the same jar.def cliVersionpicks the uika-cli version to run. The default is the plugin's own version.UIKA_JFRalso carries text evidence. Anything in that directory that is not a recording is passed on unchanged, so-Xlog:class+loadoutput and a classlist mix with the recordings. There is no separate setting.UIKA_CLI_PATHruns a CLI you already have instead of resolving one, so a build can run air-gapped or against a locally built CLI. It takes the jar, or an executable that runs it, such as a script that starts the jar on another JDK. It wins over the CLI version, nothing is downloaded, and a value that is not a file, or an executable that lost its bit, fails naming the variable.
To collect runtime load evidence, mix
UikaTestModule into the test modules, last so its forkArgs wins:
object test extends JavaTests, TestModule.Junit5, net.exoego.uika.mill.UikaTestModuleExport UIKA_JFR=<dir> for the test run, and keep it exported for the
upgradeCheck step, which reads the same variable back. One value serves both
phases. It is an environment variable rather than a setting because you switch
it on per run. As a setting it would make every forked test run record.
Collect with test. It is a Mill command and always forks, while a cached
testCached replays without forking a JVM and records nothing, the same
reason the Bazel recipe needs --nocache_test_results.
Your own override def forkArgs = Seq(...) replaces the list and drops the
injected flag. Append to super.forkArgs() instead. ./mill testLocal does
not fork, so it records nothing.
--draft-exclude-file maps to
def draftExcludeFile.
The base-branch-to-PR CI wiring is the same for every tool, with this page's two commands inside it.