Skip to content

Commit ba7276d

Browse files
committed
chore: check BREAKING changes in a maven release
1 parent 118865a commit ba7276d

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

.github/workflows/release_maven.yml

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ on:
2121
permissions:
2222
contents: write
2323
id-token: write
24+
pull-requests: read
2425

2526
env:
2627
AWS_REGION: us-west-2
@@ -34,6 +35,211 @@ jobs:
3435
uses: actions/checkout@v6
3536
with:
3637
fetch-depth: 0
38+
39+
- name: Determine previous release tag
40+
id: previous_release
41+
shell: bash
42+
run: |
43+
previous_tag="$(git tag --merged HEAD --list 'v*' --sort=-version:refname | head -n 1)"
44+
if [ -z "$previous_tag" ]; then
45+
echo "No previous release tag found. Skipping BREAKING change validation."
46+
echo "tag=" >> "$GITHUB_OUTPUT"
47+
exit 0
48+
fi
49+
50+
echo "Found previous release tag: $previous_tag"
51+
echo "tag=$previous_tag" >> "$GITHUB_OUTPUT"
52+
53+
- name: Collect release commit SHAs
54+
id: release_commits
55+
if: steps.previous_release.outputs.tag != ''
56+
shell: bash
57+
run: |
58+
{
59+
echo "shas<<EOF"
60+
git rev-list "${{ steps.previous_release.outputs.tag }}..HEAD"
61+
echo "EOF"
62+
} >> "$GITHUB_OUTPUT"
63+
64+
- name: Collect release pull request numbers from git history
65+
id: release_pull_requests
66+
if: steps.previous_release.outputs.tag != ''
67+
shell: bash
68+
run: |
69+
{
70+
echo "numbers<<EOF"
71+
git log --format='%s' "${{ steps.previous_release.outputs.tag }}..HEAD" |
72+
sed -nE 's/^.*\(#([0-9]+)\)$/\1/p; s/^Merge pull request #([0-9]+).*$/\1/p' |
73+
sort -u
74+
echo "EOF"
75+
} >> "$GITHUB_OUTPUT"
76+
77+
- name: Validate BREAKING changes require major version bump
78+
if: steps.previous_release.outputs.tag != ''
79+
uses: actions/github-script@v8
80+
env:
81+
PREVIOUS_TAG: ${{ steps.previous_release.outputs.tag }}
82+
RELEASE_VERSION: ${{ github.event.inputs.release_version }}
83+
RELEASE_COMMITS: ${{ steps.release_commits.outputs.shas }}
84+
RELEASE_PULL_REQUESTS: ${{ steps.release_pull_requests.outputs.numbers }}
85+
with:
86+
script: |
87+
const previousTag = process.env.PREVIOUS_TAG;
88+
const releaseVersion = process.env.RELEASE_VERSION;
89+
const commitShas = process.env.RELEASE_COMMITS
90+
.split('\n')
91+
.map((sha) => sha.trim())
92+
.filter(Boolean);
93+
const releasePullRequestNumbers = process.env.RELEASE_PULL_REQUESTS
94+
.split('\n')
95+
.map((number) => number.trim())
96+
.filter(Boolean)
97+
.map((number) => Number.parseInt(number, 10))
98+
.filter((number) => Number.isInteger(number));
99+
const owner = context.repo.owner;
100+
const repo = context.repo.repo;
101+
const breakingLabel = 'BREAKING';
102+
103+
const parseMajor = (version) => {
104+
const match = version.match(/^v?(\d+)\.\d+\.\d+(?:[-+].*)?$/);
105+
if (!match) {
106+
throw new Error(`Unable to parse semantic version from "${version}"`);
107+
}
108+
return Number.parseInt(match[1], 10);
109+
};
110+
111+
const previousMajor = parseMajor(previousTag);
112+
const releaseMajor = parseMajor(releaseVersion);
113+
114+
if (commitShas.length === 0) {
115+
core.info(`No commits found between ${previousTag} and HEAD. Skipping BREAKING change validation.`);
116+
return;
117+
}
118+
119+
const prsByNumber = new Map();
120+
const linkedIssuesByPullRequest = new Map();
121+
122+
for (const commitSha of commitShas) {
123+
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
124+
owner,
125+
repo,
126+
commit_sha: commitSha,
127+
});
128+
129+
for (const pullRequest of pullRequests) {
130+
if (pullRequest.merged_at) {
131+
prsByNumber.set(pullRequest.number, pullRequest);
132+
}
133+
}
134+
}
135+
136+
for (const pullRequestNumber of releasePullRequestNumbers) {
137+
if (prsByNumber.has(pullRequestNumber)) {
138+
continue;
139+
}
140+
141+
const { data: pullRequest } = await github.rest.pulls.get({
142+
owner,
143+
repo,
144+
pull_number: pullRequestNumber,
145+
});
146+
147+
if (pullRequest.merged_at) {
148+
prsByNumber.set(pullRequest.number, pullRequest);
149+
}
150+
}
151+
152+
const hasBreakingLabel = (labels) =>
153+
labels.some((label) => label.name?.toUpperCase() === breakingLabel);
154+
155+
const getLinkedIssues = async (pullRequestNumber) => {
156+
if (linkedIssuesByPullRequest.has(pullRequestNumber)) {
157+
return linkedIssuesByPullRequest.get(pullRequestNumber);
158+
}
159+
160+
const query = `
161+
query($owner: String!, $repo: String!, $number: Int!) {
162+
repository(owner: $owner, name: $repo) {
163+
pullRequest(number: $number) {
164+
closingIssuesReferences(first: 50) {
165+
nodes {
166+
number
167+
labels(first: 50) {
168+
nodes {
169+
name
170+
}
171+
}
172+
}
173+
}
174+
}
175+
}
176+
}
177+
`;
178+
179+
const result = await github.graphql(query, {
180+
owner,
181+
repo,
182+
number: pullRequestNumber,
183+
});
184+
const linkedIssues =
185+
result.repository.pullRequest.closingIssuesReferences.nodes.map((issue) => ({
186+
number: issue.number,
187+
labels: issue.labels.nodes,
188+
}));
189+
190+
linkedIssuesByPullRequest.set(pullRequestNumber, linkedIssues);
191+
return linkedIssues;
192+
};
193+
194+
core.info(
195+
`Evaluating ${prsByNumber.size} pull requests since ${previousTag}: ${[...prsByNumber.keys()]
196+
.map((number) => `#${number}`)
197+
.join(', ')}`
198+
);
199+
200+
const breakingPullRequests = [];
201+
202+
for (const pullRequest of prsByNumber.values()) {
203+
const prHasBreakingLabel = hasBreakingLabel(pullRequest.labels);
204+
const linkedIssues = await getLinkedIssues(pullRequest.number);
205+
const breakingIssues = linkedIssues.filter((issue) => hasBreakingLabel(issue.labels));
206+
207+
if (prHasBreakingLabel || breakingIssues.length > 0) {
208+
breakingPullRequests.push({
209+
number: pullRequest.number,
210+
breakingIssues,
211+
});
212+
}
213+
}
214+
215+
if (breakingPullRequests.length === 0) {
216+
core.info(`No BREAKING-labeled pull requests or linked issues found since ${previousTag}.`);
217+
return;
218+
}
219+
220+
const formatBreakingReferences = (pullRequest) => {
221+
const issueSuffix =
222+
pullRequest.breakingIssues.length === 0
223+
? ''
224+
: ` (linked issues: ${pullRequest.breakingIssues.map((issue) => `#${issue.number}`).join(', ')})`;
225+
226+
return `#${pullRequest.number}${issueSuffix}`;
227+
};
228+
229+
if (releaseMajor > previousMajor) {
230+
core.info(
231+
`Found BREAKING-labeled pull requests or linked issues (${breakingPullRequests
232+
.map(formatBreakingReferences)
233+
.join(', ')}), and release version ${releaseVersion} bumps the major version from ${previousMajor} to ${releaseMajor}.`
234+
);
235+
return;
236+
}
237+
238+
core.setFailed(
239+
`Release ${releaseVersion} includes BREAKING-labeled pull requests or linked issues (${breakingPullRequests
240+
.map(formatBreakingReferences)
241+
.join(', ')}) since ${previousTag}, but the major version was not bumped from ${previousMajor}.`
242+
);
37243
38244
- name: Setup Java
39245
uses: actions/setup-java@v5

0 commit comments

Comments
 (0)