release-please 17.4.1 → 17.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/bin/release-please.js +39 -10
- package/build/src/bootstrapper.d.ts +2 -2
- package/build/src/changelog-notes/default.js +9 -1
- package/build/src/changelog-notes/github.d.ts +2 -2
- package/build/src/changelog-notes.d.ts +1 -0
- package/build/src/commit.d.ts +6 -0
- package/build/src/factories/changelog-notes-factory.d.ts +2 -2
- package/build/src/factories/plugin-factory.d.ts +2 -2
- package/build/src/factories/versioning-strategy-factory.d.ts +2 -2
- package/build/src/factory.d.ts +2 -2
- package/build/src/github-api.d.ts +260 -0
- package/build/src/github-api.js +701 -0
- package/build/src/github.d.ts +21 -171
- package/build/src/github.js +154 -699
- package/build/src/index.d.ts +2 -2
- package/build/src/index.js +1 -1
- package/build/src/local-github.d.ts +271 -0
- package/build/src/local-github.js +776 -0
- package/build/src/manifest.d.ts +7 -5
- package/build/src/manifest.js +28 -26
- package/build/src/plugin.d.ts +3 -3
- package/build/src/plugins/group-priority.d.ts +2 -2
- package/build/src/plugins/linked-versions.d.ts +2 -2
- package/build/src/plugins/maven-workspace.d.ts +2 -2
- package/build/src/plugins/merge.d.ts +2 -2
- package/build/src/plugins/node-workspace.d.ts +2 -2
- package/build/src/plugins/sentence-case.d.ts +2 -2
- package/build/src/plugins/workspace.d.ts +2 -2
- package/build/src/scm.d.ts +80 -0
- package/build/src/scm.js +16 -0
- package/build/src/strategies/base.d.ts +5 -3
- package/build/src/strategies/base.js +2 -0
- package/build/src/util/pull-request-overflow-handler.d.ts +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright 2026 Google LLC
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.sleepInMs = exports.wrapAsync = exports.GitHubApi = exports.MAX_ISSUE_BODY_SIZE = exports.MAX_SLEEP_SECONDS = exports.GH_GRAPHQL_URL = exports.GH_API_URL = void 0;
|
|
17
|
+
const rest_1 = require("@octokit/rest");
|
|
18
|
+
const request_1 = require("@octokit/request");
|
|
19
|
+
const request_error_1 = require("@octokit/request-error");
|
|
20
|
+
const errors_1 = require("./errors");
|
|
21
|
+
const logger_1 = require("./util/logger");
|
|
22
|
+
const graphql_1 = require("@octokit/graphql");
|
|
23
|
+
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
24
|
+
const http_proxy_agent_1 = require("http-proxy-agent");
|
|
25
|
+
exports.GH_API_URL = 'https://api.github.com';
|
|
26
|
+
exports.GH_GRAPHQL_URL = 'https://api.github.com';
|
|
27
|
+
exports.MAX_SLEEP_SECONDS = 20;
|
|
28
|
+
exports.MAX_ISSUE_BODY_SIZE = 65536;
|
|
29
|
+
class GitHubApi {
|
|
30
|
+
constructor(options) {
|
|
31
|
+
var _a;
|
|
32
|
+
this.graphqlRequest = (0, exports.wrapAsync)(async (opts, options) => {
|
|
33
|
+
var _a;
|
|
34
|
+
let maxRetries = (_a = options === null || options === void 0 ? void 0 : options.maxRetries) !== null && _a !== void 0 ? _a : 5;
|
|
35
|
+
let seconds = 1;
|
|
36
|
+
while (maxRetries >= 0) {
|
|
37
|
+
try {
|
|
38
|
+
const response = await this.graphql(opts);
|
|
39
|
+
if (response) {
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
this.logger.trace('no GraphQL response, retrying');
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (err.status !== 502) {
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
if (maxRetries === 0) {
|
|
49
|
+
this.logger.warn('ran out of retries and response is required');
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
this.logger.info(`received 502 error, ${maxRetries} attempts remaining`);
|
|
53
|
+
}
|
|
54
|
+
maxRetries -= 1;
|
|
55
|
+
if (maxRetries >= 0) {
|
|
56
|
+
this.logger.trace(`sleeping ${seconds} seconds`);
|
|
57
|
+
await (0, exports.sleepInMs)(1000 * seconds);
|
|
58
|
+
seconds = Math.min(seconds * 2, exports.MAX_SLEEP_SECONDS);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.logger.trace('ran out of retries');
|
|
62
|
+
return undefined;
|
|
63
|
+
});
|
|
64
|
+
this.createPullRequest = (0, exports.wrapAsync)(async (pullRequest, targetBranch, options) => {
|
|
65
|
+
const pullResponseData = (await this.octokit.pulls.create({
|
|
66
|
+
owner: this.repository.owner,
|
|
67
|
+
repo: this.repository.repo,
|
|
68
|
+
title: pullRequest.title,
|
|
69
|
+
head: `${this.repository.owner}:${pullRequest.headBranchName}`,
|
|
70
|
+
base: targetBranch,
|
|
71
|
+
body: pullRequest.body,
|
|
72
|
+
maintainer_can_modify: true,
|
|
73
|
+
draft: !!(options === null || options === void 0 ? void 0 : options.draft),
|
|
74
|
+
})).data;
|
|
75
|
+
this.logger.info(`Successfully opened pull request available at url: ${pullResponseData.html_url}.`);
|
|
76
|
+
return await this.getPullRequest(pullResponseData.number);
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Fetch a pull request given the pull number
|
|
80
|
+
* @param {number} number The pull request number
|
|
81
|
+
* @returns {PullRequest}
|
|
82
|
+
*/
|
|
83
|
+
this.getPullRequest = (0, exports.wrapAsync)(async (number) => {
|
|
84
|
+
const response = await this.octokit.pulls.get({
|
|
85
|
+
owner: this.repository.owner,
|
|
86
|
+
repo: this.repository.repo,
|
|
87
|
+
pull_number: number,
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
headBranchName: response.data.head.ref,
|
|
91
|
+
baseBranchName: response.data.base.ref,
|
|
92
|
+
number: response.data.number,
|
|
93
|
+
title: response.data.title,
|
|
94
|
+
body: response.data.body || '',
|
|
95
|
+
files: [],
|
|
96
|
+
labels: response.data.labels
|
|
97
|
+
.map((label) => label.name)
|
|
98
|
+
.filter((name) => !!name),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
this.updatePullRequest = (0, exports.wrapAsync)(async (number, title, body) => {
|
|
102
|
+
const response = await this.octokit.pulls.update({
|
|
103
|
+
owner: this.repository.owner,
|
|
104
|
+
repo: this.repository.repo,
|
|
105
|
+
pull_number: number,
|
|
106
|
+
title,
|
|
107
|
+
body,
|
|
108
|
+
state: 'open',
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
headBranchName: response.data.head.ref,
|
|
112
|
+
baseBranchName: response.data.base.ref,
|
|
113
|
+
number: response.data.number,
|
|
114
|
+
title: response.data.title,
|
|
115
|
+
body: response.data.body || '',
|
|
116
|
+
files: [],
|
|
117
|
+
labels: response.data.labels
|
|
118
|
+
.map((label) => label.name)
|
|
119
|
+
.filter((name) => !!name),
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* Create a GitHub release
|
|
124
|
+
*
|
|
125
|
+
* @param {Release} release Release parameters
|
|
126
|
+
* @param {ScmReleaseOptions} options Release option parameters
|
|
127
|
+
* @throws {DuplicateReleaseError} if the release tag already exists
|
|
128
|
+
* @throws {GitHubAPIError} on other API errors
|
|
129
|
+
*/
|
|
130
|
+
this.createRelease = (0, exports.wrapAsync)(async (release, options = {}) => {
|
|
131
|
+
if (options.forceTag) {
|
|
132
|
+
try {
|
|
133
|
+
await this.octokit.git.createRef({
|
|
134
|
+
owner: this.repository.owner,
|
|
135
|
+
repo: this.repository.repo,
|
|
136
|
+
ref: `refs/tags/${release.tag.toString()}`,
|
|
137
|
+
sha: release.sha,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
// ignore if tag already exists
|
|
142
|
+
if (err.status === 422) {
|
|
143
|
+
this.logger.debug(`Tag ${release.tag.toString()} already exists, skipping tag creation`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const resp = await this.octokit.repos.createRelease({
|
|
151
|
+
name: release.name,
|
|
152
|
+
owner: this.repository.owner,
|
|
153
|
+
repo: this.repository.repo,
|
|
154
|
+
tag_name: release.tag.toString(),
|
|
155
|
+
body: release.notes,
|
|
156
|
+
draft: !!options.draft,
|
|
157
|
+
prerelease: !!options.prerelease,
|
|
158
|
+
target_commitish: release.sha,
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
id: resp.data.id,
|
|
162
|
+
name: resp.data.name || undefined,
|
|
163
|
+
tagName: resp.data.tag_name,
|
|
164
|
+
sha: resp.data.target_commitish,
|
|
165
|
+
notes: resp.data.body_text ||
|
|
166
|
+
resp.data.body ||
|
|
167
|
+
resp.data.body_html ||
|
|
168
|
+
undefined,
|
|
169
|
+
url: resp.data.html_url,
|
|
170
|
+
draft: resp.data.draft,
|
|
171
|
+
uploadUrl: resp.data.upload_url,
|
|
172
|
+
};
|
|
173
|
+
}, e => {
|
|
174
|
+
if (e instanceof request_error_1.RequestError) {
|
|
175
|
+
if (e.status === 422 &&
|
|
176
|
+
errors_1.GitHubAPIError.parseErrors(e).some(error => {
|
|
177
|
+
return error.code === 'already_exists';
|
|
178
|
+
})) {
|
|
179
|
+
throw new errors_1.DuplicateReleaseError(e, 'tagName');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
/**
|
|
184
|
+
* Makes a comment on a issue/pull request.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} comment - The body of the comment to post.
|
|
187
|
+
* @param {number} number - The issue or pull request number.
|
|
188
|
+
* @throws {GitHubAPIError} on an API error
|
|
189
|
+
*/
|
|
190
|
+
this.commentOnIssue = (0, exports.wrapAsync)(async (comment, number) => {
|
|
191
|
+
this.logger.debug(`adding comment to https://github.com/${this.repository.owner}/${this.repository.repo}/issues/${number}`);
|
|
192
|
+
const resp = await this.octokit.issues.createComment({
|
|
193
|
+
owner: this.repository.owner,
|
|
194
|
+
repo: this.repository.repo,
|
|
195
|
+
issue_number: number,
|
|
196
|
+
body: comment,
|
|
197
|
+
});
|
|
198
|
+
return resp.data.html_url;
|
|
199
|
+
});
|
|
200
|
+
/**
|
|
201
|
+
* Removes labels from an issue/pull request.
|
|
202
|
+
*
|
|
203
|
+
* @param {string[]} labels The labels to remove.
|
|
204
|
+
* @param {number} number The issue/pull request number.
|
|
205
|
+
*/
|
|
206
|
+
this.removeIssueLabels = (0, exports.wrapAsync)(async (labels, number) => {
|
|
207
|
+
if (labels.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.logger.debug(`removing labels: ${labels} from issue/pull ${number}`);
|
|
211
|
+
await Promise.all(labels.map(label => this.octokit.issues.removeLabel({
|
|
212
|
+
owner: this.repository.owner,
|
|
213
|
+
repo: this.repository.repo,
|
|
214
|
+
issue_number: number,
|
|
215
|
+
name: label,
|
|
216
|
+
})));
|
|
217
|
+
});
|
|
218
|
+
/**
|
|
219
|
+
* Adds label to an issue/pull request.
|
|
220
|
+
*
|
|
221
|
+
* @param {string[]} labels The labels to add.
|
|
222
|
+
* @param {number} number The issue/pull request number.
|
|
223
|
+
*/
|
|
224
|
+
this.addIssueLabels = (0, exports.wrapAsync)(async (labels, number) => {
|
|
225
|
+
if (labels.length === 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.logger.debug(`adding labels: ${labels} from issue/pull ${number}`);
|
|
229
|
+
await this.octokit.issues.addLabels({
|
|
230
|
+
owner: this.repository.owner,
|
|
231
|
+
repo: this.repository.repo,
|
|
232
|
+
issue_number: number,
|
|
233
|
+
labels,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
this.repository = options.repository;
|
|
237
|
+
this.octokitAPIs = options.octokitAPIs;
|
|
238
|
+
this.octokit = options.octokitAPIs.octokit;
|
|
239
|
+
this.graphql = options.octokitAPIs.graphql;
|
|
240
|
+
this.logger = (_a = options.logger) !== null && _a !== void 0 ? _a : logger_1.logger;
|
|
241
|
+
}
|
|
242
|
+
static createDefaultAgent(baseUrl, defaultProxy) {
|
|
243
|
+
if (!defaultProxy) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
const { host, port } = defaultProxy;
|
|
247
|
+
if (new URL(baseUrl).protocol.replace(':', '') === 'http') {
|
|
248
|
+
return new http_proxy_agent_1.HttpProxyAgent(`http://${host}:${port}`);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
return new https_proxy_agent_1.HttpsProxyAgent(`https://${host}:${port}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
static async create(options) {
|
|
255
|
+
var _a, _b, _c, _d;
|
|
256
|
+
const apiUrl = (_a = options.apiUrl) !== null && _a !== void 0 ? _a : exports.GH_API_URL;
|
|
257
|
+
const graphqlUrl = (_b = options.graphqlUrl) !== null && _b !== void 0 ? _b : exports.GH_GRAPHQL_URL;
|
|
258
|
+
const releasePleaseVersion = require('../../package.json').version;
|
|
259
|
+
const apis = (_c = options.octokitAPIs) !== null && _c !== void 0 ? _c : {
|
|
260
|
+
octokit: new rest_1.Octokit({
|
|
261
|
+
baseUrl: apiUrl,
|
|
262
|
+
auth: options.token,
|
|
263
|
+
request: {
|
|
264
|
+
agent: this.createDefaultAgent(apiUrl, options.proxy),
|
|
265
|
+
fetch: options.fetch,
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
request: request_1.request.defaults({
|
|
269
|
+
baseUrl: apiUrl,
|
|
270
|
+
headers: {
|
|
271
|
+
'user-agent': `release-please/${releasePleaseVersion}`,
|
|
272
|
+
Authorization: `token ${options.token}`,
|
|
273
|
+
},
|
|
274
|
+
fetch: options.fetch,
|
|
275
|
+
}),
|
|
276
|
+
graphql: graphql_1.graphql.defaults({
|
|
277
|
+
baseUrl: graphqlUrl,
|
|
278
|
+
request: {
|
|
279
|
+
agent: this.createDefaultAgent(graphqlUrl, options.proxy),
|
|
280
|
+
fetch: options.fetch,
|
|
281
|
+
},
|
|
282
|
+
headers: {
|
|
283
|
+
'user-agent': `release-please/${releasePleaseVersion}`,
|
|
284
|
+
Authorization: `token ${options.token}`,
|
|
285
|
+
'content-type': 'application/vnd.github.v3+json',
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
};
|
|
289
|
+
const opts = {
|
|
290
|
+
repository: {
|
|
291
|
+
owner: options.owner,
|
|
292
|
+
repo: options.repo,
|
|
293
|
+
defaultBranch: (_d = options.defaultBranch) !== null && _d !== void 0 ? _d : (await GitHubApi.defaultBranch(options.owner, options.repo, apis.octokit)),
|
|
294
|
+
},
|
|
295
|
+
octokitAPIs: apis,
|
|
296
|
+
logger: options.logger,
|
|
297
|
+
};
|
|
298
|
+
return new GitHubApi(opts);
|
|
299
|
+
}
|
|
300
|
+
static async defaultBranch(owner, repo, octokit) {
|
|
301
|
+
const { data } = await octokit.repos.get({
|
|
302
|
+
repo,
|
|
303
|
+
owner,
|
|
304
|
+
});
|
|
305
|
+
return data.default_branch;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Iterate through merged pull requests with a max number of results scanned.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} targetBranch Target branch of commit.
|
|
311
|
+
* @param {string} status The status of the pull request. Defaults to 'MERGED'.
|
|
312
|
+
* @param {number} maxResults Limit the number of results searched. Defaults to
|
|
313
|
+
* unlimited.
|
|
314
|
+
* @param {boolean} includeFiles Whether to fetch the list of files included in
|
|
315
|
+
* the pull request. Defaults to `true`.
|
|
316
|
+
* @yields {PullRequest}
|
|
317
|
+
* @throws {GitHubAPIError} on an API error
|
|
318
|
+
*/
|
|
319
|
+
async *pullRequestIterator(targetBranch, status = 'MERGED', maxResults = Number.MAX_SAFE_INTEGER, includeFiles = true) {
|
|
320
|
+
const generator = includeFiles
|
|
321
|
+
? this.pullRequestIteratorWithFiles(targetBranch, status, maxResults)
|
|
322
|
+
: this.pullRequestIteratorWithoutFiles(targetBranch, status, maxResults);
|
|
323
|
+
for await (const pullRequest of generator) {
|
|
324
|
+
yield pullRequest;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Helper implementation of pullRequestIterator that includes files via
|
|
329
|
+
* the graphQL API.
|
|
330
|
+
*
|
|
331
|
+
* @param {string} targetBranch The base branch of the pull request
|
|
332
|
+
* @param {string} status The status of the pull request
|
|
333
|
+
* @param {number} maxResults Limit the number of results searched
|
|
334
|
+
*/
|
|
335
|
+
async *pullRequestIteratorWithFiles(targetBranch, status = 'MERGED', maxResults = Number.MAX_SAFE_INTEGER) {
|
|
336
|
+
let cursor = undefined;
|
|
337
|
+
let results = 0;
|
|
338
|
+
while (results < maxResults) {
|
|
339
|
+
const response = await this.pullRequestsGraphQL(targetBranch, status, cursor);
|
|
340
|
+
// no response usually means we ran out of results
|
|
341
|
+
if (!response) {
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
for (let i = 0; i < response.data.length; i++) {
|
|
345
|
+
results += 1;
|
|
346
|
+
yield response.data[i];
|
|
347
|
+
}
|
|
348
|
+
if (!response.pageInfo.hasNextPage) {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
cursor = response.pageInfo.endCursor;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Helper implementation of pullRequestIterator that excludes files
|
|
356
|
+
* via the REST API.
|
|
357
|
+
*
|
|
358
|
+
* @param {string} targetBranch The base branch of the pull request
|
|
359
|
+
* @param {string} status The status of the pull request
|
|
360
|
+
* @param {number} maxResults Limit the number of results searched
|
|
361
|
+
*/
|
|
362
|
+
async *pullRequestIteratorWithoutFiles(targetBranch, status = 'MERGED', maxResults = Number.MAX_SAFE_INTEGER) {
|
|
363
|
+
const statusMap = {
|
|
364
|
+
OPEN: 'open',
|
|
365
|
+
CLOSED: 'closed',
|
|
366
|
+
MERGED: 'closed',
|
|
367
|
+
};
|
|
368
|
+
let results = 0;
|
|
369
|
+
for await (const { data: pulls } of this.octokit.paginate.iterator('GET /repos/{owner}/{repo}/pulls', {
|
|
370
|
+
state: statusMap[status],
|
|
371
|
+
owner: this.repository.owner,
|
|
372
|
+
repo: this.repository.repo,
|
|
373
|
+
base: targetBranch,
|
|
374
|
+
sort: 'updated',
|
|
375
|
+
direction: 'desc',
|
|
376
|
+
})) {
|
|
377
|
+
for (const pull of pulls) {
|
|
378
|
+
// The REST API does not have an option for "merged"
|
|
379
|
+
// pull requests - they are closed with a `merged_at` timestamp
|
|
380
|
+
if (status !== 'MERGED' || pull.merged_at) {
|
|
381
|
+
results += 1;
|
|
382
|
+
yield {
|
|
383
|
+
headBranchName: pull.head.ref,
|
|
384
|
+
baseBranchName: pull.base.ref,
|
|
385
|
+
number: pull.number,
|
|
386
|
+
title: pull.title,
|
|
387
|
+
body: pull.body || '',
|
|
388
|
+
labels: pull.labels.map((label) => label.name),
|
|
389
|
+
files: [],
|
|
390
|
+
sha: pull.merge_commit_sha || undefined,
|
|
391
|
+
};
|
|
392
|
+
if (results >= maxResults) {
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (results >= maxResults) {
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Return a list of merged pull requests. The list is not guaranteed to be sorted
|
|
404
|
+
* by merged_at, but is generally most recent first.
|
|
405
|
+
*
|
|
406
|
+
* @param {string} targetBranch - Base branch of the pull request. Defaults to
|
|
407
|
+
* the configured default branch.
|
|
408
|
+
* @param {number} page - Page of results. Defaults to 1.
|
|
409
|
+
* @param {number} perPage - Number of results per page. Defaults to 100.
|
|
410
|
+
* @returns {PullRequestHistory | null} - List of merged pull requests
|
|
411
|
+
* @throws {GitHubAPIError} on an API error
|
|
412
|
+
*/
|
|
413
|
+
async pullRequestsGraphQL(targetBranch, states = 'MERGED', cursor) {
|
|
414
|
+
var _a;
|
|
415
|
+
this.logger.debug(`Fetching ${states} pull requests on branch ${targetBranch} with cursor ${cursor}`);
|
|
416
|
+
const response = await this.graphqlRequest({
|
|
417
|
+
query: `query mergedPullRequests($owner: String!, $repo: String!, $num: Int!, $maxFilesChanged: Int, $targetBranch: String!, $states: [PullRequestState!], $cursor: String) {
|
|
418
|
+
repository(owner: $owner, name: $repo) {
|
|
419
|
+
pullRequests(first: $num, after: $cursor, baseRefName: $targetBranch, states: $states, orderBy: {field: CREATED_AT, direction: DESC}) {
|
|
420
|
+
nodes {
|
|
421
|
+
number
|
|
422
|
+
title
|
|
423
|
+
baseRefName
|
|
424
|
+
headRefName
|
|
425
|
+
labels(first: 10) {
|
|
426
|
+
nodes {
|
|
427
|
+
name
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
body
|
|
431
|
+
mergeCommit {
|
|
432
|
+
oid
|
|
433
|
+
}
|
|
434
|
+
files(first: $maxFilesChanged) {
|
|
435
|
+
nodes {
|
|
436
|
+
path
|
|
437
|
+
}
|
|
438
|
+
pageInfo {
|
|
439
|
+
endCursor
|
|
440
|
+
hasNextPage
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
pageInfo {
|
|
445
|
+
endCursor
|
|
446
|
+
hasNextPage
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}`,
|
|
451
|
+
cursor,
|
|
452
|
+
owner: this.repository.owner,
|
|
453
|
+
repo: this.repository.repo,
|
|
454
|
+
num: 25,
|
|
455
|
+
targetBranch,
|
|
456
|
+
states,
|
|
457
|
+
maxFilesChanged: 64,
|
|
458
|
+
});
|
|
459
|
+
if (!((_a = response === null || response === void 0 ? void 0 : response.repository) === null || _a === void 0 ? void 0 : _a.pullRequests)) {
|
|
460
|
+
this.logger.warn(`Could not find merged pull requests for branch ${targetBranch} - it likely does not exist.`);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
const pullRequests = (response.repository.pullRequests.nodes ||
|
|
464
|
+
[]);
|
|
465
|
+
return {
|
|
466
|
+
pageInfo: response.repository.pullRequests.pageInfo,
|
|
467
|
+
data: pullRequests.map(pullRequest => {
|
|
468
|
+
var _a, _b, _c;
|
|
469
|
+
return {
|
|
470
|
+
sha: (_a = pullRequest.mergeCommit) === null || _a === void 0 ? void 0 : _a.oid,
|
|
471
|
+
number: pullRequest.number,
|
|
472
|
+
baseBranchName: pullRequest.baseRefName,
|
|
473
|
+
headBranchName: pullRequest.headRefName,
|
|
474
|
+
labels: (((_b = pullRequest.labels) === null || _b === void 0 ? void 0 : _b.nodes) || []).map(l => l.name),
|
|
475
|
+
title: pullRequest.title,
|
|
476
|
+
body: pullRequest.body + '',
|
|
477
|
+
files: (((_c = pullRequest.files) === null || _c === void 0 ? void 0 : _c.nodes) || []).map(node => node.path),
|
|
478
|
+
};
|
|
479
|
+
}),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Iterate through releases with a max number of results scanned.
|
|
484
|
+
*
|
|
485
|
+
* @param {ReleaseIteratorOptions} options Query options
|
|
486
|
+
* @param {number} options.maxResults Limit the number of results scanned.
|
|
487
|
+
* Defaults to unlimited.
|
|
488
|
+
* @yields {ScmRelease}
|
|
489
|
+
* @throws {GitHubAPIError} on an API error
|
|
490
|
+
*/
|
|
491
|
+
async *releaseIterator(options = {}) {
|
|
492
|
+
var _a;
|
|
493
|
+
const maxResults = (_a = options.maxResults) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER;
|
|
494
|
+
let results = 0;
|
|
495
|
+
let cursor = undefined;
|
|
496
|
+
while (true) {
|
|
497
|
+
const response = await this.releaseGraphQL(cursor);
|
|
498
|
+
if (!response) {
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
for (let i = 0; i < response.data.length; i++) {
|
|
502
|
+
if ((results += 1) > maxResults) {
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
yield response.data[i];
|
|
506
|
+
}
|
|
507
|
+
if (results > maxResults || !response.pageInfo.hasNextPage) {
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
cursor = response.pageInfo.endCursor;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async releaseGraphQL(cursor) {
|
|
514
|
+
var _a, _b, _c;
|
|
515
|
+
this.logger.debug(`Fetching releases with cursor ${cursor}`);
|
|
516
|
+
const response = await this.graphqlRequest({
|
|
517
|
+
query: `query releases($owner: String!, $repo: String!, $num: Int!, $cursor: String) {
|
|
518
|
+
repository(owner: $owner, name: $repo) {
|
|
519
|
+
releases(first: $num, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) {
|
|
520
|
+
nodes {
|
|
521
|
+
name
|
|
522
|
+
tag {
|
|
523
|
+
name
|
|
524
|
+
}
|
|
525
|
+
tagCommit {
|
|
526
|
+
oid
|
|
527
|
+
}
|
|
528
|
+
url
|
|
529
|
+
description
|
|
530
|
+
isDraft
|
|
531
|
+
}
|
|
532
|
+
pageInfo {
|
|
533
|
+
endCursor
|
|
534
|
+
hasNextPage
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}`,
|
|
539
|
+
cursor,
|
|
540
|
+
owner: this.repository.owner,
|
|
541
|
+
repo: this.repository.repo,
|
|
542
|
+
num: 25,
|
|
543
|
+
});
|
|
544
|
+
if (!((_c = (_b = (_a = response === null || response === void 0 ? void 0 : response.repository) === null || _a === void 0 ? void 0 : _a.releases) === null || _b === void 0 ? void 0 : _b.nodes) === null || _c === void 0 ? void 0 : _c.length)) {
|
|
545
|
+
this.logger.warn('Could not find releases.');
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
const releases = response.repository.releases.nodes;
|
|
549
|
+
return {
|
|
550
|
+
pageInfo: response.repository.releases.pageInfo,
|
|
551
|
+
data: releases
|
|
552
|
+
.filter(release => !!release.tagCommit)
|
|
553
|
+
.map(release => {
|
|
554
|
+
if (!release.tag || !release.tagCommit) {
|
|
555
|
+
this.logger.debug(release);
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
name: release.name || undefined,
|
|
559
|
+
tagName: release.tag ? release.tag.name : 'unknown',
|
|
560
|
+
sha: release.tagCommit.oid,
|
|
561
|
+
notes: release.description,
|
|
562
|
+
url: release.url,
|
|
563
|
+
draft: release.isDraft,
|
|
564
|
+
};
|
|
565
|
+
}),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Generate release notes from GitHub at tag
|
|
570
|
+
* @param {string} tagName Name of new release tag
|
|
571
|
+
* @param {string} targetCommitish Target commitish for new tag
|
|
572
|
+
* @param {string} previousTag Optional. Name of previous tag to analyze commits since
|
|
573
|
+
*/
|
|
574
|
+
async generateReleaseNotes(tagName, targetCommitish, previousTag) {
|
|
575
|
+
const resp = await this.octokit.repos.generateReleaseNotes({
|
|
576
|
+
owner: this.repository.owner,
|
|
577
|
+
repo: this.repository.repo,
|
|
578
|
+
tag_name: tagName,
|
|
579
|
+
previous_tag_name: previousTag,
|
|
580
|
+
target_commitish: targetCommitish,
|
|
581
|
+
});
|
|
582
|
+
return resp.data.body;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Create a single file on a new branch based on an existing
|
|
586
|
+
* branch. This will force-push to that branch.
|
|
587
|
+
* @param {string} filename Filename with path in the repository
|
|
588
|
+
* @param {string} contents Contents of the file
|
|
589
|
+
* @param {string} newBranchName Name of the new branch
|
|
590
|
+
* @param {string} baseBranchName Name of the base branch (where
|
|
591
|
+
* new branch is forked from)
|
|
592
|
+
* @returns {string} HTML URL of the new file
|
|
593
|
+
*/
|
|
594
|
+
async createFileOnNewBranch(filename, contents, newBranchName, baseBranchName) {
|
|
595
|
+
// create or update new branch to match base branch
|
|
596
|
+
await this.forkBranch(newBranchName, baseBranchName);
|
|
597
|
+
// use the single file upload API
|
|
598
|
+
const { data: { content }, } = await this.octokit.repos.createOrUpdateFileContents({
|
|
599
|
+
owner: this.repository.owner,
|
|
600
|
+
repo: this.repository.repo,
|
|
601
|
+
path: filename,
|
|
602
|
+
// contents need to be base64 encoded
|
|
603
|
+
content: Buffer.from(contents, 'binary').toString('base64'),
|
|
604
|
+
message: 'Saving release notes',
|
|
605
|
+
branch: newBranchName,
|
|
606
|
+
});
|
|
607
|
+
return (content === null || content === void 0 ? void 0 : content.html_url) || '';
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Fork a branch from a base branch.
|
|
611
|
+
*/
|
|
612
|
+
async forkBranch(targetBranchName, baseBranchName) {
|
|
613
|
+
const baseBranchSha = await this.getBranchSha(baseBranchName);
|
|
614
|
+
if (!baseBranchSha) {
|
|
615
|
+
throw new errors_1.ConfigurationError(`Unable to find base branch: ${baseBranchName}`, 'core', `${this.repository.owner}/${this.repository.repo}`);
|
|
616
|
+
}
|
|
617
|
+
if (await this.getBranchSha(targetBranchName)) {
|
|
618
|
+
const branchSha = await this.updateBranchSha(targetBranchName, baseBranchSha);
|
|
619
|
+
this.logger.debug(`Updated ${targetBranchName} to match ${baseBranchName} at ${branchSha}`);
|
|
620
|
+
return branchSha;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
const branchSha = await this.createNewBranch(targetBranchName, baseBranchSha);
|
|
624
|
+
this.logger.debug(`Created ${targetBranchName} from ${baseBranchName} at ${branchSha}`);
|
|
625
|
+
return branchSha;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Helper to fetch the SHA of a branch
|
|
630
|
+
*/
|
|
631
|
+
async getBranchSha(branchName) {
|
|
632
|
+
this.logger.debug(`Looking up SHA for branch: ${branchName}`);
|
|
633
|
+
try {
|
|
634
|
+
const { data: { object: { sha }, }, } = await this.octokit.git.getRef({
|
|
635
|
+
owner: this.repository.owner,
|
|
636
|
+
repo: this.repository.repo,
|
|
637
|
+
ref: `heads/${branchName}`,
|
|
638
|
+
});
|
|
639
|
+
this.logger.debug(`SHA for branch: ${sha}`);
|
|
640
|
+
return sha;
|
|
641
|
+
}
|
|
642
|
+
catch (e) {
|
|
643
|
+
if (e instanceof request_error_1.RequestError && e.status === 404) {
|
|
644
|
+
this.logger.debug(`Branch: ${branchName} does not exist`);
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
throw e;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Helper to create a new branch from a given SHA.
|
|
652
|
+
*/
|
|
653
|
+
async createNewBranch(branchName, branchSha) {
|
|
654
|
+
this.logger.debug(`Creating new branch: ${branchName} at ${branchSha}`);
|
|
655
|
+
const { data: { object: { sha }, }, } = await this.octokit.git.createRef({
|
|
656
|
+
owner: this.repository.owner,
|
|
657
|
+
repo: this.repository.repo,
|
|
658
|
+
ref: `refs/heads/${branchName}`,
|
|
659
|
+
sha: branchSha,
|
|
660
|
+
});
|
|
661
|
+
this.logger.debug(`New branch: ${branchName} at ${sha}`);
|
|
662
|
+
return sha;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Helper to update branch SHA.
|
|
666
|
+
*/
|
|
667
|
+
async updateBranchSha(branchName, branchSha) {
|
|
668
|
+
this.logger.debug(`Updating branch ${branchName} to ${branchSha}`);
|
|
669
|
+
const { data: { object: { sha }, }, } = await this.octokit.git.updateRef({
|
|
670
|
+
owner: this.repository.owner,
|
|
671
|
+
repo: this.repository.repo,
|
|
672
|
+
ref: `heads/${branchName}`,
|
|
673
|
+
sha: branchSha,
|
|
674
|
+
force: true,
|
|
675
|
+
});
|
|
676
|
+
this.logger.debug(`Updated branch: ${branchName} to ${sha}`);
|
|
677
|
+
return sha;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
exports.GitHubApi = GitHubApi;
|
|
681
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
682
|
+
const wrapAsync = (fn, errorHandler) => {
|
|
683
|
+
return async (...args) => {
|
|
684
|
+
try {
|
|
685
|
+
return await fn(...args);
|
|
686
|
+
}
|
|
687
|
+
catch (e) {
|
|
688
|
+
if (errorHandler) {
|
|
689
|
+
errorHandler(e);
|
|
690
|
+
}
|
|
691
|
+
if (e instanceof request_error_1.RequestError) {
|
|
692
|
+
throw new errors_1.GitHubAPIError(e);
|
|
693
|
+
}
|
|
694
|
+
throw e;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
};
|
|
698
|
+
exports.wrapAsync = wrapAsync;
|
|
699
|
+
const sleepInMs = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
700
|
+
exports.sleepInMs = sleepInMs;
|
|
701
|
+
//# sourceMappingURL=github-api.js.map
|