Skip to content

Commit 3649f77

Browse files
zeitlingerjaydeluca
authored andcommitted
feat: track api-diff baseline via Renovate and store diffs in docs/apidiffs (#2174)
## Summary Fixes the manual-bump drift in the api-diff baseline (#2170) and makes API changes visible in review. - **Single source of truth.** The baseline lives only in the `<api.diff.baseline.version>` pom property (bumped from the already-drifted `1.5.1` to the actual latest release `1.6.1`). `mise.toml` and `api-diff.yml` no longer hardcode it. - **Renovate owns the bump.** A custom regex manager tracks the latest *published* `io.prometheus:prometheus-metrics-core` on Maven Central and bumps the property on the `renovate/api-diff-baseline` branch. Because Renovate only proposes published versions, there is no Maven Central propagation race and no post-release workflow or app token needed. - **Diffs committed to `docs/apidiffs/`.** `mise run api-diff` syncs the japicmp per-module reports into `docs/apidiffs/<module>.diff` via `.github/scripts/sync-api-diffs.sh`, stripping the volatile preamble so files only churn on real API changes. The api-diff workflow fails if they are stale, so every API change shows up in the PR diff. - **Regeneration on bump.** `generate-api-diff-baseline.yml` regenerates `docs/apidiffs` on the Renovate branch and pushes it back, mirroring `generate-protobuf.yml`. - `docs/apidiffs/**` is marked `linguist-generated` so flint skips it. ## Notes - The seed diffs are large because `1.6.1` predates `@StableApi` (the bootstrap noise documented in #2168). They shrink to near-empty once a release contains the annotations. - Like the protobuf flow, a `GITHUB_TOKEN` push doesn't re-trigger CI — close/reopen the Renovate bump PR to run the api-diff check after regeneration (noted in the workflow). ## Validation - `mise run api-diff` (generates the 25 seed diffs; verified idempotent) - `mise run lint` - `renovate-config-validator --strict`, actionlint, zizmor Closes #2170 ## Also fixes a pre-existing compat-test break on `main` `micrometer-compatibility` and `jmx-exporter-compatibility` fail on `main` (e.g. [#2173](#2173)), unrelated to this change. The compat harness installs local artifacts with `-Dmaven.test.skip=true`, which skips building the `*:test-jar` artifacts that the `activeByDefault` `default` profiles declare as test dependencies, breaking resolution (e.g. `prometheus-metrics-exposition-textformats:jar:tests`). Fixed by deactivating those profiles in the compat install (`-P !default`), matching what the release task already does (`-P release,!default`). Verified locally: full `mvnw install -Dmaven.test.skip=true -P !default` → BUILD SUCCESS. ## Reviewer note: the seed diffs show the full stable surface (expected) The committed `docs/apidiffs/current_vs_latest/*.txt` list the **entire** `@StableApi` surface as additions, not a small delta. This is the documented bootstrap state from #2168, not a bug: - The japicmp include filter is `@io.prometheus.metrics.annotations.StableApi`. The baseline `1.6.1` jar predates `@StableApi`, so its filtered surface is empty → every annotated class in current code registers as "NEW". The header genuinely compares against the `1.6.1` jar; the annotation filter is what makes everything look added. - Verified the filter is correct: only `@StableApi`-annotated types appear (non-stable classes like `CKMSQuantiles`/`Buffer` are excluded; `CallbackMetric` shows only as an inherited `NEW SUPERCLASS` reference). It is the same root cause as the `breaking-api-change-accepted` label on this PR. It self-corrects after the next release ships `@StableApi`: Renovate bumps the baseline to that release, the bump workflow regenerates `current_vs_latest/` as an annotated-vs-annotated (near-empty) diff, and the archived `<new>_vs_<old>/` becomes the first real release diff. So the current files are best read as a one-time record of the initial declared stable API surface. --------- Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com> Signed-off-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent 03c9994 commit 3649f77

35 files changed

Lines changed: 2057 additions & 7 deletions

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
CHANGELOG.md linguist-generated
22
**/src/main/generated/** linguist-generated
3+
docs/apidiffs/** linguist-generated
34
docs/** linguist-documentation

.github/renovate-tracked-deps.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
"mise"
2727
]
2828
},
29+
".github/workflows/generate-api-diff-baseline.yml": {
30+
"regex": [
31+
"mise"
32+
]
33+
},
2934
".github/workflows/generate-protobuf.yml": {
3035
"regex": [
3136
"mise"
@@ -176,6 +181,11 @@
176181
"maven-wrapper": [
177182
"maven-wrapper"
178183
]
184+
},
185+
"pom.xml": {
186+
"regex": [
187+
"io.prometheus:prometheus-metrics-core"
188+
]
179189
}
180190
}
181191
}

.github/renovate.json5

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@
3737
matchDepNames: ["com.google.protobuf:protobuf-java", "protoc"],
3838
groupName: "protobuf",
3939
separateMajorMinor: false,
40+
},
41+
{
42+
description: "Isolate the api-diff baseline on renovate/api-diff-baseline so generate-api-diff-baseline can regenerate docs/apidiffs; merge manually after a close/reopen re-runs CI",
43+
matchFileNames: ["pom.xml"],
44+
matchPackageNames: ["io.prometheus:prometheus-metrics-core"],
45+
groupName: "api-diff-baseline",
46+
automerge: false,
47+
},
48+
{
49+
"description": "Flint autofix: align extractVersion for protoc",
50+
"extractVersion": "^(?<version>.+)\\.0\\.0$",
51+
"matchDepNames": [
52+
"protoc"
53+
]
4054
}
4155
],
4256
customManagers: [
@@ -48,5 +62,13 @@
4862
"# renovate: datasource=(?<datasource>[a-z-]+?)(?: depName=(?<depName>.+?))?(?: packageName=(?<packageName>.+?))?(?: versioning=(?<versioning>[a-z-]+?))?\\s.+?_VERSION\\s*=\\s*\"?(?<currentValue>[^@\"]+?)(?:@(?<currentDigest>sha256:[a-f0-9]+))?\"?\\s"
4963
]
5064
},
65+
{
66+
"customType": "regex",
67+
"description": "update the api-diff baseline to the latest published release",
68+
"managerFilePatterns": ["/^pom\\.xml$/"],
69+
"matchStrings": [
70+
"<!-- renovate: datasource=(?<datasource>[a-z-]+?) packageName=(?<packageName>.+?) -->\\s*<api\\.diff\\.baseline\\.version>(?<currentValue>[^<]+)</api\\.diff\\.baseline\\.version>"
71+
]
72+
},
5173
],
5274
}

.github/scripts/sync-api-diffs.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
# Refresh docs/apidiffs/current_vs_latest/ from the japicmp reports produced by
3+
# `mvn verify -P api-diff`.
4+
#
5+
# Each StableApi module gets one committed <module>.txt describing how its
6+
# published API surface differs from the baseline release
7+
# (<api.diff.baseline.version> in pom.xml). Committing these makes every API
8+
# change visible in the pull request diff. The CI check fails when the
9+
# regenerated files differ from what is committed.
10+
#
11+
# The layout and format match opentelemetry-java's docs/apidiffs: the
12+
# "Comparing ... .jar against ... .jar" header is kept, while the constant
13+
# ignore-missing-classes warning and the semantic-versioning suggestion are
14+
# dropped as noise.
15+
set -euo pipefail
16+
17+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
18+
out_dir="$repo_root/docs/apidiffs/current_vs_latest"
19+
20+
shopt -s nullglob globstar
21+
22+
reports=("$repo_root"/**/target/japicmp/api-diff.diff)
23+
if [ "${#reports[@]}" -eq 0 ]; then
24+
echo "No japicmp reports found. Run 'mvn verify -P api-diff' first." >&2
25+
exit 1
26+
fi
27+
28+
rm -rf "$out_dir"
29+
mkdir -p "$out_dir"
30+
31+
for report in "${reports[@]}"; do
32+
# report path: <repo>/<module path>/target/japicmp/api-diff.diff
33+
module="$(basename "$(dirname "$(dirname "$(dirname "$report")")")")"
34+
grep -vE '^(WARNING: You are using the option|Semantic versioning suggestion)' \
35+
"$report" >"$out_dir/$module.txt"
36+
done
37+
38+
echo "Wrote ${#reports[@]} API diff report(s) to docs/apidiffs/current_vs_latest/."

.github/workflows/api-diff.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ on:
1212
workflow_dispatch:
1313
inputs:
1414
baseline_version:
15-
description: Version to compare the PR artifacts against
15+
description: >-
16+
Override the baseline version to compare against.
17+
Defaults to <api.diff.baseline.version> in pom.xml.
1618
required: false
17-
default: "1.5.1"
19+
default: ""
1820

1921
permissions:
2022
contents: read
@@ -23,7 +25,9 @@ jobs:
2325
api-diff:
2426
runs-on: ubuntu-24.04
2527
env:
26-
API_DIFF_BASELINE_VERSION: ${{ inputs.baseline_version || '1.5.1' }}
28+
# Empty unless overridden via workflow_dispatch; the api-diff task then
29+
# falls back to <api.diff.baseline.version> in pom.xml.
30+
API_DIFF_BASELINE_VERSION: ${{ inputs.baseline_version }}
2731
BREAKING_API_CHANGE_ACCEPTED: >-
2832
${{ contains(github.event.pull_request.labels.*.name, 'breaking-api-change-accepted') }}
2933
@@ -42,6 +46,13 @@ jobs:
4246
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
4347
- name: Run japicmp API diff
4448
run: mise run api-diff
49+
- name: Check docs/apidiffs is up to date
50+
run: |
51+
if ! git diff --exit-code -- docs/apidiffs; then
52+
echo "::error::Published API surface changed but docs/apidiffs is stale."
53+
echo "Run 'mise run api-diff' locally and commit the updated docs/apidiffs."
54+
exit 1
55+
fi
4556
- name: Fail on incompatible published API changes
4657
run: |
4758
python3 - <<'PY'
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
name: Generate API Diff Baseline
3+
4+
# When Renovate bumps <api.diff.baseline.version> (on the renovate/api-diff-baseline
5+
# branch), regenerate docs/apidiffs against the new baseline and push the result
6+
# back onto the PR. Renovate only proposes versions already published to Maven
7+
# Central, so japicmp can always resolve the new baseline.
8+
#
9+
# Before regenerating, the previous current_vs_latest is archived as
10+
# docs/apidiffs/<new>_vs_<old>/ to keep a per-release history (like
11+
# opentelemetry-java). This is an approximation of a release-vs-release diff:
12+
# the archived files compare the dev snapshot (which has just become <new>)
13+
# against <old>, so any commits merged between the release and this bump are
14+
# included too.
15+
#
16+
# Mirrors generate-protobuf.yml: a read-only `generate` job produces a patch
17+
# artifact and a privileged `publish` job pushes it back.
18+
19+
on:
20+
push:
21+
branches:
22+
- "renovate/api-diff-baseline"
23+
24+
permissions: {}
25+
26+
jobs:
27+
generate:
28+
runs-on: ubuntu-24.04
29+
permissions:
30+
contents: read # checkout + read-only `git fetch origin main` for the verify step
31+
steps:
32+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
33+
with:
34+
ref: ${{ github.ref }}
35+
persist-credentials: false
36+
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
37+
with:
38+
version: v2026.5.18
39+
sha256: cfac593469d028d7ae5fe36e37bd7c59118b5238e92d8a876209578464f24a84
40+
- name: Cache local Maven repository
41+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
42+
with:
43+
path: ~/.m2/repository
44+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
45+
restore-keys: |
46+
${{ runner.os }}-maven-
47+
- name: Archive the previous current_vs_latest
48+
run: |
49+
git fetch origin main
50+
baseline() {
51+
grep -oP '(?<=<api\.diff\.baseline\.version>)[^<]+' "$1"
52+
}
53+
OLD=$(git show origin/main:pom.xml | baseline /dev/stdin)
54+
NEW=$(baseline pom.xml)
55+
if [[ -z "$OLD" || -z "$NEW" ]]; then
56+
echo "::error::Could not read api.diff.baseline.version"
57+
exit 1
58+
fi
59+
if [[ "$OLD" == "$NEW" ]]; then
60+
echo "::error::api.diff.baseline.version unchanged ($NEW); nothing to bump"
61+
exit 1
62+
fi
63+
if [[ -d docs/apidiffs/current_vs_latest ]]; then
64+
cp -r docs/apidiffs/current_vs_latest "docs/apidiffs/${NEW}_vs_${OLD}"
65+
echo "Archived current_vs_latest as ${NEW}_vs_${OLD}"
66+
fi
67+
- name: Regenerate docs/apidiffs
68+
run: mise run api-diff
69+
- name: Validate and export docs/apidiffs as a patch
70+
run: |
71+
# Stage first so newly added files (the archived dir) are captured.
72+
git add docs/apidiffs
73+
OUTSIDE=$(git status --porcelain -- ':(exclude)docs/apidiffs')
74+
if [[ -n "$OUTSIDE" ]]; then
75+
echo "::error::Unexpected changes outside docs/apidiffs:"
76+
echo "$OUTSIDE"
77+
exit 1
78+
fi
79+
git diff --cached -- docs/apidiffs > /tmp/api-diff-baseline.patch
80+
- name: Upload generated patch
81+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
82+
with:
83+
name: api-diff-baseline-patch
84+
path: /tmp/api-diff-baseline.patch
85+
retention-days: 5
86+
87+
publish:
88+
runs-on: ubuntu-24.04
89+
needs: generate
90+
permissions:
91+
contents: write # push regenerated docs/apidiffs back to the renovate branch
92+
steps:
93+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
94+
with:
95+
ref: ${{ github.ref }}
96+
# zizmor: ignore[artipacked] -- needs credentials to push
97+
persist-credentials: true
98+
- name: Download generated patch
99+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
100+
with:
101+
name: api-diff-baseline-patch
102+
path: /tmp/patch
103+
- name: Commit and push regenerated docs/apidiffs
104+
run: |
105+
PATCH=/tmp/patch/api-diff-baseline.patch
106+
if [[ ! -s "$PATCH" ]]; then
107+
echo "No regenerated changes to commit"
108+
exit 0
109+
fi
110+
git apply "$PATCH"
111+
# Note: GITHUB_TOKEN pushes don't trigger CI re-runs.
112+
# Close and reopen the PR to trigger CI after this commit.
113+
git config user.name "github-actions[bot]"
114+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
115+
git add docs/apidiffs
116+
git commit -m "chore: regenerate docs/apidiffs"
117+
git push

.mise/lib/jmx_exporter_compat.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ def install_local_artifacts(root_dir: Path = Path.cwd()) -> None:
9797
# our main artifacts, and our test sources target a newer release than
9898
# the compatibility JDK supports.
9999
"-Dmaven.test.skip=true",
100+
# Deactivate the activeByDefault profiles that add test-only
101+
# dependencies (incl. a test-jar). With maven.test.skip those are not
102+
# built, so leaving the profile active breaks dependency resolution.
103+
# Same approach the release task uses (-P 'release,!default').
104+
"-P",
105+
"!default",
100106
"-Dcoverage.skip=true",
101107
"-Dcheckstyle.skip=true",
102108
"-Dwarnings=-nowarn",

.mise/lib/micrometer_compat.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ def install_local_artifacts(root_dir: Path = Path.cwd()) -> None:
129129
# our main artifacts, and our test sources target a newer release than
130130
# the compatibility JDK supports.
131131
"-Dmaven.test.skip=true",
132+
# Deactivate the activeByDefault profiles that add test-only
133+
# dependencies (incl. a test-jar). With maven.test.skip those are not
134+
# built, so leaving the profile active breaks dependency resolution.
135+
# Same approach the release task uses (-P 'release,!default').
136+
"-P",
137+
"!default",
132138
"-Dcoverage.skip=true",
133139
"-Dcheckstyle.skip=true",
134140
"-Dwarnings=-nowarn",

docs/apidiffs/current_vs_latest/prometheus-metrics-annotations.txt

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)