-
Notifications
You must be signed in to change notification settings - Fork 3.5k
229 lines (199 loc) · 9.44 KB
/
comment-on-release.yml
File metadata and controls
229 lines (199 loc) · 9.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
name: Comment on PRs in Release
on:
release:
types: [published]
permissions:
pull-requests: write
contents: read
jobs:
comment-on-prs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- name: Get previous release
id: previous_release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
CURRENT_TAG: ${{ github.event.release.tag_name }}
with:
script: |
const currentTag = process.env.CURRENT_TAG;
// Paginate: with two release lines publishing interleaved, the
// previous release on this line can sit far down the list.
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
if (!releases.some(r => r.tag_name === currentTag)) {
console.log('Current release not found in list');
return null;
}
const major = tag => (tag.match(/^v?(\d+)/) || [])[1];
if (major(currentTag) === undefined) {
console.log(`Cannot parse a major version from ${currentTag}; skipping comments`);
return null;
}
// The list is ordered by release creation date, which does not
// reliably reflect tag topology (for example, a release published
// from a long-lived draft keeps its draft creation date). Instead
// of trusting list order, compare every same-major release and
// pick the nearest ancestor of the current tag: the one the
// smallest number of commits behind it. The major check runs
// first so cross-line candidates cost no API calls; per_page=1
// because only status/ahead_by are needed here (the commits are
// fetched in the next step). For the first release of a new major
// line there is no same-line predecessor, and we skip commenting
// rather than compare across the entire new line's history.
let best = null;
for (const candidate of releases) {
if (candidate.tag_name === currentTag || candidate.draft) continue;
if (major(candidate.tag_name) !== major(currentTag)) continue;
let comparison;
try {
({ data: comparison } = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: candidate.tag_name,
head: currentTag,
per_page: 1
}));
} catch (error) {
// Tolerate only candidates whose tag no longer resolves;
// anything else (rate limits, server errors) must fail the
// job rather than silently produce a wrong comparison base.
if (error.status === 404) {
console.log(`Skipping ${candidate.tag_name}: tag does not resolve`);
continue;
}
throw error;
}
// 'identical' covers a release re-cut on the same commit; it
// yields an empty commit range downstream, hence no comments.
if (comparison.status !== 'ahead' && comparison.status !== 'identical') {
console.log(`Skipping ${candidate.tag_name}: not an ancestor of ${currentTag} (status: ${comparison.status})`);
continue;
}
if (best === null || comparison.ahead_by < best.aheadBy) {
best = { tagName: candidate.tag_name, aheadBy: comparison.ahead_by };
}
}
if (best === null) {
console.log(`No previous release found for ${currentTag} on its major line (it may be the first); skipping comments`);
return null;
}
console.log(`Found previous release: ${best.tagName} (${best.aheadBy} commits behind ${currentTag})`);
return best.tagName;
- name: Get merged PRs between releases
id: get_prs
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
CURRENT_TAG: ${{ github.event.release.tag_name }}
PREVIOUS_TAG_JSON: ${{ steps.previous_release.outputs.result }}
with:
script: |
const currentTag = process.env.CURRENT_TAG;
const previousTag = JSON.parse(process.env.PREVIOUS_TAG_JSON);
if (!previousTag) {
console.log('No previous release found, skipping');
return [];
}
console.log(`Finding PRs between ${previousTag} and ${currentTag}`);
// Get commits between previous and current release. A single
// compare response caps the commit list, so paginate — but bound
// the total: a range this large means a mis-selected base, and
// commenting on hundreds of PRs is worse than commenting on none.
const MAX_COMMITS = 250;
const commits = [];
for (let page = 1; ; page++) {
const { data: comparison } = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: previousTag,
head: currentTag,
per_page: 100,
page
});
commits.push(...comparison.commits);
if (commits.length > MAX_COMMITS) {
console.log(`Range ${previousTag}...${currentTag} exceeds ${MAX_COMMITS} commits; skipping comments`);
return [];
}
if (comparison.commits.length < 100) break;
}
console.log(`Found ${commits.length} commits`);
// Get PRs associated with each commit using GitHub API
const prNumbers = new Set();
for (const commit of commits) {
try {
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: commit.sha
});
for (const pr of prs) {
if (pr.merged_at) {
prNumbers.add(pr.number);
console.log(`Found merged PR: #${pr.number}`);
}
}
} catch (error) {
console.log(`Failed to get PRs for commit ${commit.sha}: ${error.message}`);
}
}
console.log(`Found ${prNumbers.size} merged PRs`);
return Array.from(prNumbers);
- name: Comment on PRs
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
PR_NUMBERS_JSON: ${{ steps.get_prs.outputs.result }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_URL: ${{ github.event.release.html_url }}
RELEASE_IS_PRERELEASE: ${{ github.event.release.prerelease }}
with:
script: |
const prNumbers = JSON.parse(process.env.PR_NUMBERS_JSON);
const releaseTag = process.env.RELEASE_TAG;
const releaseUrl = process.env.RELEASE_URL;
// Trust the tag as well as the flag, in case the release manager
// forgets to tick the pre-release checkbox.
const isPrerelease = process.env.RELEASE_IS_PRERELEASE === 'true' || /\d(a|b|rc)\d/.test(releaseTag);
const releaseKind = isPrerelease ? 'pre-release' : 'release';
const comment = `This pull request is included in ${releaseKind} [${releaseTag}](${releaseUrl})`;
let commentedCount = 0;
for (const prNumber of prNumbers) {
try {
// Check if we've already commented on this PR for this
// release. Paginate: comments are returned oldest-first, so
// on a busy PR an earlier bot comment is exactly what would
// fall off a single page.
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100
});
const alreadyCommented = comments.some(c =>
c.user.type === 'Bot' && c.body.includes(`[${releaseTag}]`)
);
if (alreadyCommented) {
console.log(`Skipping PR #${prNumber} - already commented for ${releaseTag}`);
continue;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: comment
});
commentedCount++;
console.log(`Successfully commented on PR #${prNumber}`);
} catch (error) {
console.error(`Failed to comment on PR #${prNumber}:`, error.message);
}
}
console.log(`Commented on ${commentedCount} of ${prNumbers.length} PRs`);