Skip to content
Open
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
39 changes: 0 additions & 39 deletions .github/workflows/api-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ on:
- opened
- synchronize
- reopened
- labeled
- unlabeled
workflow_dispatch:
inputs:
baseline_version:
Expand All @@ -29,8 +27,6 @@ jobs:
# Empty unless overridden via workflow_dispatch; the api-diff task then
# falls back to <api.diff.baseline.version> in pom.xml.
API_DIFF_BASELINE_VERSION: ${{ inputs.baseline_version }}
BREAKING_API_CHANGE_ACCEPTED: >-
${{ contains(github.event.pull_request.labels.*.name, 'breaking-api-change-accepted') }}

steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
Expand All @@ -54,38 +50,3 @@ jobs:
echo "Run 'mise run api-diff' locally and commit the updated docs/apidiffs."
exit 1
fi
- name: Fail on incompatible published API changes
run: |
python3 - <<'PY'
import os
from pathlib import Path
import sys
import xml.etree.ElementTree as ET

failures = []
for report in sorted(Path(".").glob("**/target/japicmp/api-diff.xml")):
parts = report.parts
module = "/".join(parts[: parts.index("target")])
tree = ET.parse(report)
for change in tree.findall(".//compatibilityChange"):
binary = change.get("binaryCompatible") == "false"
source = change.get("sourceCompatible") == "false"
if binary or source:
failures.append((module, change.get("type", "unknown")))

if not failures:
print("No incompatible published API changes detected.")
sys.exit(0)

print("Incompatible published API changes detected:")
for module, change_type in failures[:100]:
print(f"- {module}: {change_type}")
if len(failures) > 100:
print(f"... and {len(failures) - 100} more")
if os.environ.get("BREAKING_API_CHANGE_ACCEPTED") == "true":
print("Accepted by PR label `breaking-api-change-accepted`.")
sys.exit(0)
print("Run `mise run api-diff` locally for full japicmp output.")
print("Reports are written to `**/target/japicmp/*`.")
sys.exit(1)
PY
77 changes: 77 additions & 0 deletions .github/workflows/detect-api-changes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: Detect API Changes

on:
# pull_request_target is used so the workflow can update labels and comments
# without checking out or executing code from the PR branch.
# zizmor: ignore[dangerous-triggers] -- this workflow only reads PR metadata
# via the GitHub API and never checks out or executes code from the PR branch.
pull_request_target:
types:
- opened
- synchronize
- reopened
- ready_for_review

permissions: {}

jobs:
detect-api-changes:
if: github.repository == 'prometheus/client_java'
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
steps:
- name: Check for API changes and update PR
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
MARKER="<!-- api-change-detector -->"

api_files=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate \
--jq '.[] | select(.filename | startswith("docs/apidiffs/current_vs_latest/")) | .filename')

comment_id=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate \
--jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" | head -1)

if [[ -z "$api_files" ]]; then
echo "No API diff files changed."
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "api-change" 2>/dev/null || true
if [[ -n "$comment_id" ]]; then
gh api --method DELETE "repos/${REPO}/issues/comments/${comment_id}"
fi
exit 0
fi

echo "API diff files changed:"
echo "$api_files"

gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "api-change"

modules=$(echo "$api_files" \
| sed 's|docs/apidiffs/current_vs_latest/||' \
| sed 's|\.txt$||' \
| sort \
| sed 's/^/- /')

body=$(cat <<EOF
${MARKER}
## :warning: API changes detected — maintainer review required

This PR modifies the published API diff for the following module(s):

${modules}

Please review the changes in \`docs/apidiffs/current_vs_latest/\` carefully before approving.
EOF
)

if [[ -n "$comment_id" ]]; then
gh api --method PATCH "repos/${REPO}/issues/comments/${comment_id}" \
--field body="$body"
else
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$body"
fi
17 changes: 6 additions & 11 deletions docs/content/internals/stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,18 @@ constructors, methods, and fields when only part of a public type is stable.

## API diff check

CI runs [japicmp](https://siom79.github.io/japicmp/) against a pinned baseline release and fails on
incompatible changes to the `@StableApi` surface. Run it locally with:
CI runs [japicmp](https://siom79.github.io/japicmp/) against a pinned baseline release and writes
the published API diffs under `docs/apidiffs/current_vs_latest/`. Pull requests must keep those
checked-in diffs up to date. Run it locally with:

```bash
mise run api-diff
```

Reports are written to `**/target/japicmp/*`.
Raw reports are written to `**/target/japicmp/*`.

The baseline version is tracked in `pom.xml` and updated by Renovate; the published baseline diffs
are stored under `docs/apidiffs/`.

## Accepting breaking changes

Backwards-incompatible changes to the `@StableApi` surface are only allowed in a major version
bump. Within a major version line, the API diff check must pass.

When a major version is being prepared, intentional incompatible changes can be accepted by adding
the `breaking-api-change-accepted` label to the pull request. The label is not an escape hatch for
minor or patch releases.
Pull requests that change `docs/apidiffs/current_vs_latest/` are automatically labeled
`api-change` for additional maintainer review.