From 144f596ad166b2c4542d4876932fc7d4c4da7c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 28 Aug 2026 20:05:32 +0200 Subject: [PATCH 1/3] Add a binary-compatibility check (MiMa), adapted for Mill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the setup validated on halotukozak-com/commons#38 (which alpaca doesn't get automatically — no scala-cli, no `com.halotukozak::` sync group applies here). Report-only: manages a needs-major PR label on a real *backward* binary break, never fails the build. Differences from the scala-cli version: - New JAR comes from `./mill show jvm.jar` instead of `scala-cli package --library`. Mill 1.x's `show` wraps paths as `ref:v0::` / `qref:v1::` — stripped with sed. - Shared classpath comes from `./mill show jvm.compileClasspath` (alpaca's actual deps: regex, made, commons) plus scala3-library from the old-artifact resolution (scala-cli's coursier shim), since Mill's compileClasspath doesn't list the Scala standard library separately. - Baseline org is `io.github.halotukozak`, not `com.halotukozak`: alpaca's publish.yml actually publishes there (confirmed from Sonatype logs — "Successfully published io.github.halotukozak.alpaca_3-0.1.4"), even though the README says `com.halotukozak::alpaca`. `com.halotukozak:alpaca_3` doesn't exist on Maven Central at all. That mismatch is a separate issue worth fixing on its own. The check itself (.mima/bin-compat-check.scala, mima-core's API) is identical to the scala-cli version — no alpaca-specific logic needed there. Verified locally: v0.1.4 -> HEAD correctly reports 192 backward problems (the internal.parser package has been substantially reworked since). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_019awuXm5QZgXajLw3PvGo1s --- .github/workflows/mima.yml | 93 ++++++++++++++++++++++++++++++++++++ .mima/bin-compat-check.scala | 48 +++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 .github/workflows/mima.yml create mode 100644 .mima/bin-compat-check.scala diff --git a/.github/workflows/mima.yml b/.github/workflows/mima.yml new file mode 100644 index 00000000..d8273eba --- /dev/null +++ b/.github/workflows/mima.yml @@ -0,0 +1,93 @@ +name: Binary compatibility + +on: + push: + branches: [ main ] + pull_request: + +permissions: + contents: read + pull-requests: write + +# Report-only, mirrors the MiMa setup on halotukozak-com/commons (synced +# from halotukozak-com/.github to the scala-cli repos) adapted for Mill. +# Never fails the build — instead, on a PR, it manages a `needs-major` label +# when there's a real *backward* binary incompatibility, and removes it +# again once a later push is compatible. +jobs: + mima: + name: MiMa (binary) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Set up JDK + uses: actions/setup-java@v6 + with: + java-version: 21 + distribution: 'temurin' + - name: Setup coursier cache + uses: coursier/cache-action@v8.1 + - uses: VirtusLab/scala-cli-setup@v1 + - name: Check binary compatibility against the last release + id: mima + run: | + set -euo pipefail + + base_tag=$(git tag -l 'v*' --sort=-v:refname | head -1) + if [ -z "$base_tag" ]; then + echo "No release tag yet — nothing to compare against." >> "$GITHUB_STEP_SUMMARY" + echo "broken=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + base_version=${base_tag#v} + # alpaca publishes under io.github.halotukozak, unlike the other + # libraries (com.halotukozak) — see the discovery in the PR. + org=io.github.halotukozak + name=alpaca + echo "Baseline: $org:${name}_3:$base_version" + + new_jar=$(./mill --disable-ticker show jvm.jar | tail -1 | sed -E 's/^"[a-z]*ref:v[0-9]+:[0-9a-f]+://; s/"$//') + + printf '' > "$RUNNER_TEMP/empty.scala" + old_cp=$(scala-cli --power compile "$RUNNER_TEMP/empty.scala" --dependency "$org::$name:$base_version" --print-class-path | tail -1) + old_jar=$(printf '%s' "$old_cp" | tr ':' '\n' | grep "/${name}_3/$base_version/") + mill_cp=$(./mill --disable-ticker show jvm.compileClasspath | jq -r '.[]' | sed -E 's/^[a-z]*ref:v[0-9]+:[0-9a-f]+://') + shared_cp="$old_cp:$mill_cp" + + set +e + scala-cli run .mima/bin-compat-check.scala -- "$old_jar" "$new_jar" "$shared_cp" | tee "$RUNNER_TEMP/mima-out.txt" + status=$? + set -e + + if [ $status -ne 0 ]; then + { + echo "### MiMa found binary incompatibilities vs \`$base_version\`" + echo '```' + cat "$RUNNER_TEMP/mima-out.txt" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + echo "::warning title=Binary compatibility::MiMa found incompatibilities vs $base_version — see the job summary" + echo "broken=true" >> "$GITHUB_OUTPUT" + else + echo "Binary compatible with \`$base_version\`." >> "$GITHUB_STEP_SUMMARY" + echo "broken=false" >> "$GITHUB_OUTPUT" + fi + + - name: Label needs-major on binary incompatibility + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if [ "${{ steps.mima.outputs.broken }}" = "true" ]; then + gh label create needs-major --repo "${{ github.repository }}" \ + --color B60205 --description "Binary-incompatible change — needs a major version bump" \ + --force >/dev/null + gh pr edit "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" \ + --add-label needs-major + else + gh pr edit "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" \ + --remove-label needs-major || true + fi diff --git a/.mima/bin-compat-check.scala b/.mima/bin-compat-check.scala new file mode 100644 index 00000000..94b82f4b --- /dev/null +++ b/.mima/bin-compat-check.scala @@ -0,0 +1,48 @@ +//> using scala 2.13 +//> using dep com.typesafe::mima-core:1.1.6 + +// Binary-compatibility check between a released artifact and the current +// build, via MiMa's core API (there is no scala-cli MiMa plugin). +// +// Usage: scala-cli run .mima/bin-compat-check.scala -- +// oldJar the previously released library JAR +// newJar the freshly built library JAR (scala-cli package --library) +// sharedClasspath pathSeparator-joined dependency classpath (scala3-library, deps, …) +// +// Only *backward* compatibility (can code compiled against oldJar still link +// against newJar) gates the exit code — that's the actual SemVer contract a +// same-major release makes. *Forward* is printed for context (it's what's new +// since oldJar) but always has "problems" whenever you add API, so it's never +// a reason to fail on its own. +// +// Exit code: 0 if backward-compatible, 1 otherwise. + +import java.io.File +import com.typesafe.tools.mima.lib.MiMaLib +import com.typesafe.tools.mima.core.Problem + +object BinCompatCheck { + def main(args: Array[String]): Unit = { + val Array(oldJar, newJar, sharedCp) = args + val classpath = sharedCp.split(File.pathSeparator).iterator + .filter(_.nonEmpty).map(new File(_)).toList + + def problems(prev: String, curr: String): List[Problem] = + new MiMaLib(classpath).collectProblems(new File(prev), new File(curr), Nil) + + val backward = problems(oldJar, newJar) + val forward = problems(newJar, oldJar) + + def report(label: String, ps: List[Problem]): Unit = + if (ps.isEmpty) println(s"[mima] $label: OK") + else { + println(s"[mima] $label: ${ps.size} problem(s)") + ps.foreach(p => println(s" - ${p.description("current")}")) + } + + report("backward (code built against the release vs the new JAR)", backward) + report("forward (new API vs the release — expected to list additions)", forward) + + if (backward.nonEmpty) sys.exit(1) + } +} From 80df0d5cfa7b6ff8440365ad0eaa23d96fa4db59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 28 Aug 2026 20:06:05 +0200 Subject: [PATCH 2/3] Ignore .scala-build/ (scala-cli's cache dir, used by the mima check script) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_019awuXm5QZgXajLw3PvGo1s --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ffc375a0..25ed4278 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ benchmarks/outputs/ __pycache__/ .planning/ + +.scala-build/ From b1639c3c6b40193df5b7d5e84aa23f8547f2c244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 28 Aug 2026 20:21:57 +0200 Subject: [PATCH 3/3] =?UTF-8?q?Switch=20baseline=20org=20to=20com.halotuko?= =?UTF-8?q?zak=20=E2=80=94=20the=20namespace=20mismatch=20is=20fixed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit alpaca now publishes under com.halotukozak (confirmed live on Maven Central: com.halotukozak:alpaca_3:0.2.0). Drop the io.github.halotukozak workaround and its explanatory comment. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_019awuXm5QZgXajLw3PvGo1s --- .github/workflows/mima.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/mima.yml b/.github/workflows/mima.yml index d8273eba..9ba96cbd 100644 --- a/.github/workflows/mima.yml +++ b/.github/workflows/mima.yml @@ -42,9 +42,7 @@ jobs: exit 0 fi base_version=${base_tag#v} - # alpaca publishes under io.github.halotukozak, unlike the other - # libraries (com.halotukozak) — see the discovery in the PR. - org=io.github.halotukozak + org=com.halotukozak name=alpaca echo "Baseline: $org:${name}_3:$base_version"