Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/mima.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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}
org=com.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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ benchmarks/outputs/
__pycache__/

.planning/

.scala-build/
48 changes: 48 additions & 0 deletions .mima/bin-compat-check.scala
Original file line number Diff line number Diff line change
@@ -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> <newJar> <sharedClasspath>
// 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)
}
}