Skip to content

Commit 85f7249

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 85f7249

4 files changed

Lines changed: 164 additions & 1 deletion

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Scans commits between $BASE_SHA and $HEAD_SHA for Conventional Commits
4+
# breaking-change markers, restricted to the packages tracked by
5+
# .release-it.json. Writes `found` and (on match) `list` to $GITHUB_OUTPUT.
6+
#
7+
# Required env: BASE_SHA, HEAD_SHA, GITHUB_OUTPUT
8+
9+
set -euo pipefail
10+
11+
PATHS=(packages/appkit packages/appkit-ui packages/shared)
12+
13+
# Conventional Commits breaking-change markers:
14+
# 1. `type!:` or `type(scope)!:` in the subject line
15+
# 2. `BREAKING CHANGE:` or `BREAKING-CHANGE:` footer line
16+
PATTERN='^(feat|fix|chore|refactor|perf|build|ci|docs|style|test|revert)(\([^)]+\))?!:|^BREAKING[ -]CHANGE:'
17+
18+
breaking=""
19+
while IFS= read -r sha; do
20+
[ -z "$sha" ] && continue
21+
msg=$(git log -1 --format=%B "$sha")
22+
if printf '%s\n' "$msg" | grep -Eq "$PATTERN"; then
23+
subject=$(git log -1 --format=%s "$sha")
24+
breaking+="- \`${sha:0:7}\` ${subject}"$'\n'
25+
fi
26+
done < <(git rev-list "$BASE_SHA".."$HEAD_SHA" -- "${PATHS[@]}")
27+
28+
if [ -n "$breaking" ]; then
29+
{
30+
echo "found=true"
31+
echo "list<<COMMITS_EOF"
32+
printf '%s' "$breaking"
33+
echo "COMMITS_EOF"
34+
} >> "$GITHUB_OUTPUT"
35+
echo "Breaking commits found:"
36+
printf '%s' "$breaking"
37+
else
38+
echo "found=false" >> "$GITHUB_OUTPUT"
39+
echo "No breaking commits found."
40+
fi
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Upserts (or removes) a sticky PR comment summarizing breaking commits
3+
* detected by `detect-breaking-commits.sh`.
4+
*
5+
* Invoked via `actions/github-script`. Inputs come from environment vars:
6+
* FOUND - "true" if the scan found breaking commits
7+
* BREAKING_LIST - markdown bullet list of breaking commits
8+
* ALLOWED - "true" if the PR carries the allow-breaking-change label
9+
*/
10+
11+
const MARKER = "<!-- pr-breaking-change-check -->";
12+
13+
module.exports = async ({ github, context }) => {
14+
const { owner, repo } = context.repo;
15+
const issue_number = context.issue.number;
16+
const found = process.env.FOUND === "true";
17+
const allowed = process.env.ALLOWED === "true";
18+
const list = process.env.BREAKING_LIST || "";
19+
20+
const comments = await github.paginate(github.rest.issues.listComments, {
21+
owner,
22+
repo,
23+
issue_number,
24+
per_page: 100,
25+
});
26+
const existing = comments.find((c) => c.body?.includes(MARKER));
27+
28+
if (!found) {
29+
if (existing) {
30+
await github.rest.issues.deleteComment({
31+
owner,
32+
repo,
33+
comment_id: existing.id,
34+
});
35+
}
36+
return;
37+
}
38+
39+
const status = allowed
40+
? "> 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."
41+
: "> 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.";
42+
43+
const body = [
44+
MARKER,
45+
"### Breaking change detected",
46+
"",
47+
"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`:",
48+
"",
49+
list.trim(),
50+
"",
51+
"Merging this PR will force a **major** version bump on the next release (`bumpStrict: true` in `.release-it.json`).",
52+
"",
53+
status,
54+
].join("\n");
55+
56+
if (existing) {
57+
await github.rest.issues.updateComment({
58+
owner,
59+
repo,
60+
comment_id: existing.id,
61+
body,
62+
});
63+
} else {
64+
await github.rest.issues.createComment({
65+
owner,
66+
repo,
67+
issue_number,
68+
body,
69+
});
70+
}
71+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
run: bash .github/scripts/detect-breaking-commits.sh
35+
36+
- name: Upsert sticky PR comment
37+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
38+
env:
39+
FOUND: ${{ steps.scan.outputs.found }}
40+
BREAKING_LIST: ${{ steps.scan.outputs.list }}
41+
ALLOWED: ${{ contains(github.event.pull_request.labels.*.name, 'allow-breaking-change') }}
42+
with:
43+
script: |
44+
const upsert = require('./.github/scripts/upsert-breaking-change-comment.cjs');
45+
await upsert({ github, context });
46+
47+
- name: Fail unless explicitly allowed
48+
if: steps.scan.outputs.found == 'true' && !contains(github.event.pull_request.labels.*.name, 'allow-breaking-change')
49+
run: |
50+
echo "::error::Breaking-change commits detected in tracked packages. Add the 'allow-breaking-change' label to bypass, or rewrite the offending commits."
51+
exit 1

knip.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"packages/appkit/src/plugins/agents/load-agents.ts",
2424
"template/**",
2525
"tools/**",
26-
"docs/**"
26+
"docs/**",
27+
".github/scripts/**"
2728
],
2829
"ignoreDependencies": ["json-schema-to-typescript"],
2930
"ignoreBinaries": ["tarball"]

0 commit comments

Comments
 (0)