Skip to content

Commit e325308

Browse files
Sundar Raghavansundargthb
authored andcommitted
ci: add breaking change detection workflow for pull requests
1 parent 88c5101 commit e325308

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# .github/workflows/breaking-change-check.yml
2+
#
3+
# Analyzes every PR that touches src/bedrock_agentcore/ for public API
4+
# breaking changes and posts a comment on the PR with the results.
5+
#
6+
# Uses griffe (https://mkdocstrings.github.io/griffe/) — the standard Python
7+
# library for static API analysis, used by mkdocstrings, Rye, and others.
8+
# It understands __all__, class hierarchies, decorators, multi-line signatures,
9+
# and parameter kind changes — far beyond what git-diff heuristics can detect.
10+
#
11+
# This workflow is INFORMATIONAL ONLY. It never blocks a merge.
12+
# Intended use: reviewer awareness for breaking changes that may need a
13+
# CHANGELOG entry or a deprecation notice before removal.
14+
#
15+
16+
name: Breaking Change Check
17+
18+
on:
19+
pull_request:
20+
branches: [main]
21+
paths:
22+
- 'src/bedrock_agentcore/**'
23+
24+
permissions:
25+
contents: read # checkout
26+
pull-requests: write # post/update PR comment
27+
28+
jobs:
29+
check-breaking-changes:
30+
name: Detect Breaking Changes
31+
runs-on: ubuntu-latest
32+
33+
steps:
34+
- name: Checkout PR
35+
uses: actions/checkout@v6
36+
with:
37+
# Full history needed so griffe can create a worktree at the base ref.
38+
fetch-depth: 0
39+
40+
- name: Set up Python
41+
uses: actions/setup-python@v6
42+
with:
43+
python-version: '3.10'
44+
45+
- name: Install griffe
46+
run: pip install griffe
47+
48+
# Fetch the base branch so the ref is available for griffe's worktree.
49+
- name: Fetch base branch
50+
run: git fetch origin "${{ github.base_ref }}" --depth=1
51+
52+
# Run griffe using its Python API for stable, structured output.
53+
# The Python API is used instead of the CLI to:
54+
# a) get markdown-formatted output for the PR comment
55+
# b) handle errors gracefully without failing the job
56+
# c) avoid relying on CLI flag stability across griffe versions
57+
#
58+
# Exit codes written to GITHUB_OUTPUT:
59+
# exit_code=0 → no breaking changes
60+
# exit_code=1 → breaking changes found
61+
# exit_code=2 → griffe itself errored (treated as a warning, not a failure)
62+
- name: Run griffe analysis
63+
id: griffe
64+
env:
65+
BASE_REF: ${{ github.base_ref }}
66+
run: |
67+
python3 - <<'PYEOF'
68+
import griffe, sys, os
69+
70+
search = ["src"]
71+
base_ref = f"origin/{os.environ['BASE_REF']}"
72+
73+
# Load the old API from the base branch via a temporary git worktree.
74+
# Load the new API from the current working tree (the PR branch).
75+
try:
76+
old = griffe.load_git("bedrock_agentcore", ref=base_ref, search_paths=search)
77+
new = griffe.load("bedrock_agentcore", search_paths=search)
78+
except griffe.GitError as e:
79+
print(f"::warning::griffe could not create worktree for {base_ref}: {e}")
80+
print(f"::warning::Skipping breaking change analysis for this PR.")
81+
# Write a neutral comment and exit cleanly — don't block the job.
82+
with open("/tmp/breaking-changes.md", "w") as f:
83+
f.write(
84+
"## ℹ️ Breaking Change Analysis Skipped\n\n"
85+
"griffe could not load the base branch for comparison. "
86+
"This is usually a transient git issue. "
87+
"A maintainer can re-run this check manually."
88+
)
89+
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
90+
out.write("exit_code=2\n")
91+
sys.exit(0)
92+
except Exception as e:
93+
print(f"::warning::griffe analysis failed unexpectedly: {e}")
94+
with open("/tmp/breaking-changes.md", "w") as f:
95+
f.write(
96+
"## ℹ️ Breaking Change Analysis Skipped\n\n"
97+
f"griffe encountered an unexpected error: `{e}`"
98+
)
99+
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
100+
out.write("exit_code=2\n")
101+
sys.exit(0)
102+
103+
breakages = list(griffe.find_breaking_changes(old, new))
104+
105+
if not breakages:
106+
body = (
107+
"## ✅ No Breaking Changes Detected\n\n"
108+
"No public API breaking changes found in this PR."
109+
)
110+
with open("/tmp/breaking-changes.md", "w") as f:
111+
f.write(body)
112+
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
113+
out.write("exit_code=0\n")
114+
print("✓ No breaking changes detected.")
115+
sys.exit(0)
116+
117+
# Format output as markdown for the PR comment.
118+
lines = [
119+
"## ⚠️ Breaking Change Warning",
120+
"",
121+
f"Found **{len(breakages)}** potential breaking change(s) in this PR:",
122+
"",
123+
]
124+
for b in breakages:
125+
lines.append(b.explain(style=griffe.ExplanationStyle.markdown))
126+
127+
lines += [
128+
"",
129+
"---",
130+
"> **Note:** This is an automated static analysis check. "
131+
"Some flagged changes may be intentional.",
132+
"> Please confirm each item is expected and, if so, add a migration note to `CHANGELOG.md`.",
133+
]
134+
135+
body = "\n".join(lines)
136+
with open("/tmp/breaking-changes.md", "w") as f:
137+
f.write(body)
138+
139+
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
140+
out.write("exit_code=1\n")
141+
142+
print(f"⚠️ {len(breakages)} breaking change(s) found.")
143+
# Exit 0 here — the job itself always succeeds (informational).
144+
# The PR comment carries the signal; the job status does not block merge.
145+
sys.exit(0)
146+
PYEOF
147+
148+
# Post or update a single persistent comment on the PR.
149+
# Uses a hidden HTML marker so subsequent pushes update the same comment
150+
# rather than creating a new one per commit.
151+
- name: Post or update PR comment
152+
uses: actions/github-script@v8
153+
with:
154+
script: |
155+
const fs = require('fs');
156+
const body = fs.readFileSync('/tmp/breaking-changes.md', 'utf8');
157+
158+
// Marker that identifies the comment as ours for future updates.
159+
const marker = '<!-- breaking-change-check -->';
160+
const fullBody = `${marker}\n${body}`;
161+
162+
const { data: comments } = await github.rest.issues.listComments({
163+
owner: context.repo.owner,
164+
repo: context.repo.repo,
165+
issue_number: context.payload.pull_request.number,
166+
});
167+
168+
const existing = comments.find(c => c.body.includes(marker));
169+
170+
if (existing) {
171+
await github.rest.issues.updateComment({
172+
owner: context.repo.owner,
173+
repo: context.repo.repo,
174+
comment_id: existing.id,
175+
body: fullBody,
176+
});
177+
console.log(`Updated existing breaking change comment (id: ${existing.id})`);
178+
} else {
179+
await github.rest.issues.createComment({
180+
owner: context.repo.owner,
181+
repo: context.repo.repo,
182+
issue_number: context.payload.pull_request.number,
183+
body: fullBody,
184+
});
185+
console.log('Created new breaking change comment');
186+
}
187+
188+
// Log summary to the Actions step output for visibility.
189+
const exitCode = '${{ steps.griffe.outputs.exit_code }}';
190+
if (exitCode === '1') {
191+
core.warning('Breaking changes detected — see PR comment for details.');
192+
}

0 commit comments

Comments
 (0)