From c149250f6e3b0625421a6f047771432597bc3dd3 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 9 Jan 2026 15:38:15 -0800 Subject: [PATCH] Add API compatibility checking with japicmp This adds a GitHub Actions workflow that runs on PRs to detect breaking changes in the public API. The check compares the current build against the baseline version on Maven Central. ENG-3367 Co-Authored-By: Claude Opus 4.5 --- .github/workflows/api-compat.yml | 22 ++++++++++++ dev-bin/release.sh | 6 +++- device-sdk/build.gradle.kts | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/api-compat.yml diff --git a/.github/workflows/api-compat.yml b/.github/workflows/api-compat.yml new file mode 100644 index 0000000..a068a99 --- /dev/null +++ b/.github/workflows/api-compat.yml @@ -0,0 +1,22 @@ +name: API Compatibility Check +on: + pull_request: +permissions: + contents: read +jobs: + api-compat: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 + with: + distribution: temurin + java-version: '17' + + - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + + - name: Check API Compatibility + run: ./gradlew :device-sdk:japicmp diff --git a/dev-bin/release.sh b/dev-bin/release.sh index 62902cd..8411c01 100755 --- a/dev-bin/release.sh +++ b/dev-bin/release.sh @@ -126,6 +126,10 @@ perl -pi -e "s/version = \"[^\"]+\"/version = \"$version\"/" build.gradle.kts # Update version in README.md perl -pi -e "s/com\.maxmind\.device:device-sdk:[0-9]+\.[0-9]+\.[0-9]+[a-zA-Z0-9\-]*/com.maxmind.device:device-sdk:$version/g" README.md +# Update baselineVersion in device-sdk/build.gradle.kts for API compatibility checking +# This ensures the next PR compares against this newly released version +perl -pi -e "s/val baselineVersion = \"[^\"]+\"/val baselineVersion = \"$version\"/" device-sdk/build.gradle.kts + git diff read -r -n 1 -p "Commit changes? (y/n) " should_commit @@ -135,7 +139,7 @@ if [ "$should_commit" != "y" ]; then exit 1 fi -git add build.gradle.kts README.md +git add build.gradle.kts README.md device-sdk/build.gradle.kts git commit -m "Preparing for $version" # Build and publish to Maven Central diff --git a/device-sdk/build.gradle.kts b/device-sdk/build.gradle.kts index 7c73861..e2d72f8 100644 --- a/device-sdk/build.gradle.kts +++ b/device-sdk/build.gradle.kts @@ -1,3 +1,5 @@ +import java.net.URI + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -8,6 +10,7 @@ plugins { alias(libs.plugins.maven.publish) signing id("tech.apter.junit5.jupiter.robolectric-extension-gradle-plugin") version "0.9.0" + id("me.champeau.gradle.japicmp") version "0.4.5" } android { @@ -168,3 +171,58 @@ mavenPublishing { signing { useGpgCmd() } + +// API compatibility checking with japicmp +// Compares the current build against the latest released version on Maven Central +// Update this version after each release (the release script should do this automatically) +val baselineVersion = "0.1.0" + +// Download baseline AAR directly from Maven Central to avoid local project resolution +val downloadBaselineAar by tasks.registering { + val outputFile = layout.buildDirectory.file("japicmp/baseline.aar") + outputs.file(outputFile) + doLast { + val url = "https://repo1.maven.org/maven2/com/maxmind/device/device-sdk/$baselineVersion/device-sdk-$baselineVersion.aar" + val destFile = outputFile.get().asFile + destFile.parentFile.mkdirs() + URI(url).toURL().openStream().use { input -> + destFile.outputStream().use { output -> + input.copyTo(output) + } + } + logger.lifecycle("Downloaded baseline AAR from $url") + } +} + +// Extract classes.jar from baseline AAR for comparison +val extractBaselineClasses by tasks.registering(Copy::class) { + dependsOn(downloadBaselineAar) + from(zipTree(layout.buildDirectory.file("japicmp/baseline.aar"))) { + include("classes.jar") + } + into(layout.buildDirectory.dir("japicmp/baseline")) +} + +// Extract classes.jar from current AAR for comparison +val extractCurrentClasses by tasks.registering(Copy::class) { + dependsOn("bundleReleaseAar") + from(zipTree(layout.buildDirectory.file("outputs/aar/device-sdk-release.aar"))) { + include("classes.jar") + } + into(layout.buildDirectory.dir("japicmp/current")) +} + +tasks.register("japicmp") { + dependsOn(extractBaselineClasses, extractCurrentClasses) + oldClasspath.from(layout.buildDirectory.file("japicmp/baseline/classes.jar")) + newClasspath.from(layout.buildDirectory.file("japicmp/current/classes.jar")) + oldArchives.from(layout.buildDirectory.file("japicmp/baseline/classes.jar")) + newArchives.from(layout.buildDirectory.file("japicmp/current/classes.jar")) + accessModifier.set("public") + onlyModified.set(true) + failOnModification.set(true) + includeSynthetic.set(false) + ignoreMissingClasses.set(true) + txtOutputFile.set(layout.buildDirectory.file("japicmp/report.txt")) + htmlOutputFile.set(layout.buildDirectory.file("japicmp/report.html")) +}