Skip to content

Commit fa4fd8e

Browse files
zeitlingerjaydeluca
authored andcommitted
feat: Add StableApi marker and API diff check (#2168)
## Summary - add `@StableApi` as the opt-in marker for published Java API - seed the guessed stable API surface from docs plus Micrometer/JMX usage - add `mise run api-diff` using japicmp against the configured baseline - add an API diff workflow that fails on incompatible published API changes ## Notes This is the bootstrap PR for the annotation-based API surface. Since `1.5.1` does not contain `@StableApi`, the first diff is noisy and mostly shows the seeded API surface as new. After a release contains the annotations, future diffs should be normal compatibility diffs. The workflow does not post PR comments or upload artifacts. If the check fails, run this locally: ```bash mise run api-diff ``` Reports are written to `**/target/japicmp/*`. Intentional incompatible changes can be accepted by adding the PR label `breaking-api-change-accepted`. ## Validation - `mise run api-diff` - `mise run build` - `mise run lint` --------- Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com> Signed-off-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent f1dc469 commit fa4fd8e

134 files changed

Lines changed: 558 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/renovate-tracked-deps.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
"mise"
1717
]
1818
},
19+
".github/workflows/api-diff.yml": {
20+
"regex": [
21+
"mise"
22+
]
23+
},
1924
".github/workflows/build.yml": {
2025
"regex": [
2126
"mise"

.github/workflows/api-diff.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
name: API Diff
3+
4+
on:
5+
pull_request:
6+
types:
7+
- opened
8+
- synchronize
9+
- reopened
10+
- labeled
11+
- unlabeled
12+
workflow_dispatch:
13+
inputs:
14+
baseline_version:
15+
description: Version to compare the PR artifacts against
16+
required: false
17+
default: "1.5.1"
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
api-diff:
24+
runs-on: ubuntu-24.04
25+
env:
26+
API_DIFF_BASELINE_VERSION: ${{ inputs.baseline_version || '1.5.1' }}
27+
BREAKING_API_CHANGE_ACCEPTED: >-
28+
${{ contains(github.event.pull_request.labels.*.name, 'breaking-api-change-accepted') }}
29+
30+
steps:
31+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
32+
with:
33+
persist-credentials: false
34+
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
35+
with:
36+
version: v2026.5.18
37+
sha256: cfac593469d028d7ae5fe36e37bd7c59118b5238e92d8a876209578464f24a84
38+
- name: Cache local Maven repository
39+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
40+
with:
41+
path: ~/.m2/repository
42+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
43+
- name: Run japicmp API diff
44+
run: mise run api-diff
45+
- name: Fail on incompatible published API changes
46+
run: |
47+
python3 - <<'PY'
48+
import os
49+
from pathlib import Path
50+
import sys
51+
import xml.etree.ElementTree as ET
52+
53+
failures = []
54+
for report in sorted(Path(".").glob("**/target/japicmp/api-diff.xml")):
55+
parts = report.parts
56+
module = "/".join(parts[: parts.index("target")])
57+
tree = ET.parse(report)
58+
for change in tree.findall(".//compatibilityChange"):
59+
binary = change.get("binaryCompatible") == "false"
60+
source = change.get("sourceCompatible") == "false"
61+
if binary or source:
62+
failures.append((module, change.get("type", "unknown")))
63+
64+
if not failures:
65+
print("No incompatible published API changes detected.")
66+
sys.exit(0)
67+
68+
print("Incompatible published API changes detected:")
69+
for module, change_type in failures[:100]:
70+
print(f"- {module}: {change_type}")
71+
if len(failures) > 100:
72+
print(f"... and {len(failures) - 100} more")
73+
if os.environ.get("BREAKING_API_CHANGE_ACCEPTED") == "true":
74+
print("Accepted by PR label `breaking-api-change-accepted`.")
75+
sys.exit(0)
76+
print("Run `mise run api-diff` locally for full japicmp output.")
77+
print("Reports are written to `**/target/japicmp/*`.")
78+
sys.exit(1)
79+
PY

.github/workflows/codeql.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
- name: Build (CodeQL traces the build)
5050
run: >
5151
./mvnw clean compile
52+
-P '!default'
5253
-DskipTests
5354
-Dcoverage.skip=true
5455
-Dcheckstyle.skip=true

mise.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ run = "./mvnw verify"
5959
description = "build all modules without tests"
6060
run = "./mvnw install -DskipTests -Dcoverage.skip=true"
6161

62+
[tasks."api-diff"]
63+
description = "Compare published API against the configured japicmp baseline"
64+
run = """
65+
BASELINE_VERSION="${API_DIFF_BASELINE_VERSION:-1.5.1}"
66+
./mvnw -B verify \
67+
-P 'api-diff,!examples-and-integration-tests' \
68+
-Dapi.diff.baseline.version="${BASELINE_VERSION}" \
69+
-DskipTests \
70+
-Dcoverage.skip=true \
71+
-Dcheckstyle.skip=true \
72+
-Dwarnings=-nowarn
73+
"""
74+
6275
[tasks."lint"]
6376
description = "Run all lints"
6477
depends = ["lint:bom"]

pom.xml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<otel.instrumentation.version>2.28.1-alpha</otel.instrumentation.version>
3030
<java.version>8</java.version>
3131
<test.java.version>25</test.java.version>
32+
<api.diff.baseline.version>1.5.1</api.diff.baseline.version>
3233
<jacoco.line-coverage>0.70</jacoco.line-coverage>
3334
<checkstyle.skip>false</checkstyle.skip>
3435
<coverage.skip>false</coverage.skip>
@@ -38,6 +39,7 @@
3839

3940
<modules>
4041
<module>prometheus-metrics-parent</module>
42+
<module>prometheus-metrics-annotations</module>
4143
<module>prometheus-metrics-bom</module>
4244
<module>prometheus-metrics-core</module>
4345
<module>prometheus-metrics-config</module>
@@ -171,6 +173,11 @@
171173
<artifactId>exec-maven-plugin</artifactId>
172174
<version>3.6.3</version>
173175
</plugin>
176+
<plugin>
177+
<groupId>com.github.siom79.japicmp</groupId>
178+
<artifactId>japicmp-maven-plugin</artifactId>
179+
<version>0.26.1</version>
180+
</plugin>
174181
</plugins>
175182
</pluginManagement>
176183
<plugins>
@@ -401,6 +408,71 @@
401408
</plugins>
402409
</build>
403410
</profile>
411+
<profile>
412+
<id>api-diff</id>
413+
<build>
414+
<plugins>
415+
<plugin>
416+
<groupId>com.github.siom79.japicmp</groupId>
417+
<artifactId>japicmp-maven-plugin</artifactId>
418+
<configuration>
419+
<oldVersion>
420+
<dependency>
421+
<groupId>${project.groupId}</groupId>
422+
<artifactId>${project.artifactId}</artifactId>
423+
<version>${api.diff.baseline.version}</version>
424+
<type>jar</type>
425+
</dependency>
426+
</oldVersion>
427+
<newVersion>
428+
<file>
429+
<path>${project.build.directory}/${project.build.finalName}.jar</path>
430+
</file>
431+
</newVersion>
432+
<parameter>
433+
<accessModifier>public</accessModifier>
434+
<onlyModified>true</onlyModified>
435+
<includes>
436+
<include>io.prometheus.metrics.annotations.StableApi</include>
437+
<include>@io.prometheus.metrics.annotations.StableApi</include>
438+
</includes>
439+
<excludes>
440+
<exclude>io.prometheus.metrics.expositionformats.generated</exclude>
441+
<exclude>io.prometheus.metrics.shaded</exclude>
442+
</excludes>
443+
<breakBuildOnModifications>false</breakBuildOnModifications>
444+
<breakBuildOnBinaryIncompatibleModifications>
445+
false
446+
</breakBuildOnBinaryIncompatibleModifications>
447+
<breakBuildOnSourceIncompatibleModifications>
448+
false
449+
</breakBuildOnSourceIncompatibleModifications>
450+
<breakBuildBasedOnSemanticVersioning>false</breakBuildBasedOnSemanticVersioning>
451+
<ignoreMissingClasses>true</ignoreMissingClasses>
452+
<ignoreMissingOldVersion>true</ignoreMissingOldVersion>
453+
<ignoreMissingOptionalDependency>true</ignoreMissingOptionalDependency>
454+
<skipPomModules>true</skipPomModules>
455+
<skipHtmlReport>true</skipHtmlReport>
456+
<reportOnlyFilename>true</reportOnlyFilename>
457+
<packagingSupporteds>
458+
<packagingSupported>bundle</packagingSupported>
459+
<packagingSupported>jar</packagingSupported>
460+
</packagingSupporteds>
461+
</parameter>
462+
</configuration>
463+
<executions>
464+
<execution>
465+
<id>api-diff</id>
466+
<phase>verify</phase>
467+
<goals>
468+
<goal>cmp</goal>
469+
</goals>
470+
</execution>
471+
</executions>
472+
</plugin>
473+
</plugins>
474+
</build>
475+
</profile>
404476
<profile>
405477
<id>errorprone</id>
406478
<activation>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<parent>
6+
<groupId>io.prometheus</groupId>
7+
<artifactId>client_java</artifactId>
8+
<version>1.6.2-SNAPSHOT</version>
9+
</parent>
10+
11+
<artifactId>prometheus-metrics-annotations</artifactId>
12+
<packaging>bundle</packaging>
13+
14+
<name>Prometheus Metrics Annotations</name>
15+
<description>
16+
Annotations for Prometheus Metrics library API contracts.
17+
</description>
18+
19+
<properties>
20+
<automatic.module.name>io.prometheus.metrics.annotations</automatic.module.name>
21+
</properties>
22+
</project>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.prometheus.metrics.annotations;
2+
3+
import static java.lang.annotation.ElementType.CONSTRUCTOR;
4+
import static java.lang.annotation.ElementType.FIELD;
5+
import static java.lang.annotation.ElementType.METHOD;
6+
import static java.lang.annotation.ElementType.TYPE;
7+
import static java.lang.annotation.RetentionPolicy.CLASS;
8+
9+
import java.lang.annotation.Documented;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Marks a Java element as part of the stable, published Prometheus Metrics API.
15+
*
16+
* <p>Use this on public or protected types to publish the type and its members. Use it on
17+
* individual constructors, methods, and fields when only part of a public type is stable.
18+
*/
19+
@Documented
20+
@Retention(CLASS)
21+
@Target({TYPE, CONSTRUCTOR, METHOD, FIELD})
22+
public @interface StableApi {}

prometheus-metrics-bom/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919

2020
<dependencyManagement>
2121
<dependencies>
22+
<dependency>
23+
<groupId>io.prometheus</groupId>
24+
<artifactId>prometheus-metrics-annotations</artifactId>
25+
<version>${project.version}</version>
26+
</dependency>
2227
<dependency>
2328
<groupId>io.prometheus</groupId>
2429
<artifactId>prometheus-metrics-config</artifactId>

prometheus-metrics-config/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
</properties>
2222

2323
<dependencies>
24+
<dependency>
25+
<groupId>io.prometheus</groupId>
26+
<artifactId>prometheus-metrics-annotations</artifactId>
27+
<version>${project.version}</version>
28+
<optional>true</optional>
29+
</dependency>
2430
<dependency>
2531
<groupId>org.junit-pioneer</groupId>
2632
<artifactId>junit-pioneer</artifactId>

prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/EscapingScheme.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package io.prometheus.metrics.config;
22

3+
import io.prometheus.metrics.annotations.StableApi;
34
import javax.annotation.Nullable;
45

6+
@StableApi
57
public enum EscapingScheme {
68
/** NO_ESCAPING indicates that a name will not be escaped. */
79
ALLOW_UTF8("allow-utf-8"),

0 commit comments

Comments
 (0)