Skip to content

Commit fe49bf2

Browse files
committed
ci: detect breaking-change commits in PRs
Adds a workflow that scans every commit in a pull request for Conventional Commits breaking-change markers (`type!:` or `BREAKING CHANGE:` footer) limited to the packages tracked by .release-it.json (appkit, appkit-ui, shared). When a breaking commit is found, the job posts a sticky PR comment explaining the major-version impact and fails the check, unless the PR carries an `allow-breaking-change` label. This prevents accidental major bumps from landing on main once the new check is added to branch-protection required checks. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
1 parent 964e40e commit fe49bf2

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: PR Breaking Change Check
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, labeled, unlabeled]
6+
branches:
7+
- main
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
detect-breaking:
19+
name: Detect Breaking Commits
20+
runs-on:
21+
group: databricks-protected-runner-group
22+
labels: linux-ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
25+
with:
26+
fetch-depth: 0
27+
ref: ${{ github.event.pull_request.head.sha }}
28+
29+
- name: Find breaking commits
30+
id: scan
31+
env:
32+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
33+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
34+
# Only commits that touch the packages tracked by .release-it.json
35+
# are reported, since those are the ones that influence the next
36+
# released version. A breaking change in docs-only or tooling-only
37+
# commits doesn't bump the published packages, so flagging them
38+
# would be noise.
39+
run: |
40+
set -euo pipefail
41+
42+
PATHS=(packages/appkit packages/appkit-ui packages/shared)
43+
44+
# Conventional Commits breaking-change markers:
45+
# 1. `type!:` or `type(scope)!:` in the subject line
46+
# 2. `BREAKING CHANGE:` or `BREAKING-CHANGE:` footer line
47+
PATTERN='^(feat|fix|chore|refactor|perf|build|ci|docs|style|test|revert)(\([^)]+\))?!:|^BREAKING[ -]CHANGE:'
48+
49+
breaking=""
50+
while IFS= read -r sha; do
51+
[ -z "$sha" ] && continue
52+
msg=$(git log -1 --format=%B "$sha")
53+
if printf '%s\n' "$msg" | grep -Eq "$PATTERN"; then
54+
subject=$(git log -1 --format=%s "$sha")
55+
breaking+="- \`${sha:0:7}\` ${subject}"$'\n'
56+
fi
57+
done < <(git rev-list "$BASE_SHA".."$HEAD_SHA" -- "${PATHS[@]}")
58+
59+
if [ -n "$breaking" ]; then
60+
{
61+
echo "found=true"
62+
echo "list<<COMMITS_EOF"
63+
printf '%s' "$breaking"
64+
echo "COMMITS_EOF"
65+
} >> "$GITHUB_OUTPUT"
66+
echo "Breaking commits found:"
67+
printf '%s' "$breaking"
68+
else
69+
echo "found=false" >> "$GITHUB_OUTPUT"
70+
echo "No breaking commits found."
71+
fi
72+
73+
- name: Upsert sticky PR comment
74+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
75+
env:
76+
FOUND: ${{ steps.scan.outputs.found }}
77+
BREAKING_LIST: ${{ steps.scan.outputs.list }}
78+
ALLOWED: ${{ contains(github.event.pull_request.labels.*.name, 'allow-breaking-change') }}
79+
with:
80+
script: |
81+
const marker = '<!-- pr-breaking-change-check -->';
82+
const { owner, repo } = context.repo;
83+
const issue_number = context.issue.number;
84+
const found = process.env.FOUND === 'true';
85+
const allowed = process.env.ALLOWED === 'true';
86+
const list = process.env.BREAKING_LIST || '';
87+
88+
const comments = await github.paginate(
89+
github.rest.issues.listComments,
90+
{ owner, repo, issue_number, per_page: 100 },
91+
);
92+
const existing = comments.find((c) => c.body && c.body.includes(marker));
93+
94+
if (!found) {
95+
if (existing) {
96+
await github.rest.issues.deleteComment({
97+
owner,
98+
repo,
99+
comment_id: existing.id,
100+
});
101+
}
102+
return;
103+
}
104+
105+
const status = allowed
106+
? '> This PR has the `allow-breaking-change` label, so this check will pass. Make sure the next release is intentionally bumped to a major version.'
107+
: '> Add the **`allow-breaking-change`** label to this PR if the breaking change is intentional, or rewrite the offending commits to remove the `!` / `BREAKING CHANGE:` footer.';
108+
109+
const body = [
110+
marker,
111+
'### Breaking change detected',
112+
'',
113+
'The following commits in this PR contain Conventional Commits breaking-change markers (`type!:` or `BREAKING CHANGE:` footer) and touch packages tracked by `.release-it.json`:',
114+
'',
115+
list.trim(),
116+
'',
117+
'Merging this PR will force a **major** version bump on the next release (`bumpStrict: true` in `.release-it.json`).',
118+
'',
119+
status,
120+
].join('\n');
121+
122+
if (existing) {
123+
await github.rest.issues.updateComment({
124+
owner,
125+
repo,
126+
comment_id: existing.id,
127+
body,
128+
});
129+
} else {
130+
await github.rest.issues.createComment({
131+
owner,
132+
repo,
133+
issue_number,
134+
body,
135+
});
136+
}
137+
138+
- name: Fail unless explicitly allowed
139+
if: steps.scan.outputs.found == 'true' && !contains(github.event.pull_request.labels.*.name, 'allow-breaking-change')
140+
run: |
141+
echo "::error::Breaking-change commits detected in tracked packages. Add the 'allow-breaking-change' label to bypass, or rewrite the offending commits."
142+
exit 1

0 commit comments

Comments
 (0)