release-please 17.4.1 → 17.5.1

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.
Files changed (34) hide show
  1. package/build/src/bin/release-please.js +39 -10
  2. package/build/src/bootstrapper.d.ts +2 -2
  3. package/build/src/changelog-notes/default.js +9 -1
  4. package/build/src/changelog-notes/github.d.ts +2 -2
  5. package/build/src/changelog-notes.d.ts +1 -0
  6. package/build/src/commit.d.ts +6 -0
  7. package/build/src/factories/changelog-notes-factory.d.ts +2 -2
  8. package/build/src/factories/plugin-factory.d.ts +2 -2
  9. package/build/src/factories/versioning-strategy-factory.d.ts +2 -2
  10. package/build/src/factory.d.ts +2 -2
  11. package/build/src/github-api.d.ts +260 -0
  12. package/build/src/github-api.js +701 -0
  13. package/build/src/github.d.ts +21 -171
  14. package/build/src/github.js +154 -699
  15. package/build/src/index.d.ts +2 -2
  16. package/build/src/index.js +1 -1
  17. package/build/src/local-github.d.ts +271 -0
  18. package/build/src/local-github.js +778 -0
  19. package/build/src/manifest.d.ts +7 -5
  20. package/build/src/manifest.js +28 -26
  21. package/build/src/plugin.d.ts +3 -3
  22. package/build/src/plugins/group-priority.d.ts +2 -2
  23. package/build/src/plugins/linked-versions.d.ts +2 -2
  24. package/build/src/plugins/maven-workspace.d.ts +2 -2
  25. package/build/src/plugins/merge.d.ts +2 -2
  26. package/build/src/plugins/node-workspace.d.ts +2 -2
  27. package/build/src/plugins/sentence-case.d.ts +2 -2
  28. package/build/src/plugins/workspace.d.ts +2 -2
  29. package/build/src/scm.d.ts +80 -0
  30. package/build/src/scm.js +16 -0
  31. package/build/src/strategies/base.d.ts +5 -3
  32. package/build/src/strategies/base.js +2 -0
  33. package/build/src/util/pull-request-overflow-handler.d.ts +2 -2
  34. package/package.json +2 -2
@@ -0,0 +1,778 @@
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.LocalGitHub = void 0;
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+ const os = require("os");
20
+ const child_process = require("child_process");
21
+ const util = require("util");
22
+ const readline = require("readline");
23
+ const execFile = util.promisify(child_process.execFile);
24
+ const mkdtemp = fs.promises.mkdtemp;
25
+ const errors_1 = require("./errors");
26
+ const manifest_1 = require("./manifest");
27
+ const git_file_utils_1 = require("@google-automations/git-file-utils");
28
+ const composite_1 = require("./updaters/composite");
29
+ const github_api_1 = require("./github-api");
30
+ const logger_1 = require("./util/logger");
31
+ /**
32
+ * LocalGitHub implements the Scm interface using a local git clone
33
+ * where possible, and falling back to the GitHub API for other operations.
34
+ */
35
+ class LocalGitHub {
36
+ constructor(repository, gitHubApi, cloneDir, options) {
37
+ var _a;
38
+ this.repository = repository;
39
+ this.gitHubApi = gitHubApi;
40
+ this.cloneDir = cloneDir;
41
+ this.logger = (_a = options === null || options === void 0 ? void 0 : options.logger) !== null && _a !== void 0 ? _a : logger_1.logger;
42
+ }
43
+ static async create(options) {
44
+ var _a;
45
+ const gitHubApi = await github_api_1.GitHubApi.create(options);
46
+ const logger = (_a = options.logger) !== null && _a !== void 0 ? _a : logger_1.logger;
47
+ let repoDir;
48
+ if (options.localRepoPath) {
49
+ repoDir = options.localRepoPath;
50
+ let isGitRepo = false;
51
+ try {
52
+ await execFile('git', ['rev-parse', '--is-inside-work-tree'], {
53
+ cwd: repoDir,
54
+ });
55
+ isGitRepo = true;
56
+ }
57
+ catch (err) {
58
+ isGitRepo = false;
59
+ }
60
+ if (!isGitRepo) {
61
+ logger.info(`Path ${repoDir} is not a git clone. Cloning repository...`);
62
+ const url = `https://github.com/${gitHubApi.repository.owner}/${gitHubApi.repository.repo}.git`;
63
+ const args = ['clone', '--', url, repoDir];
64
+ if (options.cloneDepth) {
65
+ args.splice(1, 0, '--depth', options.cloneDepth.toString());
66
+ }
67
+ logger.debug(`Executing: git ${args.join(' ')}`);
68
+ await execFile('git', args);
69
+ }
70
+ else {
71
+ logger.info(`Using existing local repository at ${repoDir}...`);
72
+ }
73
+ const branch = gitHubApi.repository.defaultBranch;
74
+ logger.debug('Executing: git fetch origin');
75
+ await execFile('git', ['fetch', 'origin'], { cwd: repoDir });
76
+ logger.debug(`Executing: git checkout ${branch}`);
77
+ await execFile('git', ['checkout', branch], { cwd: repoDir });
78
+ logger.debug(`Executing: git reset --hard origin/${branch}`);
79
+ await execFile('git', ['reset', '--hard', `origin/${branch}`], {
80
+ cwd: repoDir,
81
+ });
82
+ }
83
+ else {
84
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'release-please-'));
85
+ logger.info(`Cloning repository to ${tempDir}...`);
86
+ const url = `https://github.com/${gitHubApi.repository.owner}/${gitHubApi.repository.repo}.git`;
87
+ const args = ['clone', '--', url, tempDir];
88
+ if (options.cloneDepth) {
89
+ args.splice(1, 0, '--depth', options.cloneDepth.toString());
90
+ }
91
+ logger.debug(`Executing: git ${args.join(' ')}`);
92
+ await execFile('git', args);
93
+ repoDir = tempDir;
94
+ }
95
+ return new LocalGitHub(gitHubApi.repository, gitHubApi, repoDir, {
96
+ logger: options.logger,
97
+ });
98
+ }
99
+ /**
100
+ * Fetch the contents of a file from the configured branch
101
+ *
102
+ * @param {string} path The path to the file in the repository
103
+ * @returns {GitHubFileContents}
104
+ * @throws {GitHubAPIError} on other API errors
105
+ */
106
+ async getFileContents(path) {
107
+ return await this.getFileContentsOnBranch(path, this.repository.defaultBranch);
108
+ }
109
+ async execGitStream(args, callback) {
110
+ return new Promise((resolve, reject) => {
111
+ const child = child_process.spawn('git', args, { cwd: this.cloneDir });
112
+ let stderr = '';
113
+ child.stderr.on('data', data => {
114
+ stderr += data;
115
+ });
116
+ const rl = readline.createInterface({
117
+ input: child.stdout,
118
+ crlfDelay: Infinity,
119
+ });
120
+ rl.on('line', callback);
121
+ child.on('close', code => {
122
+ if (code !== 0) {
123
+ reject(new Error(`Command failed ${code}: ${stderr}`));
124
+ }
125
+ else {
126
+ resolve();
127
+ }
128
+ });
129
+ });
130
+ }
131
+ async ensureRef(ref) {
132
+ try {
133
+ await execFile('git', ['rev-parse', '--verify', ref], {
134
+ cwd: this.cloneDir,
135
+ });
136
+ return ref;
137
+ }
138
+ catch (err) {
139
+ this.logger.debug(`Ref ${ref} not found locally, trying to fetch from origin...`);
140
+ try {
141
+ await execFile('git', ['fetch', 'origin', '--', ref], {
142
+ cwd: this.cloneDir,
143
+ });
144
+ return 'FETCH_HEAD';
145
+ }
146
+ catch (fetchErr) {
147
+ throw err; // Throw original error if fetch fails
148
+ }
149
+ }
150
+ }
151
+ /**
152
+ * Fetch the contents of a file
153
+ *
154
+ * @param {string} path The path to the file in the repository
155
+ * @param {string} branch The branch to fetch from
156
+ * @returns {GitHubFileContents}
157
+ * @throws {FileNotFoundError} if the file cannot be found
158
+ * @throws {GitHubAPIError} on other API errors
159
+ */
160
+ async getFileContentsOnBranch(path, branch) {
161
+ this.logger.debug(`Fetching file contents for file ${path} on branch ${branch}`);
162
+ const ref = await this.ensureRef(branch);
163
+ const lsTreeResult = await execFile('git', ['ls-tree', ref, path], {
164
+ cwd: this.cloneDir,
165
+ });
166
+ if (!lsTreeResult.stdout.trim()) {
167
+ throw new errors_1.FileNotFoundError(path);
168
+ }
169
+ const [info] = lsTreeResult.stdout.split('\t');
170
+ const [mode, , sha] = info.split(' ');
171
+ const { stdout } = await execFile('git', ['show', `${ref}:${path}`], {
172
+ cwd: this.cloneDir,
173
+ maxBuffer: 100 * 1024 * 1024,
174
+ });
175
+ return {
176
+ content: Buffer.from(stdout).toString('base64'),
177
+ parsedContent: stdout,
178
+ sha,
179
+ mode,
180
+ };
181
+ }
182
+ async getFileJson(path, branch) {
183
+ const content = await this.getFileContentsOnBranch(path, branch);
184
+ return JSON.parse(content.parsedContent);
185
+ }
186
+ /**
187
+ * Returns a list of paths to all files with a given name.
188
+ *
189
+ * If a prefix is specified, only return paths that match
190
+ * the provided prefix.
191
+ *
192
+ * @param filename The name of the file to find
193
+ * @param prefix Optional path prefix used to filter results
194
+ * @returns {string[]} List of file paths
195
+ * @throws {GitHubAPIError} on an API error
196
+ */
197
+ async findFilesByFilename(filename, prefix) {
198
+ return this.findFilesByFilenameAndRef(filename, this.repository.defaultBranch, prefix);
199
+ }
200
+ /**
201
+ * Returns a list of paths to all files with a given name.
202
+ *
203
+ * If a prefix is specified, only return paths that match
204
+ * the provided prefix.
205
+ *
206
+ * @param filename The name of the file to find
207
+ * @param ref Git reference to search files in
208
+ * @param prefix Optional path prefix used to filter results
209
+ * @throws {GitHubAPIError} on an API error
210
+ */
211
+ async findFilesByFilenameAndRef(filename, ref, prefix) {
212
+ this.logger.debug(`Looking in local clone for file ${filename} with ref ${ref} and prefix '${prefix}'`);
213
+ let normalizedPrefix = prefix
214
+ ? prefix.replace(/^[/\\]/, '').replace(/[/\\]$/, '')
215
+ : '';
216
+ if (normalizedPrefix === manifest_1.ROOT_PROJECT_PATH) {
217
+ normalizedPrefix = '';
218
+ }
219
+ const treePath = normalizedPrefix ? `${normalizedPrefix}/` : '.';
220
+ const resolvedRef = await this.ensureRef(ref);
221
+ this.logger.trace(`Executing stream: git ls-tree -r --name-only ${resolvedRef} ${treePath}`);
222
+ const matchedPaths = [];
223
+ await this.execGitStream(['ls-tree', '-r', '--name-only', resolvedRef, treePath], line => {
224
+ const trimmed = line.trim();
225
+ if (trimmed && path.posix.basename(trimmed) === filename) {
226
+ matchedPaths.push(trimmed);
227
+ }
228
+ });
229
+ if (normalizedPrefix) {
230
+ return matchedPaths
231
+ .map(p => {
232
+ if (p === normalizedPrefix)
233
+ return '';
234
+ if (p.startsWith(`${normalizedPrefix}/`)) {
235
+ return p.slice(normalizedPrefix.length + 1);
236
+ }
237
+ return p;
238
+ })
239
+ .filter(p => p !== '');
240
+ }
241
+ return matchedPaths;
242
+ }
243
+ /**
244
+ * Returns a list of paths to all files matching a glob pattern.
245
+ *
246
+ * If a prefix is specified, only return paths that match
247
+ * the provided prefix.
248
+ *
249
+ * @param glob The glob to match
250
+ * @param prefix Optional path prefix used to filter results
251
+ * @returns {string[]} List of file paths
252
+ * @throws {GitHubAPIError} on an API error
253
+ */
254
+ async findFilesByGlob(glob, prefix) {
255
+ return this.findFilesByGlobAndRef(glob, this.repository.defaultBranch, prefix);
256
+ }
257
+ /**
258
+ * Returns a list of paths to all files matching a glob pattern.
259
+ *
260
+ * If a prefix is specified, only return paths that match
261
+ * the provided prefix.
262
+ *
263
+ * @param glob The glob to match
264
+ * @param ref Git reference to search files in
265
+ * @param prefix Optional path prefix used to filter results
266
+ * @throws {GitHubAPIError} on an API error
267
+ */
268
+ async findFilesByGlobAndRef(glob, ref, prefix) {
269
+ this.logger.debug(`Looking in local clone for file matching glob ${glob} with ref ${ref} and prefix '${prefix}'`);
270
+ let normalizedPrefix = prefix
271
+ ? prefix.replace(/^[/\\]/, '').replace(/[/\\]$/, '')
272
+ : '';
273
+ if (normalizedPrefix === manifest_1.ROOT_PROJECT_PATH) {
274
+ normalizedPrefix = '';
275
+ }
276
+ const treePath = normalizedPrefix ? `${normalizedPrefix}/` : '.';
277
+ const resolvedRef = await this.ensureRef(ref);
278
+ const files = [];
279
+ const dirs = new Set();
280
+ await this.execGitStream(['ls-tree', '-r', '--name-only', resolvedRef, treePath], line => {
281
+ const trimmed = line.trim();
282
+ if (trimmed) {
283
+ files.push(trimmed);
284
+ let dir = path.posix.dirname(trimmed);
285
+ while (dir !== '.' && dir !== '/') {
286
+ dirs.add(dir);
287
+ dir = path.posix.dirname(dir);
288
+ }
289
+ }
290
+ });
291
+ const allPaths = [...files, ...dirs];
292
+ // Make paths relative to prefix if provided
293
+ let relativePaths = allPaths;
294
+ if (normalizedPrefix) {
295
+ relativePaths = allPaths
296
+ .map(p => {
297
+ if (p === normalizedPrefix)
298
+ return '';
299
+ if (p.startsWith(`${normalizedPrefix}/`)) {
300
+ return p.slice(normalizedPrefix.length + 1);
301
+ }
302
+ return p;
303
+ })
304
+ .filter(p => p !== '');
305
+ }
306
+ const regex = globToRegex(glob);
307
+ return relativePaths.filter(p => regex.test(p));
308
+ }
309
+ /**
310
+ * Returns a list of paths to all files with a given file
311
+ * extension.
312
+ *
313
+ * If a prefix is specified, only return paths that match
314
+ * the provided prefix.
315
+ *
316
+ * @param extension The file extension used to filter results.
317
+ * Example: `js`, `java`
318
+ * @param prefix Optional path prefix used to filter results
319
+ * @returns {string[]} List of file paths
320
+ * @throws {GitHubAPIError} on an API error
321
+ */
322
+ async findFilesByExtension(extension, prefix) {
323
+ return this.findFilesByExtensionAndRef(extension, this.repository.defaultBranch, prefix);
324
+ }
325
+ /**
326
+ * Returns a list of paths to all files with a given file
327
+ * extension.
328
+ *
329
+ * If a prefix is specified, only return paths that match
330
+ * the provided prefix.
331
+ *
332
+ * @param extension The file extension used to filter results.
333
+ * Example: `js`, `java`
334
+ * @param ref Git reference to search files in
335
+ * @param prefix Optional path prefix used to filter results
336
+ * @returns {string[]} List of file paths
337
+ * @throws {GitHubAPIError} on an API error
338
+ */
339
+ async findFilesByExtensionAndRef(extension, ref, prefix) {
340
+ this.logger.debug(`Looking in local clone for file matching extension ${extension} with ref ${ref} and prefix '${prefix}'`);
341
+ let normalizedPrefix = prefix
342
+ ? prefix.replace(/^[/\\]/, '').replace(/[/\\]$/, '')
343
+ : '';
344
+ if (normalizedPrefix === manifest_1.ROOT_PROJECT_PATH) {
345
+ normalizedPrefix = '';
346
+ }
347
+ const treePath = normalizedPrefix ? `${normalizedPrefix}/` : '.';
348
+ const resolvedRef = await this.ensureRef(ref);
349
+ const matchedPaths = [];
350
+ await this.execGitStream(['ls-tree', '-r', '--name-only', resolvedRef, treePath], line => {
351
+ const trimmed = line.trim();
352
+ if (trimmed && trimmed.endsWith(`.${extension}`)) {
353
+ matchedPaths.push(trimmed);
354
+ }
355
+ });
356
+ if (normalizedPrefix) {
357
+ return matchedPaths
358
+ .map(p => {
359
+ if (p === normalizedPrefix)
360
+ return '';
361
+ if (p.startsWith(`${normalizedPrefix}/`)) {
362
+ return p.slice(normalizedPrefix.length + 1);
363
+ }
364
+ return p;
365
+ })
366
+ .filter(p => p !== '');
367
+ }
368
+ return matchedPaths;
369
+ }
370
+ /**
371
+ * Returns the list of commits to the default branch after the provided filter
372
+ * query has been satified.
373
+ *
374
+ * @param {string} targetBranch Target branch of commit
375
+ * @param {CommitFilter} filter Callback function that returns whether a
376
+ * commit/pull request matches certain criteria
377
+ * @param {CommitIteratorOptions} options Query options
378
+ * @param {number} options.maxResults Limit the number of results searched.
379
+ * Defaults to unlimited.
380
+ * @param {boolean} options.backfillFiles If set, use the REST API for
381
+ * fetching the list of touched files in this commit. Defaults to `false`.
382
+ * @returns {Commit[]} List of commits to current branch
383
+ * @throws {GitHubAPIError} on an API error
384
+ */
385
+ async commitsSince(targetBranch, filter, options) {
386
+ const commits = [];
387
+ const generator = this.mergeCommitIterator(targetBranch, options);
388
+ for await (const commit of generator) {
389
+ if (filter(commit)) {
390
+ break;
391
+ }
392
+ commits.push(commit);
393
+ }
394
+ return commits;
395
+ }
396
+ /**
397
+ * Iterate through commit history with a max number of results scanned.
398
+ *
399
+ * @param {string} targetBranch target branch of commit
400
+ * @param {CommitIteratorOptions} options Query options
401
+ * @param {number} options.maxResults Limit the number of results searched.
402
+ * Defaults to unlimited.
403
+ * @param {boolean} options.backfillFiles If set, use the REST API for
404
+ * fetching the list of touched files in this commit. Defaults to `false`.
405
+ * @yields {Commit}
406
+ * @throws {GitHubAPIError} on an API error
407
+ */
408
+ async *mergeCommitIterator(targetBranch, options) {
409
+ var _a;
410
+ this.logger.debug(`Looking in local clone for commits on branch ${targetBranch}`);
411
+ const backfillFiles = (_a = options === null || options === void 0 ? void 0 : options.backfillFiles) !== null && _a !== void 0 ? _a : true;
412
+ let format = '---COMMIT_START---%n%H%n%B';
413
+ if (backfillFiles) {
414
+ format += '%n---FILES_START---';
415
+ }
416
+ const ref = await this.ensureRef(targetBranch);
417
+ const args = ['log', ref, `--pretty=format:${format}`];
418
+ if (backfillFiles) {
419
+ args.push('--name-only');
420
+ }
421
+ if (options === null || options === void 0 ? void 0 : options.maxResults) {
422
+ args.push('-n', options.maxResults.toString());
423
+ }
424
+ const { stdout } = await execFile('git', args, {
425
+ cwd: this.cloneDir,
426
+ maxBuffer: 100 * 1024 * 1024,
427
+ });
428
+ const blocks = stdout.split('---COMMIT_START---\n');
429
+ for (const block of blocks) {
430
+ if (!block.trim())
431
+ continue;
432
+ let commitInfo = block;
433
+ let files = [];
434
+ if (backfillFiles) {
435
+ const parts = block.split('\n---FILES_START---\n');
436
+ commitInfo = parts[0];
437
+ if (parts[1]) {
438
+ files = parts[1]
439
+ .split('\n')
440
+ .map((f) => f.trim())
441
+ .filter((f) => f);
442
+ }
443
+ }
444
+ const lines = commitInfo.split('\n');
445
+ const sha = lines[0].trim();
446
+ const message = lines.slice(1).join('\n').trim();
447
+ if (!sha)
448
+ continue;
449
+ const commit = {
450
+ sha,
451
+ message,
452
+ files: backfillFiles ? files : undefined,
453
+ };
454
+ const subject = lines[1] ? lines[1].trim() : '';
455
+ let prNumber;
456
+ let headBranchName = '';
457
+ const squashMatch = subject.match(/\s\(#(\d+)\)$/);
458
+ const mergeMatch = subject.match(/^Merge pull request #(\d+) from (.*)$/);
459
+ if (squashMatch) {
460
+ prNumber = parseInt(squashMatch[1], 10);
461
+ }
462
+ else if (mergeMatch) {
463
+ prNumber = parseInt(mergeMatch[1], 10);
464
+ headBranchName = mergeMatch[2].trim();
465
+ }
466
+ if (prNumber) {
467
+ commit.pullRequest = {
468
+ sha,
469
+ number: prNumber,
470
+ title: subject.replace(/\s\(#(\d+)\)$/, ''),
471
+ body: message,
472
+ labels: [],
473
+ files: backfillFiles ? files : [],
474
+ baseBranchName: targetBranch,
475
+ headBranchName,
476
+ };
477
+ }
478
+ yield commit;
479
+ }
480
+ }
481
+ /**
482
+ * Iterate through merged pull requests with a max number of results scanned.
483
+ *
484
+ * @param {string} targetBranch The base branch of the pull request
485
+ * @param {string} status The status of the pull request
486
+ * @param {number} maxResults Limit the number of results searched. Defaults to
487
+ * unlimited.
488
+ * @param {boolean} includeFiles Whether to fetch the list of files included in
489
+ * the pull request. Defaults to `true`.
490
+ * @yields {PullRequest}
491
+ * @throws {GitHubAPIError} on an API error
492
+ */
493
+ async *pullRequestIterator(targetBranch, status, maxResults, includeFiles) {
494
+ yield* this.gitHubApi.pullRequestIterator(targetBranch, status, maxResults, includeFiles);
495
+ }
496
+ /**
497
+ * Iterate through releases with a max number of results scanned.
498
+ *
499
+ * @param {ReleaseIteratorOptions} options Query options
500
+ * @param {number} options.maxResults Limit the number of results searched.
501
+ * Defaults to unlimited.
502
+ * @yields {GitHubRelease}
503
+ * @throws {GitHubAPIError} on an API error
504
+ */
505
+ async *releaseIterator(options) {
506
+ yield* this.gitHubApi.releaseIterator(options);
507
+ }
508
+ /**
509
+ * Iterate through tags with a max number of results scanned.
510
+ *
511
+ * @param {TagIteratorOptions} options Query options
512
+ * @param {number} options.maxResults Limit the number of results searched.
513
+ * Defaults to unlimited.
514
+ * @yields {GitHubTag}
515
+ * @throws {GitHubAPIError} on an API error
516
+ */
517
+ async *tagIterator(options) {
518
+ const { stdout } = await execFile('git', [
519
+ 'for-each-ref',
520
+ '--sort=-version:refname',
521
+ 'refs/tags',
522
+ '--format=%(refname:short)|%(objectname)|%(*objectname)',
523
+ ], { cwd: this.cloneDir });
524
+ const maxResults = (options === null || options === void 0 ? void 0 : options.maxResults) || Number.MAX_SAFE_INTEGER;
525
+ let results = 0;
526
+ for (const line of stdout.split('\n')) {
527
+ if (!line)
528
+ continue;
529
+ const [name, objectSha, commitSha] = line.split('|');
530
+ const sha = commitSha || objectSha;
531
+ if (sha) {
532
+ yield { name, sha };
533
+ results++;
534
+ if (results >= maxResults)
535
+ break;
536
+ }
537
+ }
538
+ }
539
+ async applyEditsAndPush(branch, targetBranch, message, changes) {
540
+ this.logger.debug(`Applying edits and pushing to ${branch}`);
541
+ // Checkout/Reset PR branch
542
+ await execFile('git', ['fetch', 'origin', '--', targetBranch], {
543
+ cwd: this.cloneDir,
544
+ });
545
+ await execFile('git', ['checkout', '-B', branch, `origin/${targetBranch}`], {
546
+ cwd: this.cloneDir,
547
+ });
548
+ // Write file edits
549
+ for (const [filePath, fileUpdate] of changes.entries()) {
550
+ const fullPath = path.join(this.cloneDir, filePath);
551
+ await fs.promises.mkdir(path.dirname(fullPath), { recursive: true });
552
+ if (fileUpdate.content !== null) {
553
+ await fs.promises.writeFile(fullPath, fileUpdate.content);
554
+ }
555
+ else {
556
+ await fs.promises.unlink(fullPath).catch(() => { });
557
+ }
558
+ if (fileUpdate.mode) {
559
+ await fs.promises.chmod(fullPath, parseInt(fileUpdate.mode, 8));
560
+ }
561
+ }
562
+ // Commit changes
563
+ const msgFile = path.join(os.tmpdir(), `release-please-commit-msg-${process.pid}-${Date.now()}`);
564
+ await fs.promises.writeFile(msgFile, message);
565
+ await execFile('git', ['add', '.'], { cwd: this.cloneDir });
566
+ try {
567
+ await execFile('git', ['commit', '--no-verify', '-F', msgFile], {
568
+ cwd: this.cloneDir,
569
+ });
570
+ }
571
+ catch (err) {
572
+ const error = err;
573
+ if (error.stdout && error.stdout.includes('nothing to commit')) {
574
+ this.logger.debug('Nothing to commit');
575
+ }
576
+ else {
577
+ throw err;
578
+ }
579
+ }
580
+ finally {
581
+ await fs.promises.unlink(msgFile).catch(() => { });
582
+ }
583
+ // Push transit
584
+ await execFile('git', ['push', '--no-verify', '-f', 'origin', branch], {
585
+ cwd: this.cloneDir,
586
+ });
587
+ }
588
+ /**
589
+ * Open a pull request
590
+ *
591
+ * @param {PullRequest} pullRequest Pull request data to update
592
+ * @param {string} targetBranch The base branch of the pull request
593
+ * @param {string} message The commit message for the commit
594
+ * @param {Update[]} updates The files to update
595
+ * @param {CreatePullRequestOptions} options The pull request options
596
+ * @throws {GitHubAPIError} on an API error
597
+ */
598
+ async createPullRequest(pullRequest, targetBranch, message, updates, options) {
599
+ const changes = await this.buildChangeSet(updates, targetBranch);
600
+ await this.applyEditsAndPush(pullRequest.headBranchName, targetBranch, message, changes);
601
+ this.logger.info('Creating pull request via GitHub API...');
602
+ return await this.gitHubApi.createPullRequest(pullRequest, targetBranch, options);
603
+ }
604
+ /**
605
+ * Update a pull request's title and body.
606
+ * @param {number} number The pull request number
607
+ * @param {ReleasePullRequest} releasePullRequest Pull request data to update
608
+ * @param {string} targetBranch The target branch of the pull request
609
+ * @param {string} options.signoffUser Optional. Commit signoff message
610
+ * @param {boolean} options.fork Optional. Whether to open the pull request from
611
+ * a fork or not. Defaults to `false`
612
+ * @param {PullRequestOverflowHandler} options.pullRequestOverflowHandler Optional.
613
+ * Handles extra large pull request body messages.
614
+ */
615
+ async updatePullRequest(number, pullRequest, targetBranch, options) {
616
+ const changes = await this.buildChangeSet(pullRequest.updates, targetBranch);
617
+ const message = pullRequest.title.toString();
618
+ await this.applyEditsAndPush(pullRequest.headRefName, targetBranch, message, changes);
619
+ const body = ((options === null || options === void 0 ? void 0 : options.pullRequestOverflowHandler)
620
+ ? await options.pullRequestOverflowHandler.handleOverflow(pullRequest)
621
+ : pullRequest.body)
622
+ .toString()
623
+ .slice(0, github_api_1.MAX_ISSUE_BODY_SIZE);
624
+ const pullResponseData = (await this.gitHubApi.octokit.pulls.update({
625
+ owner: this.repository.owner,
626
+ repo: this.repository.repo,
627
+ pull_number: number,
628
+ title: pullRequest.title.toString(),
629
+ body,
630
+ state: 'open',
631
+ })).data;
632
+ return {
633
+ headBranchName: pullResponseData.head.ref,
634
+ baseBranchName: pullResponseData.base.ref,
635
+ number: pullResponseData.number,
636
+ title: pullResponseData.title,
637
+ body: pullResponseData.body || '',
638
+ files: [],
639
+ labels: pullResponseData.labels
640
+ .map((label) => label.name)
641
+ .filter((name) => !!name),
642
+ };
643
+ }
644
+ async getPullRequest(number) {
645
+ return await this.gitHubApi.getPullRequest(number);
646
+ }
647
+ /**
648
+ * Create a GitHub release
649
+ *
650
+ * @param {Release} release Release parameters
651
+ * @param {ReleaseOptions} options Release option parameters
652
+ * @throws {DuplicateReleaseError} if the release tag already exists
653
+ * @throws {GitHubAPIError} on other API errors
654
+ */
655
+ async createRelease(release, options) {
656
+ return await this.gitHubApi.createRelease(release, options);
657
+ }
658
+ /**
659
+ * Makes a comment on a issue/pull request.
660
+ *
661
+ * @param {string} comment - The body of the comment to post.
662
+ * @param {number} number - The issue or pull request number.
663
+ * @throws {GitHubAPIError} on an API error
664
+ */
665
+ async commentOnIssue(comment, number) {
666
+ return await this.gitHubApi.commentOnIssue(comment, number);
667
+ }
668
+ /**
669
+ * Removes labels from an issue/pull request.
670
+ *
671
+ * @param {string[]} labels The labels to remove.
672
+ * @param {number} number The issue/pull request number.
673
+ */
674
+ async removeIssueLabels(labels, number) {
675
+ return await this.gitHubApi.removeIssueLabels(labels, number);
676
+ }
677
+ /**
678
+ * Adds label to an issue/pull request.
679
+ *
680
+ * @param {string[]} labels The labels to add.
681
+ * @param {number} number The issue/pull request number.
682
+ */
683
+ async addIssueLabels(labels, number) {
684
+ return await this.gitHubApi.addIssueLabels(labels, number);
685
+ }
686
+ /**
687
+ * Generate release notes from GitHub at tag
688
+ * @param {string} tagName Name of new release tag
689
+ * @param {string} targetCommitish Target commitish for new tag
690
+ * @param {string} previousTag Optional. Name of previous tag to analyze commits since
691
+ */
692
+ async generateReleaseNotes(tagName, targetCommitish, previousTag) {
693
+ return await this.gitHubApi.generateReleaseNotes(tagName, targetCommitish, previousTag);
694
+ }
695
+ /**
696
+ * Create a single file on a new branch based on an existing
697
+ * branch. This will force-push to that branch.
698
+ * @param {string} filename Filename with path in the repository
699
+ * @param {string} contents Contents of the file
700
+ * @param {string} newBranchName Name of the new branch
701
+ * @param {string} baseBranchName Name of the base branch (where
702
+ * new branch is forked from)
703
+ * @returns {string} HTML URL of the new file
704
+ */
705
+ async createFileOnNewBranch(filename, contents, newBranchName, baseBranchName) {
706
+ return await this.gitHubApi.createFileOnNewBranch(filename, contents, newBranchName, baseBranchName);
707
+ }
708
+ /**
709
+ * Given a set of proposed updates, build a changeset to suggest.
710
+ *
711
+ * @param {Update[]} updates The proposed updates
712
+ * @param {string} defaultBranch The target branch
713
+ * @return {Changes} The changeset to suggest.
714
+ * @throws {GitHubAPIError} on an API error
715
+ */
716
+ async buildChangeSet(updates, defaultBranch) {
717
+ const mergedUpdates = (0, composite_1.mergeUpdates)(updates);
718
+ const changes = new Map();
719
+ for (const update of mergedUpdates) {
720
+ let content;
721
+ try {
722
+ content = await this.getFileContentsOnBranch(update.path, defaultBranch);
723
+ }
724
+ catch (err) {
725
+ if (!(err instanceof errors_1.FileNotFoundError))
726
+ throw err;
727
+ if (!update.createIfMissing) {
728
+ console.warn(`file ${update.path} did not exist`);
729
+ continue;
730
+ }
731
+ }
732
+ const newContents = update.updater.updateContent(content ? content.parsedContent : undefined);
733
+ if (newContents) {
734
+ changes.set(update.path, {
735
+ content: newContents,
736
+ originalContent: content ? content.parsedContent : null,
737
+ mode: content ? content.mode : git_file_utils_1.DEFAULT_FILE_MODE,
738
+ });
739
+ }
740
+ }
741
+ return changes;
742
+ }
743
+ }
744
+ exports.LocalGitHub = LocalGitHub;
745
+ function globToRegex(glob) {
746
+ let reg = '';
747
+ let i = 0;
748
+ while (i < glob.length) {
749
+ const c = glob[i];
750
+ if (c === '*') {
751
+ if (i + 1 < glob.length && glob[i + 1] === '*') {
752
+ if (i + 2 < glob.length && glob[i + 2] === '/') {
753
+ reg += '(?:.*\\/)?';
754
+ i += 2;
755
+ }
756
+ else {
757
+ reg += '.*';
758
+ i++;
759
+ }
760
+ }
761
+ else {
762
+ reg += '[^/]*';
763
+ }
764
+ }
765
+ else if (c === '?') {
766
+ reg += '[^/]';
767
+ }
768
+ else if (['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\'].includes(c)) {
769
+ reg += '\\' + c;
770
+ }
771
+ else {
772
+ reg += c;
773
+ }
774
+ i++;
775
+ }
776
+ return new RegExp(`^${reg}$`);
777
+ }
778
+ //# sourceMappingURL=local-github.js.map