From 5d56dab8a113b4b0ec6b47e84fbfe11c074e78cf Mon Sep 17 00:00:00 2001 From: halotukozak Date: Fri, 28 Aug 2026 18:41:43 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=84=20created=20local=20'.mima/bin?= =?UTF-8?q?-compat-check.scala'=20from=20remote=20'.mima/bin-compat-check.?= =?UTF-8?q?scala'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mima/bin-compat-check.scala | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .mima/bin-compat-check.scala diff --git a/.mima/bin-compat-check.scala b/.mima/bin-compat-check.scala new file mode 100644 index 0000000..94b82f4 --- /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 0e14c6f0d3f5a426b0ceabc3c9b4307e5e2d1c6d Mon Sep 17 00:00:00 2001 From: halotukozak Date: Fri, 28 Aug 2026 18:41:43 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=84=20created=20local=20'.github/w?= =?UTF-8?q?orkflows/mima.yml'=20from=20remote=20'shared/mima.yml'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/mima.yml | 90 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .github/workflows/mima.yml diff --git a/.github/workflows/mima.yml b/.github/workflows/mima.yml new file mode 100644 index 0000000..8a615ed --- /dev/null +++ b/.github/workflows/mima.yml @@ -0,0 +1,90 @@ +name: Binary compatibility + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + pull-requests: write + +# Report-only: pre-1.0, and the check itself is a prototype. It never fails +# the build — instead, on a PR, it manages a `needs-major` label (binary +# incompatibility means an eventual major bump) so the break is visible and +# trackable without blocking. The label is removed again once a later push +# to the same PR is compatible. +# +# There's no `needs-minor` (source/TASTy-compat-only breaks) yet: TASTy-MiMa +# can't read Scala 3.9's TASTy format yet (tasty-mima lags the compiler). +# Add it here once that's unblocked. +jobs: + mima: + name: MiMa (binary) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - 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} + org=$(grep -oP '(?<=using publish.organization ).*' project.scala | tr -d '"') + name=$(grep -oP '(?<=using publish.name ).*' project.scala | tr -d '"') + echo "Baseline: $org:${name}_3:$base_version" + + scala-cli --power package . --library -o "$RUNNER_TEMP/new.jar" --force + + 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/") + shared_cp=$(scala-cli --power compile . --print-class-path | tail -1) + + set +e + scala-cli run .mima/bin-compat-check.scala -- "$old_jar" "$RUNNER_TEMP/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