Some releases change the public binary API without it being flagged as a breaking change, which surfaces for consumers as a runtime NoSuchMethodError/NoClassDefFoundError (from a jar compiled against the previous version) rather than a compile error that would be caught early.
The clearest recent example is 0.8.15: #377 removed Lombok and hand-wrote the builders, which renamed the generated builder class DefaultX509Source.X509SourceOptions.X509SourceOptionsBuilder → Builder. Code compiled against <= 0.8.14 then fails at runtime against >= 0.8.15:
java.lang.NoSuchMethodError: 'io.spiffe.workloadapi.DefaultX509Source$X509SourceOptions$X509SourceOptionsBuilder io.spiffe.workloadapi.DefaultX509Source$X509SourceOptions.builder()'
It was listed under "Dependency updates" in the 0.8.15 notes, so the ABI impact appears to have been unintentional.
Proposal
Add a binary-compatibility gate to the build that diffs each build's public API against the last published release and fails on binary-incompatible changes, applied to the consumer-facing modules (java-spiffe-core, java-spiffe-provider).
For this repo specifically (Groovy Gradle, pure Java, multi-module, published to Maven Central via com.vanniktech.maven.publish), the japicmp Gradle plugin looks like the best fit: it applies per subproject and can resolve the previous release from Maven Central as the baseline automatically, so there's no per-release config to maintain. revapi is a more powerful alternative if richer rules are ever needed, though it is likely heavier than this project requires.
This wouldn't block intentional breaking changes; it would make them explicit (an allowlist entry plus a changelog note) instead of silent, so consumers can plan for them.
We're happy to open a PR to wire this up.
Some releases change the public binary API without it being flagged as a breaking change, which surfaces for consumers as a runtime
NoSuchMethodError/NoClassDefFoundError(from a jar compiled against the previous version) rather than a compile error that would be caught early.The clearest recent example is 0.8.15: #377 removed Lombok and hand-wrote the builders, which renamed the generated builder class
DefaultX509Source.X509SourceOptions.X509SourceOptionsBuilder→Builder. Code compiled against<= 0.8.14then fails at runtime against>= 0.8.15:It was listed under "Dependency updates" in the 0.8.15 notes, so the ABI impact appears to have been unintentional.
Proposal
Add a binary-compatibility gate to the build that diffs each build's public API against the last published release and fails on binary-incompatible changes, applied to the consumer-facing modules (
java-spiffe-core,java-spiffe-provider).For this repo specifically (Groovy Gradle, pure Java, multi-module, published to Maven Central via
com.vanniktech.maven.publish), the japicmp Gradle plugin looks like the best fit: it applies per subproject and can resolve the previous release from Maven Central as the baseline automatically, so there's no per-release config to maintain. revapi is a more powerful alternative if richer rules are ever needed, though it is likely heavier than this project requires.This wouldn't block intentional breaking changes; it would make them explicit (an allowlist entry plus a changelog note) instead of silent, so consumers can plan for them.
We're happy to open a PR to wire this up.