@i-santos/create-package-starter 1.4.0 → 1.5.0-beta.10
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/README.md +210 -1
- package/lib/run.js +4034 -443
- package/package.json +1 -1
- package/template/.github/PULL_REQUEST_TEMPLATE.md +5 -10
- package/template/.github/workflows/auto-retarget-pr.yml +57 -0
- package/template/.github/workflows/ci.yml +15 -0
- package/template/.github/workflows/promote-stable.yml +103 -0
- package/template/.github/workflows/release.yml +9 -1
- package/template/CONTRIBUTING.md +8 -0
- package/template/README.md +11 -0
- package/template/gitignore +24 -0
- package/template/package.json +6 -1
package/lib/run.js
CHANGED
|
@@ -1,38 +1,66 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
|
+
const readline = require('readline/promises');
|
|
4
5
|
|
|
5
6
|
const CHANGESETS_DEP = '@changesets/cli';
|
|
6
7
|
const CHANGESETS_DEP_VERSION = '^2.29.7';
|
|
7
8
|
const DEFAULT_BASE_BRANCH = 'main';
|
|
9
|
+
const DEFAULT_BETA_BRANCH = 'release/beta';
|
|
10
|
+
const DEFAULT_PROMOTE_WORKFLOW = 'promote-stable.yml';
|
|
8
11
|
const DEFAULT_RULESET_NAME = 'Default main branch protection';
|
|
12
|
+
const REQUIRED_CHECK_CONTEXT = 'required-check';
|
|
13
|
+
const DEFAULT_RELEASE_AUTH = 'pat';
|
|
14
|
+
const RELEASE_AUTH_MODES = new Set(['github-token', 'pat', 'app', 'manual-trigger']);
|
|
15
|
+
const RELEASE_AUTH_APP_REQUIRED_SECRETS = ['GH_APP_PRIVATE_KEY'];
|
|
16
|
+
const RELEASE_AUTH_APP_ID_SECRETS = ['GH_APP_CLIENT_ID', 'GH_APP_ID'];
|
|
17
|
+
const RELEASE_AUTH_DOC_LINKS = {
|
|
18
|
+
overview: 'https://docs.github.com/apps',
|
|
19
|
+
create: 'https://docs.github.com/apps/creating-github-apps/registering-a-github-app/registering-a-github-app',
|
|
20
|
+
install: 'https://docs.github.com/apps/using-github-apps/installing-your-own-github-app',
|
|
21
|
+
secrets: 'https://docs.github.com/actions/security-guides/using-secrets-in-github-actions',
|
|
22
|
+
internal: 'https://github.com/i-santos/package-starter/blob/main/docs/release-auth-github-app.md'
|
|
23
|
+
};
|
|
9
24
|
|
|
10
25
|
const MANAGED_FILE_SPECS = [
|
|
11
26
|
['.changeset/config.json', '.changeset/config.json'],
|
|
12
27
|
['.changeset/README.md', '.changeset/README.md'],
|
|
13
28
|
['.github/workflows/ci.yml', '.github/workflows/ci.yml'],
|
|
14
29
|
['.github/workflows/release.yml', '.github/workflows/release.yml'],
|
|
30
|
+
['.github/workflows/promote-stable.yml', '.github/workflows/promote-stable.yml'],
|
|
31
|
+
['.github/workflows/auto-retarget-pr.yml', '.github/workflows/auto-retarget-pr.yml'],
|
|
15
32
|
['.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE.md'],
|
|
16
33
|
['.github/CODEOWNERS', '.github/CODEOWNERS'],
|
|
17
34
|
['CONTRIBUTING.md', 'CONTRIBUTING.md'],
|
|
18
35
|
['README.md', 'README.md'],
|
|
19
|
-
['.gitignore', '
|
|
36
|
+
['.gitignore', 'gitignore']
|
|
20
37
|
];
|
|
38
|
+
const INIT_CREATE_ONLY_FILES = new Set(['README.md', 'CONTRIBUTING.md']);
|
|
21
39
|
|
|
22
40
|
function usage() {
|
|
23
41
|
return [
|
|
24
42
|
'Usage:',
|
|
25
|
-
' create-package-starter --name <name> [--out <directory>] [--default-branch <branch>]',
|
|
26
|
-
' create-package-starter init [--dir <directory>] [--force] [--cleanup-legacy-release] [--scope <scope>] [--default-branch <branch>]',
|
|
43
|
+
' create-package-starter --name <name> [--out <directory>] [--default-branch <branch>] [--release-auth github-token|pat|app|manual-trigger]',
|
|
44
|
+
' create-package-starter init [--dir <directory>] [--force] [--cleanup-legacy-release] [--scope <scope>] [--default-branch <branch>] [--with-github] [--with-npm] [--with-beta] [--repo <owner/repo>] [--beta-branch <branch>] [--ruleset <path>] [--release-auth github-token|pat|app|manual-trigger] [--dry-run] [--yes]',
|
|
27
45
|
' create-package-starter setup-github [--repo <owner/repo>] [--default-branch <branch>] [--ruleset <path>] [--dry-run]',
|
|
46
|
+
' create-package-starter setup-beta [--dir <directory>] [--repo <owner/repo>] [--beta-branch <branch>] [--default-branch <branch>] [--release-auth github-token|pat|app|manual-trigger] [--force] [--dry-run] [--yes]',
|
|
47
|
+
' create-package-starter open-pr [--repo <owner/repo>] [--base <branch>] [--head <branch>] [--title <text>] [--body <text>] [--body-file <path>] [--template <path>] [--draft] [--auto-merge] [--watch-checks] [--check-timeout <minutes>] [--yes] [--dry-run]',
|
|
48
|
+
' create-package-starter release-cycle [--repo <owner/repo>] [--mode auto|open-pr|publish] [--phase code|full] [--track auto|beta|stable] [--promote-stable] [--promote-type patch|minor|major] [--promote-summary <text>] [--head <branch>] [--base <branch>] [--title <text>] [--body-file <path>] [--update-pr-description] [--draft] [--auto-merge] [--watch-checks] [--check-timeout <minutes>] [--confirm-merges] [--merge-when-green] [--merge-method squash|merge|rebase] [--wait-release-pr] [--release-pr-timeout <minutes>] [--merge-release-pr] [--verify-npm] [--confirm-cleanup] [--sync-base auto|rebase|merge|off] [--no-resume] [--no-cleanup] [--yes] [--dry-run]',
|
|
49
|
+
' create-package-starter promote-stable [--dir <directory>] [--type patch|minor|major] [--summary <text>] [--dry-run]',
|
|
28
50
|
' create-package-starter setup-npm [--dir <directory>] [--publish-first] [--dry-run]',
|
|
29
51
|
'',
|
|
30
52
|
'Examples:',
|
|
31
53
|
' create-package-starter --name hello-package',
|
|
32
|
-
' create-package-starter --name @i-santos/swarm --out ./packages',
|
|
54
|
+
' create-package-starter --name @i-santos/swarm --out ./packages --release-auth pat',
|
|
33
55
|
' create-package-starter init --dir ./my-package',
|
|
34
56
|
' create-package-starter init --cleanup-legacy-release',
|
|
35
57
|
' create-package-starter setup-github --repo i-santos/firestack --dry-run',
|
|
58
|
+
' create-package-starter init --dir . --with-github --with-beta --with-npm --yes',
|
|
59
|
+
' create-package-starter setup-beta --dir . --beta-branch release/beta --release-auth app',
|
|
60
|
+
' create-package-starter open-pr --auto-merge --watch-checks',
|
|
61
|
+
' create-package-starter release-cycle --yes',
|
|
62
|
+
' create-package-starter release-cycle --promote-stable --promote-type minor --yes',
|
|
63
|
+
' create-package-starter promote-stable --dir . --type patch --summary "Promote beta to stable"',
|
|
36
64
|
' create-package-starter setup-npm --dir . --publish-first'
|
|
37
65
|
].join('\n');
|
|
38
66
|
}
|
|
@@ -49,7 +77,9 @@ function parseValueFlag(argv, index, flag) {
|
|
|
49
77
|
function parseCreateArgs(argv) {
|
|
50
78
|
const args = {
|
|
51
79
|
out: process.cwd(),
|
|
52
|
-
defaultBranch: DEFAULT_BASE_BRANCH
|
|
80
|
+
defaultBranch: DEFAULT_BASE_BRANCH,
|
|
81
|
+
releaseAuth: DEFAULT_RELEASE_AUTH,
|
|
82
|
+
releaseAuthProvided: false
|
|
53
83
|
};
|
|
54
84
|
|
|
55
85
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -73,6 +103,13 @@ function parseCreateArgs(argv) {
|
|
|
73
103
|
continue;
|
|
74
104
|
}
|
|
75
105
|
|
|
106
|
+
if (token === '--release-auth') {
|
|
107
|
+
args.releaseAuth = parseValueFlag(argv, i, '--release-auth');
|
|
108
|
+
args.releaseAuthProvided = true;
|
|
109
|
+
i += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
if (token === '--help' || token === '-h') {
|
|
77
114
|
args.help = true;
|
|
78
115
|
continue;
|
|
@@ -90,7 +127,17 @@ function parseInitArgs(argv) {
|
|
|
90
127
|
force: false,
|
|
91
128
|
cleanupLegacyRelease: false,
|
|
92
129
|
defaultBranch: DEFAULT_BASE_BRANCH,
|
|
93
|
-
|
|
130
|
+
betaBranch: DEFAULT_BETA_BRANCH,
|
|
131
|
+
scope: '',
|
|
132
|
+
repo: '',
|
|
133
|
+
ruleset: '',
|
|
134
|
+
releaseAuth: DEFAULT_RELEASE_AUTH,
|
|
135
|
+
releaseAuthProvided: false,
|
|
136
|
+
withGithub: false,
|
|
137
|
+
withNpm: false,
|
|
138
|
+
withBeta: false,
|
|
139
|
+
dryRun: false,
|
|
140
|
+
yes: false
|
|
94
141
|
};
|
|
95
142
|
|
|
96
143
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -114,6 +161,56 @@ function parseInitArgs(argv) {
|
|
|
114
161
|
continue;
|
|
115
162
|
}
|
|
116
163
|
|
|
164
|
+
if (token === '--beta-branch') {
|
|
165
|
+
args.betaBranch = parseValueFlag(argv, i, '--beta-branch');
|
|
166
|
+
i += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (token === '--repo') {
|
|
171
|
+
args.repo = parseValueFlag(argv, i, '--repo');
|
|
172
|
+
i += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (token === '--ruleset') {
|
|
177
|
+
args.ruleset = parseValueFlag(argv, i, '--ruleset');
|
|
178
|
+
i += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (token === '--release-auth') {
|
|
183
|
+
args.releaseAuth = parseValueFlag(argv, i, '--release-auth');
|
|
184
|
+
args.releaseAuthProvided = true;
|
|
185
|
+
i += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (token === '--with-github') {
|
|
190
|
+
args.withGithub = true;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (token === '--with-npm') {
|
|
195
|
+
args.withNpm = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (token === '--with-beta') {
|
|
200
|
+
args.withBeta = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (token === '--dry-run') {
|
|
205
|
+
args.dryRun = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (token === '--yes') {
|
|
210
|
+
args.yes = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
117
214
|
if (token === '--force') {
|
|
118
215
|
args.force = true;
|
|
119
216
|
continue;
|
|
@@ -215,549 +312,3988 @@ function parseSetupNpmArgs(argv) {
|
|
|
215
312
|
return args;
|
|
216
313
|
}
|
|
217
314
|
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
315
|
+
function parseSetupBetaArgs(argv) {
|
|
316
|
+
const args = {
|
|
317
|
+
dir: process.cwd(),
|
|
318
|
+
betaBranch: DEFAULT_BETA_BRANCH,
|
|
319
|
+
defaultBranch: DEFAULT_BASE_BRANCH,
|
|
320
|
+
releaseAuth: DEFAULT_RELEASE_AUTH,
|
|
321
|
+
releaseAuthProvided: false,
|
|
322
|
+
force: false,
|
|
323
|
+
yes: false,
|
|
324
|
+
dryRun: false
|
|
325
|
+
};
|
|
225
326
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
mode: 'setup-github',
|
|
229
|
-
args: parseSetupGithubArgs(argv.slice(1))
|
|
230
|
-
};
|
|
231
|
-
}
|
|
327
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
328
|
+
const token = argv[i];
|
|
232
329
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
}
|
|
330
|
+
if (token === '--dir') {
|
|
331
|
+
args.dir = parseValueFlag(argv, i, '--dir');
|
|
332
|
+
i += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
239
335
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
336
|
+
if (token === '--repo') {
|
|
337
|
+
args.repo = parseValueFlag(argv, i, '--repo');
|
|
338
|
+
i += 1;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
245
341
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
342
|
+
if (token === '--beta-branch') {
|
|
343
|
+
args.betaBranch = parseValueFlag(argv, i, '--beta-branch');
|
|
344
|
+
i += 1;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
250
347
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
348
|
+
if (token === '--default-branch') {
|
|
349
|
+
args.defaultBranch = parseValueFlag(argv, i, '--default-branch');
|
|
350
|
+
i += 1;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
255
353
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
354
|
+
if (token === '--release-auth') {
|
|
355
|
+
args.releaseAuth = parseValueFlag(argv, i, '--release-auth');
|
|
356
|
+
args.releaseAuthProvided = true;
|
|
357
|
+
i += 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
260
360
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
361
|
+
if (token === '--force') {
|
|
362
|
+
args.force = true;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
265
365
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
366
|
+
if (token === '--yes') {
|
|
367
|
+
args.yes = true;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
270
370
|
|
|
271
|
-
|
|
272
|
-
|
|
371
|
+
if (token === '--dry-run') {
|
|
372
|
+
args.dryRun = true;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
273
375
|
|
|
274
|
-
|
|
275
|
-
|
|
376
|
+
if (token === '--help' || token === '-h') {
|
|
377
|
+
args.help = true;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
276
380
|
|
|
277
|
-
|
|
278
|
-
output = output.replace(new RegExp(`__${key}__`, 'g'), value);
|
|
381
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
279
382
|
}
|
|
280
383
|
|
|
281
|
-
return
|
|
384
|
+
return args;
|
|
282
385
|
}
|
|
283
386
|
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
387
|
+
function parsePromoteStableArgs(argv) {
|
|
388
|
+
const args = {
|
|
389
|
+
dir: process.cwd(),
|
|
390
|
+
type: 'patch',
|
|
391
|
+
summary: 'Promote beta track to stable release.',
|
|
392
|
+
dryRun: false
|
|
393
|
+
};
|
|
288
394
|
|
|
289
|
-
for (
|
|
290
|
-
const
|
|
291
|
-
const destPath = path.join(targetDir, entry.name);
|
|
292
|
-
const relativePath = path.posix.join(relativeBase, entry.name);
|
|
395
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
396
|
+
const token = argv[i];
|
|
293
397
|
|
|
294
|
-
if (
|
|
295
|
-
|
|
398
|
+
if (token === '--dir') {
|
|
399
|
+
args.dir = parseValueFlag(argv, i, '--dir');
|
|
400
|
+
i += 1;
|
|
296
401
|
continue;
|
|
297
402
|
}
|
|
298
403
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
404
|
+
if (token === '--type') {
|
|
405
|
+
args.type = parseValueFlag(argv, i, '--type');
|
|
406
|
+
i += 1;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
304
409
|
|
|
305
|
-
|
|
306
|
-
|
|
410
|
+
if (token === '--summary') {
|
|
411
|
+
args.summary = parseValueFlag(argv, i, '--summary');
|
|
412
|
+
i += 1;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
307
415
|
|
|
308
|
-
|
|
309
|
-
|
|
416
|
+
if (token === '--dry-run') {
|
|
417
|
+
args.dryRun = true;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
310
420
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
421
|
+
if (token === '--help' || token === '-h') {
|
|
422
|
+
args.help = true;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
315
427
|
}
|
|
316
428
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
} catch (error) {
|
|
320
|
-
throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
429
|
+
if (!['patch', 'minor', 'major'].includes(args.type)) {
|
|
430
|
+
throw new Error(`Invalid --type value: ${args.type}. Expected patch, minor, or major.`);
|
|
321
431
|
}
|
|
322
|
-
}
|
|
323
432
|
|
|
324
|
-
|
|
325
|
-
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
433
|
+
return args;
|
|
326
434
|
}
|
|
327
435
|
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
436
|
+
function parseOpenPrArgs(argv) {
|
|
437
|
+
const args = {
|
|
438
|
+
repo: '',
|
|
439
|
+
base: '',
|
|
440
|
+
head: '',
|
|
441
|
+
title: '',
|
|
442
|
+
body: '',
|
|
443
|
+
bodyFile: '',
|
|
444
|
+
template: '',
|
|
445
|
+
draft: false,
|
|
446
|
+
autoMerge: false,
|
|
447
|
+
watchChecks: false,
|
|
448
|
+
checkTimeout: 30,
|
|
449
|
+
updateExistingPr: true,
|
|
450
|
+
yes: false,
|
|
451
|
+
dryRun: false
|
|
339
452
|
};
|
|
340
|
-
}
|
|
341
453
|
|
|
342
|
-
|
|
343
|
-
|
|
454
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
455
|
+
const token = argv[i];
|
|
344
456
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
console.log(`scripts skipped: ${list(summary.skippedScriptKeys)}`);
|
|
351
|
-
console.log(`scripts removed: ${list(summary.removedScriptKeys)}`);
|
|
352
|
-
console.log(`dependencies updated: ${list(summary.updatedDependencyKeys)}`);
|
|
353
|
-
console.log(`dependencies skipped: ${list(summary.skippedDependencyKeys)}`);
|
|
354
|
-
console.log(`warnings: ${list(summary.warnings)}`);
|
|
355
|
-
}
|
|
457
|
+
if (token === '--repo') {
|
|
458
|
+
args.repo = parseValueFlag(argv, i, '--repo');
|
|
459
|
+
i += 1;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
356
462
|
|
|
357
|
-
|
|
358
|
-
|
|
463
|
+
if (token === '--base') {
|
|
464
|
+
args.base = parseValueFlag(argv, i, '--base');
|
|
465
|
+
i += 1;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
359
468
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
469
|
+
if (token === '--head') {
|
|
470
|
+
args.head = parseValueFlag(argv, i, '--head');
|
|
471
|
+
i += 1;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
363
474
|
|
|
364
|
-
|
|
365
|
-
|
|
475
|
+
if (token === '--title') {
|
|
476
|
+
args.title = parseValueFlag(argv, i, '--title');
|
|
477
|
+
i += 1;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
366
480
|
|
|
367
|
-
|
|
368
|
-
|
|
481
|
+
if (token === '--body') {
|
|
482
|
+
args.body = parseValueFlag(argv, i, '--body');
|
|
483
|
+
i += 1;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
369
486
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
487
|
+
if (token === '--body-file') {
|
|
488
|
+
args.bodyFile = parseValueFlag(argv, i, '--body-file');
|
|
489
|
+
i += 1;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
373
492
|
|
|
374
|
-
|
|
375
|
-
|
|
493
|
+
if (token === '--template') {
|
|
494
|
+
args.template = parseValueFlag(argv, i, '--template');
|
|
495
|
+
i += 1;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
376
498
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
499
|
+
if (token === '--check-timeout') {
|
|
500
|
+
args.checkTimeout = Number.parseFloat(parseValueFlag(argv, i, '--check-timeout'));
|
|
501
|
+
i += 1;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
381
504
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
505
|
+
if (token === '--update-pr-description') {
|
|
506
|
+
args.updateExistingPr = true;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
386
509
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
510
|
+
if (token === '--draft') {
|
|
511
|
+
args.draft = true;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
391
514
|
|
|
392
|
-
|
|
393
|
-
|
|
515
|
+
if (token === '--auto-merge') {
|
|
516
|
+
args.autoMerge = true;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
394
519
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const templatePath = path.join(templateDir, templateRelativePath);
|
|
520
|
+
if (token === '--watch-checks') {
|
|
521
|
+
args.watchChecks = true;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
400
524
|
|
|
401
|
-
if (
|
|
402
|
-
|
|
525
|
+
if (token === '--yes') {
|
|
526
|
+
args.yes = true;
|
|
527
|
+
continue;
|
|
403
528
|
}
|
|
404
529
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
530
|
+
if (token === '--dry-run') {
|
|
531
|
+
args.dryRun = true;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
409
534
|
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
summary.overwrittenFiles.push(targetRelativePath);
|
|
414
|
-
} else {
|
|
415
|
-
summary.skippedFiles.push(targetRelativePath);
|
|
535
|
+
if (token === '--help' || token === '-h') {
|
|
536
|
+
args.help = true;
|
|
537
|
+
continue;
|
|
416
538
|
}
|
|
539
|
+
|
|
540
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (!Number.isFinite(args.checkTimeout) || args.checkTimeout <= 0) {
|
|
544
|
+
throw new Error('Invalid --check-timeout value. Expected a positive number (minutes).');
|
|
417
545
|
}
|
|
546
|
+
|
|
547
|
+
return args;
|
|
418
548
|
}
|
|
419
549
|
|
|
420
|
-
function
|
|
421
|
-
const
|
|
550
|
+
function parseReleaseCycleArgs(argv) {
|
|
551
|
+
const args = {
|
|
552
|
+
repo: '',
|
|
553
|
+
mode: 'auto',
|
|
554
|
+
phase: 'full',
|
|
555
|
+
phaseProvided: false,
|
|
556
|
+
track: 'auto',
|
|
557
|
+
promoteStable: false,
|
|
558
|
+
promoteType: 'patch',
|
|
559
|
+
promoteSummary: 'Promote beta track to stable release.',
|
|
560
|
+
head: '',
|
|
561
|
+
base: '',
|
|
562
|
+
title: '',
|
|
563
|
+
bodyFile: '',
|
|
564
|
+
updatePrDescription: false,
|
|
565
|
+
draft: false,
|
|
566
|
+
autoMerge: true,
|
|
567
|
+
watchChecks: true,
|
|
568
|
+
checkTimeout: 30,
|
|
569
|
+
confirmMerges: false,
|
|
570
|
+
syncBase: 'auto',
|
|
571
|
+
resume: true,
|
|
572
|
+
mergeWhenGreen: true,
|
|
573
|
+
mergeMethod: 'squash',
|
|
574
|
+
waitReleasePr: true,
|
|
575
|
+
releasePrTimeout: 30,
|
|
576
|
+
mergeReleasePr: true,
|
|
577
|
+
verifyNpm: true,
|
|
578
|
+
confirmCleanup: false,
|
|
579
|
+
noCleanup: false,
|
|
580
|
+
yes: false,
|
|
581
|
+
dryRun: false
|
|
582
|
+
};
|
|
422
583
|
|
|
423
|
-
for (
|
|
424
|
-
const
|
|
425
|
-
|| key.startsWith('release:beta')
|
|
426
|
-
|| key.startsWith('release:stable')
|
|
427
|
-
|| key.startsWith('release:promote')
|
|
428
|
-
|| key.startsWith('release:rollback');
|
|
584
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
585
|
+
const token = argv[i];
|
|
429
586
|
|
|
430
|
-
if (
|
|
587
|
+
if (token === '--repo') {
|
|
588
|
+
args.repo = parseValueFlag(argv, i, '--repo');
|
|
589
|
+
i += 1;
|
|
431
590
|
continue;
|
|
432
591
|
}
|
|
433
592
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
593
|
+
if (token === '--mode') {
|
|
594
|
+
args.mode = parseValueFlag(argv, i, '--mode');
|
|
595
|
+
i += 1;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
438
598
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
599
|
+
if (token === '--phase') {
|
|
600
|
+
args.phase = parseValueFlag(argv, i, '--phase');
|
|
601
|
+
args.phaseProvided = true;
|
|
602
|
+
i += 1;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
443
605
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
606
|
+
if (token === '--track') {
|
|
607
|
+
args.track = parseValueFlag(argv, i, '--track');
|
|
608
|
+
i += 1;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
448
611
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
612
|
+
if (token === '--promote-type') {
|
|
613
|
+
args.promoteType = parseValueFlag(argv, i, '--promote-type');
|
|
614
|
+
i += 1;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
452
617
|
|
|
453
|
-
|
|
618
|
+
if (token === '--promote-summary') {
|
|
619
|
+
args.promoteSummary = parseValueFlag(argv, i, '--promote-summary');
|
|
620
|
+
i += 1;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
454
623
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
};
|
|
624
|
+
if (token === '--head') {
|
|
625
|
+
args.head = parseValueFlag(argv, i, '--head');
|
|
626
|
+
i += 1;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
461
629
|
|
|
462
|
-
|
|
630
|
+
if (token === '--base') {
|
|
631
|
+
args.base = parseValueFlag(argv, i, '--base');
|
|
632
|
+
i += 1;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
463
635
|
|
|
464
|
-
|
|
465
|
-
|
|
636
|
+
if (token === '--title') {
|
|
637
|
+
args.title = parseValueFlag(argv, i, '--title');
|
|
638
|
+
i += 1;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
466
641
|
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
packageJsonChanged = true;
|
|
471
|
-
summary.updatedScriptKeys.push(key);
|
|
472
|
-
} else if (options.force && packageJson.scripts[key] !== value) {
|
|
473
|
-
packageJson.scripts[key] = value;
|
|
474
|
-
packageJsonChanged = true;
|
|
475
|
-
summary.updatedScriptKeys.push(key);
|
|
476
|
-
} else {
|
|
477
|
-
summary.skippedScriptKeys.push(key);
|
|
478
|
-
}
|
|
642
|
+
if (token === '--body-file') {
|
|
643
|
+
args.bodyFile = parseValueFlag(argv, i, '--body-file');
|
|
644
|
+
i += 1;
|
|
479
645
|
continue;
|
|
480
646
|
}
|
|
481
647
|
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
packageJson.scripts[key] = value;
|
|
485
|
-
packageJsonChanged = true;
|
|
486
|
-
}
|
|
487
|
-
summary.updatedScriptKeys.push(key);
|
|
648
|
+
if (token === '--update-pr-description') {
|
|
649
|
+
args.updatePrDescription = true;
|
|
488
650
|
continue;
|
|
489
651
|
}
|
|
490
652
|
|
|
491
|
-
|
|
492
|
-
|
|
653
|
+
if (token === '--check-timeout') {
|
|
654
|
+
args.checkTimeout = Number.parseFloat(parseValueFlag(argv, i, '--check-timeout'));
|
|
655
|
+
i += 1;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
493
658
|
|
|
494
|
-
|
|
659
|
+
if (token === '--release-pr-timeout') {
|
|
660
|
+
args.releasePrTimeout = Number.parseFloat(parseValueFlag(argv, i, '--release-pr-timeout'));
|
|
661
|
+
i += 1;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
495
664
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
665
|
+
if (token === '--merge-method') {
|
|
666
|
+
args.mergeMethod = parseValueFlag(argv, i, '--merge-method');
|
|
667
|
+
i += 1;
|
|
668
|
+
continue;
|
|
500
669
|
}
|
|
501
|
-
summary.updatedDependencyKeys.push(CHANGESETS_DEP);
|
|
502
|
-
} else {
|
|
503
|
-
summary.skippedDependencyKeys.push(CHANGESETS_DEP);
|
|
504
|
-
}
|
|
505
670
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (summary.removedScriptKeys.length > before) {
|
|
510
|
-
packageJsonChanged = true;
|
|
671
|
+
if (token === '--draft') {
|
|
672
|
+
args.draft = true;
|
|
673
|
+
continue;
|
|
511
674
|
}
|
|
512
|
-
}
|
|
513
675
|
|
|
514
|
-
|
|
676
|
+
if (token === '--auto-merge') {
|
|
677
|
+
args.autoMerge = true;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
515
680
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
PACKAGE_NAME: packageName,
|
|
520
|
-
DEFAULT_BRANCH: options.defaultBranch,
|
|
521
|
-
SCOPE: deriveScope(options.scope, packageName)
|
|
681
|
+
if (token === '--watch-checks') {
|
|
682
|
+
args.watchChecks = true;
|
|
683
|
+
continue;
|
|
522
684
|
}
|
|
523
|
-
}, summary);
|
|
524
685
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
686
|
+
if (token === '--confirm-merges') {
|
|
687
|
+
args.confirmMerges = true;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
528
690
|
|
|
529
|
-
|
|
530
|
-
|
|
691
|
+
if (token === '--sync-base') {
|
|
692
|
+
args.syncBase = parseValueFlag(argv, i, '--sync-base');
|
|
693
|
+
i += 1;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
531
696
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
697
|
+
if (token === '--no-resume') {
|
|
698
|
+
args.resume = false;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
536
701
|
|
|
537
|
-
|
|
538
|
-
|
|
702
|
+
if (token === '--merge-when-green') {
|
|
703
|
+
args.mergeWhenGreen = true;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
539
706
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
707
|
+
if (token === '--wait-release-pr') {
|
|
708
|
+
args.waitReleasePr = true;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
543
711
|
|
|
544
|
-
|
|
545
|
-
|
|
712
|
+
if (token === '--merge-release-pr') {
|
|
713
|
+
args.mergeReleasePr = true;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
546
716
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
717
|
+
if (token === '--promote-stable') {
|
|
718
|
+
args.promoteStable = true;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
550
721
|
|
|
551
|
-
|
|
722
|
+
if (token === '--verify-npm') {
|
|
723
|
+
args.verifyNpm = true;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
552
726
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
});
|
|
727
|
+
if (token === '--confirm-cleanup') {
|
|
728
|
+
args.confirmCleanup = true;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
558
731
|
|
|
559
|
-
|
|
732
|
+
if (token === '--no-cleanup') {
|
|
733
|
+
args.noCleanup = true;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
560
736
|
|
|
561
|
-
|
|
562
|
-
|
|
737
|
+
if (token === '--yes') {
|
|
738
|
+
args.yes = true;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
563
741
|
|
|
564
|
-
|
|
565
|
-
|
|
742
|
+
if (token === '--dry-run') {
|
|
743
|
+
args.dryRun = true;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
566
746
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
747
|
+
if (token === '--help' || token === '-h') {
|
|
748
|
+
args.help = true;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
571
751
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
752
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
753
|
+
}
|
|
575
754
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
...options
|
|
580
|
-
});
|
|
581
|
-
}
|
|
755
|
+
if (!['auto', 'open-pr', 'publish'].includes(args.mode)) {
|
|
756
|
+
throw new Error('Invalid --mode value. Expected auto, open-pr, or publish.');
|
|
757
|
+
}
|
|
582
758
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
759
|
+
if (!['code', 'full'].includes(args.phase)) {
|
|
760
|
+
throw new Error('Invalid --phase value. Expected code or full.');
|
|
761
|
+
}
|
|
586
762
|
|
|
587
|
-
if (
|
|
588
|
-
|
|
763
|
+
if (!['auto', 'beta', 'stable'].includes(args.track)) {
|
|
764
|
+
throw new Error('Invalid --track value. Expected auto, beta, or stable.');
|
|
589
765
|
}
|
|
590
766
|
|
|
591
|
-
|
|
592
|
-
|
|
767
|
+
if (!['patch', 'minor', 'major'].includes(args.promoteType)) {
|
|
768
|
+
throw new Error('Invalid --promote-type value. Expected patch, minor, or major.');
|
|
769
|
+
}
|
|
593
770
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
return args.repo;
|
|
771
|
+
if (!['auto', 'rebase', 'merge', 'off'].includes(args.syncBase)) {
|
|
772
|
+
throw new Error('Invalid --sync-base value. Expected auto, rebase, merge, or off.');
|
|
597
773
|
}
|
|
598
774
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
throw new Error('Could not infer repository. Use --repo <owner/repo>.');
|
|
775
|
+
if (!['squash', 'merge', 'rebase'].includes(args.mergeMethod)) {
|
|
776
|
+
throw new Error('Invalid --merge-method value. Expected squash, merge, or rebase.');
|
|
602
777
|
}
|
|
603
778
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
779
|
+
if (!Number.isFinite(args.checkTimeout) || args.checkTimeout <= 0) {
|
|
780
|
+
throw new Error('Invalid --check-timeout value. Expected a positive number (minutes).');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (!Number.isFinite(args.releasePrTimeout) || args.releasePrTimeout <= 0) {
|
|
784
|
+
throw new Error('Invalid --release-pr-timeout value. Expected a positive number (minutes).');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return args;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function validateReleaseAuthMode(mode, flagName = '--release-auth') {
|
|
791
|
+
if (!RELEASE_AUTH_MODES.has(mode)) {
|
|
792
|
+
throw new Error(`Invalid ${flagName} value: ${mode}. Expected one of: github-token, pat, app, manual-trigger.`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function parseArgs(argv) {
|
|
797
|
+
if (argv[0] === 'init') {
|
|
798
|
+
const args = parseInitArgs(argv.slice(1));
|
|
799
|
+
validateReleaseAuthMode(args.releaseAuth);
|
|
800
|
+
return {
|
|
801
|
+
mode: 'init',
|
|
802
|
+
args
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (argv[0] === 'setup-github') {
|
|
807
|
+
return {
|
|
808
|
+
mode: 'setup-github',
|
|
809
|
+
args: parseSetupGithubArgs(argv.slice(1))
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (argv[0] === 'setup-npm') {
|
|
814
|
+
return {
|
|
815
|
+
mode: 'setup-npm',
|
|
816
|
+
args: parseSetupNpmArgs(argv.slice(1))
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (argv[0] === 'setup-beta') {
|
|
821
|
+
const args = parseSetupBetaArgs(argv.slice(1));
|
|
822
|
+
validateReleaseAuthMode(args.releaseAuth);
|
|
823
|
+
return {
|
|
824
|
+
mode: 'setup-beta',
|
|
825
|
+
args
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (argv[0] === 'promote-stable') {
|
|
830
|
+
return {
|
|
831
|
+
mode: 'promote-stable',
|
|
832
|
+
args: parsePromoteStableArgs(argv.slice(1))
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (argv[0] === 'open-pr') {
|
|
837
|
+
return {
|
|
838
|
+
mode: 'open-pr',
|
|
839
|
+
args: parseOpenPrArgs(argv.slice(1))
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (argv[0] === 'release-cycle') {
|
|
844
|
+
return {
|
|
845
|
+
mode: 'release-cycle',
|
|
846
|
+
args: parseReleaseCycleArgs(argv.slice(1))
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const args = parseCreateArgs(argv);
|
|
851
|
+
validateReleaseAuthMode(args.releaseAuth);
|
|
852
|
+
return {
|
|
853
|
+
mode: 'create',
|
|
854
|
+
args
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function validateName(name) {
|
|
859
|
+
if (typeof name !== 'string') {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const plain = /^[a-z0-9][a-z0-9._-]*$/;
|
|
864
|
+
const scoped = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/;
|
|
865
|
+
return plain.test(name) || scoped.test(name);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function packageDirFromName(packageName) {
|
|
869
|
+
const parts = packageName.split('/');
|
|
870
|
+
return parts[parts.length - 1];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function deriveScope(argsScope, packageName) {
|
|
874
|
+
if (argsScope) {
|
|
875
|
+
return argsScope;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (typeof packageName === 'string' && packageName.startsWith('@')) {
|
|
879
|
+
const first = packageName.split('/')[0];
|
|
880
|
+
return first.slice(1);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return 'team';
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function buildReleaseAuthVariables(releaseAuthMode) {
|
|
887
|
+
if (releaseAuthMode === 'github-token') {
|
|
888
|
+
return {
|
|
889
|
+
RELEASE_AUTH_APP_STEP: '',
|
|
890
|
+
RELEASE_AUTH_CHECKOUT_TOKEN: '${{ secrets.GITHUB_TOKEN }}',
|
|
891
|
+
RELEASE_AUTH_GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (releaseAuthMode === 'pat') {
|
|
896
|
+
return {
|
|
897
|
+
RELEASE_AUTH_APP_STEP: '',
|
|
898
|
+
RELEASE_AUTH_CHECKOUT_TOKEN: '${{ secrets.CHANGESETS_GH_TOKEN || secrets.GITHUB_TOKEN }}',
|
|
899
|
+
RELEASE_AUTH_GITHUB_TOKEN: '${{ secrets.CHANGESETS_GH_TOKEN || secrets.GITHUB_TOKEN }}'
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (releaseAuthMode === 'app') {
|
|
904
|
+
return {
|
|
905
|
+
RELEASE_AUTH_APP_STEP: [
|
|
906
|
+
' - name: Generate GitHub App token',
|
|
907
|
+
' id: app-token',
|
|
908
|
+
' uses: actions/create-github-app-token@v1',
|
|
909
|
+
' with:',
|
|
910
|
+
' app-id: ${{ secrets.GH_APP_ID || secrets.GH_APP_CLIENT_ID }}',
|
|
911
|
+
' private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}',
|
|
912
|
+
''
|
|
913
|
+
].join('\n'),
|
|
914
|
+
RELEASE_AUTH_CHECKOUT_TOKEN: '${{ steps.app-token.outputs.token }}',
|
|
915
|
+
RELEASE_AUTH_GITHUB_TOKEN: '${{ steps.app-token.outputs.token }}'
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
RELEASE_AUTH_APP_STEP: '',
|
|
921
|
+
RELEASE_AUTH_CHECKOUT_TOKEN: '${{ secrets.GITHUB_TOKEN }}',
|
|
922
|
+
RELEASE_AUTH_GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function appendReleaseAuthWarnings(summary, releaseAuthMode, options = {}) {
|
|
927
|
+
if (releaseAuthMode === 'manual-trigger') {
|
|
928
|
+
summary.warnings.push('release-auth recommendation: use pat/app when you need automatic CI retriggers for release PR updates.');
|
|
929
|
+
summary.warnings.push('manual-trigger mode selected: release PR updates may not retrigger CI automatically.');
|
|
930
|
+
summary.warnings.push('If release PR checks are pending, push an empty commit to changeset-release/* to retrigger CI.');
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (releaseAuthMode === 'app') {
|
|
935
|
+
summary.warnings.push('release-auth recommendation: app mode is preferred for long-lived org/repo automation.');
|
|
936
|
+
const missing = options.missingAppSecrets || [];
|
|
937
|
+
if (missing.length > 0) {
|
|
938
|
+
summary.warnings.push(`release-auth app mode selected: missing repository secrets: ${missing.join(', ')}`);
|
|
939
|
+
} else if (options.appSecretsChecked) {
|
|
940
|
+
summary.warnings.push('release-auth app mode selected: required repository secrets detected.');
|
|
941
|
+
} else {
|
|
942
|
+
summary.warnings.push('release-auth app mode selected: ensure GH_APP_CLIENT_ID (or GH_APP_ID) and GH_APP_PRIVATE_KEY repository secrets are configured.');
|
|
943
|
+
}
|
|
944
|
+
summary.warnings.push(`GitHub Apps overview: ${RELEASE_AUTH_DOC_LINKS.overview}`);
|
|
945
|
+
summary.warnings.push(`Create GitHub App: ${RELEASE_AUTH_DOC_LINKS.create}`);
|
|
946
|
+
summary.warnings.push(`Install GitHub App: ${RELEASE_AUTH_DOC_LINKS.install}`);
|
|
947
|
+
summary.warnings.push(`Manage Actions secrets: ${RELEASE_AUTH_DOC_LINKS.secrets}`);
|
|
948
|
+
summary.warnings.push(`Project guide: ${RELEASE_AUTH_DOC_LINKS.internal}`);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (releaseAuthMode === 'pat') {
|
|
953
|
+
summary.warnings.push('release-auth recommendation: pat mode is the fastest setup for solo/small projects.');
|
|
954
|
+
summary.warnings.push('release-auth pat mode selected: ensure CHANGESETS_GH_TOKEN secret is configured for reliable release PR check retriggers.');
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
summary.warnings.push('release-auth recommendation: github-token mode is simplest, but may skip downstream workflow retriggers.');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function resolveReleaseAuthSelection(args, summary, options = {}) {
|
|
962
|
+
if (args.releaseAuthProvided) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
967
|
+
summary.warnings.push(`--release-auth not provided in non-interactive mode. Defaulting to "${args.releaseAuth}".`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const selected = await askChoice(
|
|
972
|
+
`${options.contextLabel || 'Select release auth mode'}:`,
|
|
973
|
+
['pat', 'app', 'github-token', 'manual-trigger'],
|
|
974
|
+
0
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
args.releaseAuth = selected;
|
|
978
|
+
summary.warnings.push(`release-auth selected interactively: ${selected}`);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function renderTemplateString(source, variables) {
|
|
982
|
+
let output = source;
|
|
983
|
+
|
|
984
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
985
|
+
output = output.replace(new RegExp(`__${key}__`, 'g'), value);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return output;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function copyDirRecursive(sourceDir, targetDir, variables, relativeBase = '') {
|
|
992
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
993
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
994
|
+
const createdFiles = [];
|
|
995
|
+
|
|
996
|
+
for (const entry of entries) {
|
|
997
|
+
const srcPath = path.join(sourceDir, entry.name);
|
|
998
|
+
const destinationEntryName = relativeBase === '' && entry.name === 'gitignore'
|
|
999
|
+
? '.gitignore'
|
|
1000
|
+
: entry.name;
|
|
1001
|
+
const destPath = path.join(targetDir, destinationEntryName);
|
|
1002
|
+
const relativePath = path.posix.join(relativeBase, destinationEntryName);
|
|
1003
|
+
|
|
1004
|
+
if (entry.isDirectory()) {
|
|
1005
|
+
createdFiles.push(...copyDirRecursive(srcPath, destPath, variables, relativePath));
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const source = fs.readFileSync(srcPath, 'utf8');
|
|
1010
|
+
const rendered = renderTemplateString(source, variables);
|
|
1011
|
+
fs.writeFileSync(destPath, rendered);
|
|
1012
|
+
createdFiles.push(relativePath);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return createdFiles;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function readJsonFile(filePath) {
|
|
1019
|
+
let raw;
|
|
1020
|
+
|
|
1021
|
+
try {
|
|
1022
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
throw new Error(`Failed to read ${filePath}: ${error.message}`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
try {
|
|
1028
|
+
return JSON.parse(raw);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function writeJsonFile(filePath, value) {
|
|
1035
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function createSummary() {
|
|
1039
|
+
return {
|
|
1040
|
+
createdFiles: [],
|
|
1041
|
+
overwrittenFiles: [],
|
|
1042
|
+
skippedFiles: [],
|
|
1043
|
+
updatedScriptKeys: [],
|
|
1044
|
+
skippedScriptKeys: [],
|
|
1045
|
+
removedScriptKeys: [],
|
|
1046
|
+
updatedDependencyKeys: [],
|
|
1047
|
+
skippedDependencyKeys: [],
|
|
1048
|
+
warnings: []
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function printSummary(title, summary) {
|
|
1053
|
+
const unique = (values) => [...new Set(values)];
|
|
1054
|
+
const formatList = (values) => {
|
|
1055
|
+
const normalized = unique(values);
|
|
1056
|
+
if (!normalized.length) {
|
|
1057
|
+
return [' - none'];
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return normalized.map((item) => ` - ${item}`);
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
console.log(title);
|
|
1064
|
+
console.log('');
|
|
1065
|
+
console.log('Preflight');
|
|
1066
|
+
console.log(' - completed');
|
|
1067
|
+
console.log('');
|
|
1068
|
+
console.log('Apply');
|
|
1069
|
+
console.log('files created:');
|
|
1070
|
+
formatList(summary.createdFiles).forEach((line) => console.log(line));
|
|
1071
|
+
console.log('files overwritten:');
|
|
1072
|
+
formatList(summary.overwrittenFiles).forEach((line) => console.log(line));
|
|
1073
|
+
console.log('files skipped:');
|
|
1074
|
+
formatList(summary.skippedFiles).forEach((line) => console.log(line));
|
|
1075
|
+
console.log('scripts updated:');
|
|
1076
|
+
formatList(summary.updatedScriptKeys).forEach((line) => console.log(line));
|
|
1077
|
+
console.log('scripts skipped:');
|
|
1078
|
+
formatList(summary.skippedScriptKeys).forEach((line) => console.log(line));
|
|
1079
|
+
console.log('scripts removed:');
|
|
1080
|
+
formatList(summary.removedScriptKeys).forEach((line) => console.log(line));
|
|
1081
|
+
console.log('dependencies updated:');
|
|
1082
|
+
formatList(summary.updatedDependencyKeys).forEach((line) => console.log(line));
|
|
1083
|
+
console.log('dependencies skipped:');
|
|
1084
|
+
formatList(summary.skippedDependencyKeys).forEach((line) => console.log(line));
|
|
1085
|
+
console.log('');
|
|
1086
|
+
console.log('Summary');
|
|
1087
|
+
console.log('warnings:');
|
|
1088
|
+
formatList(summary.warnings).forEach((line) => console.log(line));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
class StepReporter {
|
|
1092
|
+
constructor() {
|
|
1093
|
+
this.active = null;
|
|
1094
|
+
this.frames = ['-', '\\', '|', '/'];
|
|
1095
|
+
this.frameIndex = 0;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
canSpin() {
|
|
1099
|
+
return Boolean(process.stdout.isTTY) && process.env.CI !== 'true';
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
start(stepId, message) {
|
|
1103
|
+
this.stop();
|
|
1104
|
+
if (!this.canSpin()) {
|
|
1105
|
+
logStep('run', message);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
this.active = {
|
|
1110
|
+
id: stepId,
|
|
1111
|
+
message,
|
|
1112
|
+
timer: setInterval(() => {
|
|
1113
|
+
const frame = this.frames[this.frameIndex % this.frames.length];
|
|
1114
|
+
this.frameIndex += 1;
|
|
1115
|
+
process.stdout.write(`\r${frame} ${message}`);
|
|
1116
|
+
}, 80)
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
end(status, message) {
|
|
1121
|
+
if (this.active && this.active.timer) {
|
|
1122
|
+
clearInterval(this.active.timer);
|
|
1123
|
+
const finalLabel = status === 'ok'
|
|
1124
|
+
? 'OK'
|
|
1125
|
+
: status === 'warn'
|
|
1126
|
+
? 'WARN'
|
|
1127
|
+
: 'ERR';
|
|
1128
|
+
process.stdout.write(`\r[${finalLabel}] ${message}\n`);
|
|
1129
|
+
this.active = null;
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
logStep(status, message);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
ok(stepId, message) {
|
|
1137
|
+
this.end('ok', message);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
warn(stepId, message) {
|
|
1141
|
+
this.end('warn', message);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
fail(stepId, message) {
|
|
1145
|
+
this.end('err', message);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
stop() {
|
|
1149
|
+
if (!this.active || !this.active.timer) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
clearInterval(this.active.timer);
|
|
1154
|
+
process.stdout.write('\r');
|
|
1155
|
+
this.active = null;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function logStep(status, message) {
|
|
1160
|
+
const labels = {
|
|
1161
|
+
run: '[RUN ]',
|
|
1162
|
+
ok: '[OK ]',
|
|
1163
|
+
warn: '[WARN]',
|
|
1164
|
+
err: '[ERR ]'
|
|
1165
|
+
};
|
|
1166
|
+
const prefix = labels[status] || '[INFO]';
|
|
1167
|
+
const writer = status === 'err' ? console.error : console.log;
|
|
1168
|
+
writer(`${prefix} ${message}`);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async function confirmOrThrow(questionText) {
|
|
1172
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1173
|
+
throw new Error(`Confirmation required but no interactive terminal was detected. Re-run with --yes if you want to proceed non-interactively.`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const rl = readline.createInterface({
|
|
1177
|
+
input: process.stdin,
|
|
1178
|
+
output: process.stdout
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
try {
|
|
1182
|
+
const answer = await rl.question(`${questionText}\nType "yes" to continue: `);
|
|
1183
|
+
if (answer.trim().toLowerCase() !== 'yes') {
|
|
1184
|
+
throw new Error('Operation cancelled by user.');
|
|
1185
|
+
}
|
|
1186
|
+
} finally {
|
|
1187
|
+
rl.close();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
async function askYesNo(questionText, defaultValue = false) {
|
|
1192
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1193
|
+
return defaultValue;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const rl = readline.createInterface({
|
|
1197
|
+
input: process.stdin,
|
|
1198
|
+
output: process.stdout
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
try {
|
|
1202
|
+
const suffix = defaultValue ? '[Y/n]' : '[y/N]';
|
|
1203
|
+
const answer = await rl.question(`${questionText} ${suffix} `);
|
|
1204
|
+
const normalized = answer.trim().toLowerCase();
|
|
1205
|
+
|
|
1206
|
+
if (!normalized) {
|
|
1207
|
+
return defaultValue;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return normalized === 'y' || normalized === 'yes';
|
|
1211
|
+
} finally {
|
|
1212
|
+
rl.close();
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
async function askChoice(questionText, choices, defaultIndex = 0) {
|
|
1217
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1218
|
+
return choices[defaultIndex];
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const rl = readline.createInterface({
|
|
1222
|
+
input: process.stdin,
|
|
1223
|
+
output: process.stdout
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
const lines = choices.map((choice, index) => `${index + 1}. ${choice}`);
|
|
1228
|
+
const answer = await rl.question(`${questionText}\n${lines.join('\n')}\nSelect option [${defaultIndex + 1}]: `);
|
|
1229
|
+
const trimmed = answer.trim();
|
|
1230
|
+
if (!trimmed) {
|
|
1231
|
+
return choices[defaultIndex];
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const numeric = Number.parseInt(trimmed, 10);
|
|
1235
|
+
if (Number.isNaN(numeric) || numeric < 1 || numeric > choices.length) {
|
|
1236
|
+
return choices[defaultIndex];
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return choices[numeric - 1];
|
|
1240
|
+
} finally {
|
|
1241
|
+
rl.close();
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function mergeSummary(target, source) {
|
|
1246
|
+
target.createdFiles.push(...source.createdFiles);
|
|
1247
|
+
target.overwrittenFiles.push(...source.overwrittenFiles);
|
|
1248
|
+
target.skippedFiles.push(...source.skippedFiles);
|
|
1249
|
+
target.updatedScriptKeys.push(...source.updatedScriptKeys);
|
|
1250
|
+
target.skippedScriptKeys.push(...source.skippedScriptKeys);
|
|
1251
|
+
target.removedScriptKeys.push(...source.removedScriptKeys);
|
|
1252
|
+
target.updatedDependencyKeys.push(...source.updatedDependencyKeys);
|
|
1253
|
+
target.skippedDependencyKeys.push(...source.skippedDependencyKeys);
|
|
1254
|
+
target.warnings.push(...source.warnings);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function createOrchestrationSummary() {
|
|
1258
|
+
return {
|
|
1259
|
+
modeDetected: '',
|
|
1260
|
+
repoResolved: '',
|
|
1261
|
+
branchPushed: '',
|
|
1262
|
+
prAction: '',
|
|
1263
|
+
prUrl: '',
|
|
1264
|
+
autoMerge: '',
|
|
1265
|
+
checks: '',
|
|
1266
|
+
merge: '',
|
|
1267
|
+
releasePr: '',
|
|
1268
|
+
releaseTrack: '',
|
|
1269
|
+
promotionWorkflow: '',
|
|
1270
|
+
promotionPr: '',
|
|
1271
|
+
npmValidation: '',
|
|
1272
|
+
cleanup: '',
|
|
1273
|
+
actionsPerformed: [],
|
|
1274
|
+
actionsSkipped: [],
|
|
1275
|
+
warnings: [],
|
|
1276
|
+
errors: []
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function printOrchestrationSummary(title, summary) {
|
|
1281
|
+
const unique = (values) => [...new Set(values)];
|
|
1282
|
+
const formatList = (values) => {
|
|
1283
|
+
const normalized = unique(values || []);
|
|
1284
|
+
if (!normalized.length) {
|
|
1285
|
+
return [' - none'];
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return normalized.map((item) => ` - ${item}`);
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
console.log(title);
|
|
1292
|
+
console.log('');
|
|
1293
|
+
console.log('Preflight');
|
|
1294
|
+
console.log(` - mode detected: ${summary.modeDetected || 'n/a'}`);
|
|
1295
|
+
console.log(` - repo resolved: ${summary.repoResolved || 'n/a'}`);
|
|
1296
|
+
console.log('');
|
|
1297
|
+
console.log('Plan');
|
|
1298
|
+
console.log(` - branch pushed: ${summary.branchPushed || 'n/a'}`);
|
|
1299
|
+
console.log(` - pr created/updated/skipped: ${summary.prAction || 'n/a'}`);
|
|
1300
|
+
console.log(` - pr url: ${summary.prUrl || 'n/a'}`);
|
|
1301
|
+
console.log('');
|
|
1302
|
+
console.log('Apply');
|
|
1303
|
+
console.log(` - auto-merge enabled/skipped: ${summary.autoMerge || 'n/a'}`);
|
|
1304
|
+
console.log(` - checks watched result: ${summary.checks || 'n/a'}`);
|
|
1305
|
+
console.log(` - merge performed/skipped: ${summary.merge || 'n/a'}`);
|
|
1306
|
+
console.log(` - release pr discovered/merged: ${summary.releasePr || 'n/a'}`);
|
|
1307
|
+
console.log(` - release track: ${summary.releaseTrack || 'n/a'}`);
|
|
1308
|
+
console.log(` - promotion workflow run: ${summary.promotionWorkflow || 'n/a'}`);
|
|
1309
|
+
console.log(` - promotion PR: ${summary.promotionPr || 'n/a'}`);
|
|
1310
|
+
console.log(` - npm validation: ${summary.npmValidation || 'n/a'}`);
|
|
1311
|
+
console.log(` - cleanup: ${summary.cleanup || 'n/a'}`);
|
|
1312
|
+
console.log('actions performed:');
|
|
1313
|
+
formatList(summary.actionsPerformed).forEach((line) => console.log(line));
|
|
1314
|
+
console.log('actions skipped:');
|
|
1315
|
+
formatList(summary.actionsSkipped).forEach((line) => console.log(line));
|
|
1316
|
+
console.log('');
|
|
1317
|
+
console.log('Summary');
|
|
1318
|
+
console.log('warnings:');
|
|
1319
|
+
formatList(summary.warnings).forEach((line) => console.log(line));
|
|
1320
|
+
console.log('errors:');
|
|
1321
|
+
formatList(summary.errors).forEach((line) => console.log(line));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function parseJsonSafely(raw, fallback = {}) {
|
|
1325
|
+
try {
|
|
1326
|
+
return JSON.parse(raw);
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
return fallback;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function sleepMs(milliseconds) {
|
|
1333
|
+
const shared = new SharedArrayBuffer(4);
|
|
1334
|
+
const view = new Int32Array(shared);
|
|
1335
|
+
Atomics.wait(view, 0, 0, milliseconds);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function nowMs(deps) {
|
|
1339
|
+
if (deps && typeof deps.now === 'function') {
|
|
1340
|
+
return Number(deps.now());
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
return Date.now();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function waitForNextPoll(timeoutAt, defaultIntervalMs, deps) {
|
|
1347
|
+
const remainingMs = Math.max(0, timeoutAt - nowMs(deps));
|
|
1348
|
+
if (remainingMs <= 0) {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const pollMs = Math.max(100, Math.min(defaultIntervalMs, remainingMs));
|
|
1353
|
+
if (deps && typeof deps.sleep === 'function') {
|
|
1354
|
+
deps.sleep(pollMs);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
sleepMs(pollMs);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function resolveGitContext(args, deps) {
|
|
1362
|
+
const insideWorkTree = deps.exec('git', ['rev-parse', '--is-inside-work-tree']);
|
|
1363
|
+
if (insideWorkTree.status !== 0 || insideWorkTree.stdout.trim() !== 'true') {
|
|
1364
|
+
throw new Error('Current directory is not a git repository.');
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const headBranch = args.head || deps.exec('git', ['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
|
1368
|
+
if (!headBranch || headBranch === 'HEAD') {
|
|
1369
|
+
throw new Error('Detached HEAD is not supported. Checkout a branch and rerun.');
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const repo = resolveRepo({ repo: args.repo }, deps);
|
|
1373
|
+
const baseBranch = args.base || (headBranch === DEFAULT_BETA_BRANCH ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH);
|
|
1374
|
+
const latestTitleResult = deps.exec('git', ['log', '-1', '--pretty=%s']);
|
|
1375
|
+
const latestTitle = latestTitleResult.status === 0 ? latestTitleResult.stdout.trim() : '';
|
|
1376
|
+
const title = args.title || latestTitle || headBranch;
|
|
1377
|
+
|
|
1378
|
+
return {
|
|
1379
|
+
repo,
|
|
1380
|
+
head: headBranch,
|
|
1381
|
+
base: baseBranch,
|
|
1382
|
+
title
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function getRecentCommitSubjects(deps, count = 10) {
|
|
1387
|
+
const result = deps.exec('git', ['log', `-n${count}`, '--pretty=%h %s']);
|
|
1388
|
+
if (result.status !== 0) {
|
|
1389
|
+
return [];
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
return result.stdout
|
|
1393
|
+
.split('\n')
|
|
1394
|
+
.map((line) => line.trim())
|
|
1395
|
+
.filter(Boolean);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function collectChangesetPackages(cwd) {
|
|
1399
|
+
const changesetDir = path.join(cwd, '.changeset');
|
|
1400
|
+
if (!fs.existsSync(changesetDir)) {
|
|
1401
|
+
return [];
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const files = fs.readdirSync(changesetDir)
|
|
1405
|
+
.filter((name) => name.endsWith('.md'));
|
|
1406
|
+
const packages = new Set();
|
|
1407
|
+
|
|
1408
|
+
for (const fileName of files) {
|
|
1409
|
+
const fullPath = path.join(changesetDir, fileName);
|
|
1410
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
1411
|
+
const parts = content.split('---');
|
|
1412
|
+
if (parts.length < 3) {
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const frontmatter = parts[1];
|
|
1417
|
+
const lines = frontmatter.split('\n');
|
|
1418
|
+
for (const line of lines) {
|
|
1419
|
+
const match = line.match(/"([^"]+)"\s*:/);
|
|
1420
|
+
if (match) {
|
|
1421
|
+
packages.add(match[1]);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
return [...packages];
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function renderPrBodyDeterministic(context, deps, options = {}) {
|
|
1430
|
+
const commits = getRecentCommitSubjects(deps, 10);
|
|
1431
|
+
const changedPackages = collectChangesetPackages(options.cwd || process.cwd());
|
|
1432
|
+
const summaryBlock = [
|
|
1433
|
+
'## Summary',
|
|
1434
|
+
`- Source branch: \`${context.head}\``,
|
|
1435
|
+
`- Target branch: \`${context.base}\``,
|
|
1436
|
+
''
|
|
1437
|
+
].join('\n');
|
|
1438
|
+
const changesBlock = [
|
|
1439
|
+
'## Changes',
|
|
1440
|
+
...(commits.length ? commits.map((item) => `- ${item}`) : ['- No recent commit messages found.']),
|
|
1441
|
+
''
|
|
1442
|
+
].join('\n');
|
|
1443
|
+
const releaseImpactBlock = [
|
|
1444
|
+
'## Release Impact',
|
|
1445
|
+
...(changedPackages.length
|
|
1446
|
+
? changedPackages.map((name) => `- Changeset package: \`${name}\``)
|
|
1447
|
+
: ['- No explicit package entries found in `.changeset/*.md`.']),
|
|
1448
|
+
''
|
|
1449
|
+
].join('\n');
|
|
1450
|
+
const validationBlock = [
|
|
1451
|
+
'## Validation',
|
|
1452
|
+
'- [ ] `npm run check`',
|
|
1453
|
+
'- [ ] CI green',
|
|
1454
|
+
''
|
|
1455
|
+
].join('\n');
|
|
1456
|
+
const checklistBlock = [
|
|
1457
|
+
'## Checklist',
|
|
1458
|
+
'- [ ] Scope and risks reviewed',
|
|
1459
|
+
'- [ ] Release impact reviewed',
|
|
1460
|
+
'- [ ] Ready to merge',
|
|
1461
|
+
''
|
|
1462
|
+
].join('\n');
|
|
1463
|
+
const generated = [summaryBlock, changesBlock, releaseImpactBlock, validationBlock, checklistBlock].join('\n');
|
|
1464
|
+
|
|
1465
|
+
if (options.body) {
|
|
1466
|
+
return options.body;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (options.bodyFile) {
|
|
1470
|
+
const fullPath = path.resolve(options.cwd || process.cwd(), options.bodyFile);
|
|
1471
|
+
return fs.readFileSync(fullPath, 'utf8');
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const templatePath = options.template
|
|
1475
|
+
? path.resolve(options.cwd || process.cwd(), options.template)
|
|
1476
|
+
: path.resolve(options.cwd || process.cwd(), '.github/PULL_REQUEST_TEMPLATE.md');
|
|
1477
|
+
if (fs.existsSync(templatePath)) {
|
|
1478
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
1479
|
+
if (templateContent.includes('<!-- GENERATED_PR_BODY -->')) {
|
|
1480
|
+
return templateContent.replace('<!-- GENERATED_PR_BODY -->', generated);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
return `${templateContent.trim()}\n\n---\n\n${generated}`;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
return generated;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function listOpenPullRequests(repo, deps) {
|
|
1490
|
+
const list = deps.exec('gh', ['pr', 'list', '--repo', repo, '--state', 'open', '--json', 'number,url,title,headRefName,baseRefName']);
|
|
1491
|
+
if (list.status !== 0) {
|
|
1492
|
+
throw new Error(`Failed to list pull requests: ${list.stderr || list.stdout}`.trim());
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const parsed = parseJsonSafely(list.stdout || '[]', []);
|
|
1496
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function findOpenPrByHeadBase(repo, head, base, deps) {
|
|
1500
|
+
const prs = listOpenPullRequests(repo, deps);
|
|
1501
|
+
return prs.find((item) => item.headRefName === head && item.baseRefName === base) || null;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function ensureBranchPushed(repo, head, deps) {
|
|
1505
|
+
const upstream = deps.exec('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
1506
|
+
const hasUpstream = upstream.status === 0;
|
|
1507
|
+
if (hasUpstream) {
|
|
1508
|
+
const ahead = deps.exec('git', ['rev-list', '--count', '@{u}..HEAD']);
|
|
1509
|
+
const aheadCount = ahead.status === 0 ? Number.parseInt(ahead.stdout.trim(), 10) : 0;
|
|
1510
|
+
const push = deps.exec('git', ['push']);
|
|
1511
|
+
if (push.status !== 0) {
|
|
1512
|
+
throw new Error(`Failed to push branch "${head}": ${push.stderr || push.stdout}`.trim());
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
return {
|
|
1516
|
+
status: aheadCount > 0 ? 'updated' : 'up-to-date',
|
|
1517
|
+
hasUpstream: true
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const push = deps.exec('git', ['push', '--set-upstream', 'origin', head]);
|
|
1522
|
+
if (push.status !== 0) {
|
|
1523
|
+
throw new Error(`Failed to push branch "${head}" with upstream: ${push.stderr || push.stdout}`.trim());
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
return {
|
|
1527
|
+
status: 'upstream-set',
|
|
1528
|
+
hasUpstream: false
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function createOrUpdatePr(context, body, args, deps) {
|
|
1533
|
+
const existing = findOpenPrByHeadBase(context.repo, context.head, context.base, deps);
|
|
1534
|
+
if (existing && !args.updateExistingPr) {
|
|
1535
|
+
return {
|
|
1536
|
+
action: 'reused',
|
|
1537
|
+
number: existing.number,
|
|
1538
|
+
url: existing.url
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const bodyFilePath = path.join(process.cwd(), `.tmp-pr-body-${Date.now()}.md`);
|
|
1543
|
+
fs.writeFileSync(bodyFilePath, body);
|
|
1544
|
+
|
|
1545
|
+
try {
|
|
1546
|
+
if (existing) {
|
|
1547
|
+
const editArgs = ['pr', 'edit', String(existing.number), '--repo', context.repo, '--title', context.title, '--body-file', bodyFilePath];
|
|
1548
|
+
const edit = deps.exec('gh', editArgs);
|
|
1549
|
+
if (edit.status !== 0) {
|
|
1550
|
+
throw new Error(`Failed to update PR #${existing.number}: ${edit.stderr || edit.stdout}`.trim());
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return {
|
|
1554
|
+
action: 'updated',
|
|
1555
|
+
number: existing.number,
|
|
1556
|
+
url: existing.url
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const createArgs = ['pr', 'create', '--repo', context.repo, '--head', context.head, '--base', context.base, '--title', context.title, '--body-file', bodyFilePath];
|
|
1561
|
+
if (args.draft) {
|
|
1562
|
+
createArgs.push('--draft');
|
|
1563
|
+
}
|
|
1564
|
+
const create = deps.exec('gh', createArgs);
|
|
1565
|
+
if (create.status !== 0) {
|
|
1566
|
+
throw new Error(`Failed to create PR: ${create.stderr || create.stdout}`.trim());
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const url = (create.stdout || '').trim().split('\n').find((line) => line.includes('http')) || '';
|
|
1570
|
+
const created = findOpenPrByHeadBase(context.repo, context.head, context.base, deps);
|
|
1571
|
+
return {
|
|
1572
|
+
action: 'created',
|
|
1573
|
+
number: created ? created.number : 0,
|
|
1574
|
+
url: url || (created ? created.url : '')
|
|
1575
|
+
};
|
|
1576
|
+
} finally {
|
|
1577
|
+
if (fs.existsSync(bodyFilePath)) {
|
|
1578
|
+
fs.unlinkSync(bodyFilePath);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function enablePrAutoMerge(repo, prNumber, mergeMethod, deps) {
|
|
1584
|
+
const methodFlag = mergeMethod === 'merge'
|
|
1585
|
+
? '--merge'
|
|
1586
|
+
: mergeMethod === 'rebase'
|
|
1587
|
+
? '--rebase'
|
|
1588
|
+
: '--squash';
|
|
1589
|
+
const result = deps.exec('gh', ['pr', 'merge', String(prNumber), '--repo', repo, methodFlag, '--auto']);
|
|
1590
|
+
if (result.status !== 0) {
|
|
1591
|
+
throw new Error(`Failed to enable auto-merge for PR #${prNumber}: ${result.stderr || result.stdout}`.trim());
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
function getPrCheckState(repo, prNumber, deps) {
|
|
1596
|
+
const result = deps.exec('gh', ['pr', 'view', String(prNumber), '--repo', repo, '--json', 'statusCheckRollup,url,number']);
|
|
1597
|
+
if (result.status !== 0) {
|
|
1598
|
+
throw new Error(`Failed to inspect PR #${prNumber} checks: ${result.stderr || result.stdout}`.trim());
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const parsed = parseJsonSafely(result.stdout || '{}', {});
|
|
1602
|
+
const rollup = Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : [];
|
|
1603
|
+
let pending = 0;
|
|
1604
|
+
let failed = 0;
|
|
1605
|
+
|
|
1606
|
+
for (const item of rollup) {
|
|
1607
|
+
const rawState = String(item.conclusion || item.state || item.status || '').toUpperCase();
|
|
1608
|
+
if (!rawState || rawState === 'EXPECTED' || rawState === 'PENDING' || rawState === 'IN_PROGRESS' || rawState === 'QUEUED' || rawState === 'WAITING') {
|
|
1609
|
+
pending += 1;
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
if (rawState === 'SUCCESS' || rawState === 'NEUTRAL' || rawState === 'SKIPPED') {
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
failed += 1;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
return {
|
|
1621
|
+
pending,
|
|
1622
|
+
failed,
|
|
1623
|
+
total: rollup.length
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function watchPrChecks(repo, prNumber, timeoutMinutes, deps) {
|
|
1628
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1629
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1630
|
+
const state = getPrCheckState(repo, prNumber, deps);
|
|
1631
|
+
if (state.failed > 0) {
|
|
1632
|
+
throw new Error(`PR #${prNumber} has failing required checks.`);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (state.pending === 0) {
|
|
1636
|
+
return 'green';
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
throw new Error(`Timed out waiting for checks on PR #${prNumber} after ${timeoutMinutes} minutes.`);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function mergePrWhenGreen(repo, prNumber, mergeMethod, deps) {
|
|
1646
|
+
const methodFlag = mergeMethod === 'merge'
|
|
1647
|
+
? '--merge'
|
|
1648
|
+
: mergeMethod === 'rebase'
|
|
1649
|
+
? '--rebase'
|
|
1650
|
+
: '--squash';
|
|
1651
|
+
const merge = deps.exec('gh', ['pr', 'merge', String(prNumber), '--repo', repo, methodFlag, '--delete-branch']);
|
|
1652
|
+
if (merge.status !== 0) {
|
|
1653
|
+
throw new Error(`Failed to merge PR #${prNumber}: ${merge.stderr || merge.stdout}`.trim());
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function getPrMergeReadiness(repo, prNumber, deps) {
|
|
1658
|
+
const view = deps.exec('gh', [
|
|
1659
|
+
'pr',
|
|
1660
|
+
'view',
|
|
1661
|
+
String(prNumber),
|
|
1662
|
+
'--repo',
|
|
1663
|
+
repo,
|
|
1664
|
+
'--json',
|
|
1665
|
+
'number,url,reviewDecision,mergeStateStatus,isDraft,headRefName'
|
|
1666
|
+
]);
|
|
1667
|
+
if (view.status !== 0) {
|
|
1668
|
+
throw new Error(`Failed to inspect merge readiness for PR #${prNumber}: ${view.stderr || view.stdout}`.trim());
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
const parsed = parseJsonSafely(view.stdout || '{}', {});
|
|
1672
|
+
return {
|
|
1673
|
+
number: parsed.number || prNumber,
|
|
1674
|
+
url: parsed.url || '',
|
|
1675
|
+
reviewDecision: String(parsed.reviewDecision || '').toUpperCase(),
|
|
1676
|
+
mergeStateStatus: String(parsed.mergeStateStatus || '').toUpperCase(),
|
|
1677
|
+
isDraft: Boolean(parsed.isDraft),
|
|
1678
|
+
headRefName: String(parsed.headRefName || '')
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function getLatestWorkflowRunForBranch(repo, branch, deps) {
|
|
1683
|
+
if (!branch) {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const runs = deps.exec('gh', [
|
|
1688
|
+
'run',
|
|
1689
|
+
'list',
|
|
1690
|
+
'--repo',
|
|
1691
|
+
repo,
|
|
1692
|
+
'--branch',
|
|
1693
|
+
branch,
|
|
1694
|
+
'--json',
|
|
1695
|
+
'databaseId,workflowName,status,conclusion,url',
|
|
1696
|
+
'--limit',
|
|
1697
|
+
'10'
|
|
1698
|
+
]);
|
|
1699
|
+
if (runs.status !== 0) {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
const parsed = parseJsonSafely(runs.stdout || '[]', []);
|
|
1704
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
return parsed[0];
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function waitForPrMergeReadinessOrThrow(repo, prNumber, label, timeoutMinutes, deps, options = {}) {
|
|
1712
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1713
|
+
let lastReadiness = null;
|
|
1714
|
+
let lastChecks = null;
|
|
1715
|
+
const allowBehindTransient = Boolean(options.allowBehindTransient);
|
|
1716
|
+
let behindObservedAt = 0;
|
|
1717
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1718
|
+
const mergeState = getPrMergeState(repo, prNumber, deps);
|
|
1719
|
+
if (mergeState.state === 'MERGED' || mergeState.mergedAt) {
|
|
1720
|
+
return {
|
|
1721
|
+
number: prNumber,
|
|
1722
|
+
url: '',
|
|
1723
|
+
reviewDecision: 'APPROVED',
|
|
1724
|
+
mergeStateStatus: 'MERGED',
|
|
1725
|
+
isDraft: false
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const readiness = getPrMergeReadiness(repo, prNumber, deps);
|
|
1730
|
+
const checks = getPrCheckState(repo, prNumber, deps);
|
|
1731
|
+
lastReadiness = readiness;
|
|
1732
|
+
lastChecks = checks;
|
|
1733
|
+
|
|
1734
|
+
if (readiness.isDraft) {
|
|
1735
|
+
throw new Error(`${label} is still a draft PR. Mark it ready for review before merge.`);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (readiness.reviewDecision === 'REVIEW_REQUIRED' || readiness.reviewDecision === 'CHANGES_REQUESTED') {
|
|
1739
|
+
throw new Error(
|
|
1740
|
+
[
|
|
1741
|
+
`${label} still requires review approval before merge.`,
|
|
1742
|
+
`reviewDecision: ${readiness.reviewDecision}`,
|
|
1743
|
+
readiness.url ? `PR: ${readiness.url}` : ''
|
|
1744
|
+
].filter(Boolean).join('\n')
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
if (checks.failed > 0) {
|
|
1749
|
+
throw new Error(`${label} has failing required checks.`);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (readiness.mergeStateStatus === 'DIRTY') {
|
|
1753
|
+
throw new Error(
|
|
1754
|
+
[
|
|
1755
|
+
`${label} is not mergeable yet due to branch policy/state.`,
|
|
1756
|
+
`mergeStateStatus: ${readiness.mergeStateStatus}`,
|
|
1757
|
+
readiness.url ? `PR: ${readiness.url}` : ''
|
|
1758
|
+
].filter(Boolean).join('\n')
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (readiness.mergeStateStatus === 'BEHIND') {
|
|
1763
|
+
if (allowBehindTransient) {
|
|
1764
|
+
const workflowRun = getLatestWorkflowRunForBranch(repo, readiness.headRefName, deps);
|
|
1765
|
+
const runStatus = String((workflowRun && workflowRun.status) || '').toLowerCase();
|
|
1766
|
+
const runConclusion = String((workflowRun && workflowRun.conclusion) || '').toLowerCase();
|
|
1767
|
+
const runCompleted = runStatus === 'completed';
|
|
1768
|
+
const runFailed = runCompleted && !['success', 'neutral', 'skipped'].includes(runConclusion);
|
|
1769
|
+
const runInProgress = ['queued', 'in_progress', 'waiting', 'requested', 'pending'].includes(runStatus);
|
|
1770
|
+
|
|
1771
|
+
if (runFailed) {
|
|
1772
|
+
throw new Error(
|
|
1773
|
+
[
|
|
1774
|
+
`${label} stayed BEHIND because latest workflow run failed.`,
|
|
1775
|
+
`workflow: ${workflowRun.workflowName || 'unknown'}`,
|
|
1776
|
+
`status/conclusion: ${workflowRun.status || 'n/a'}/${workflowRun.conclusion || 'n/a'}`,
|
|
1777
|
+
workflowRun.url ? `run: ${workflowRun.url}` : '',
|
|
1778
|
+
readiness.url ? `PR: ${readiness.url}` : ''
|
|
1779
|
+
].filter(Boolean).join('\n')
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (!behindObservedAt) {
|
|
1784
|
+
behindObservedAt = nowMs(deps);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// Avoid waiting the full global timeout when there is no active workflow progress.
|
|
1788
|
+
const behindElapsedMs = nowMs(deps) - behindObservedAt;
|
|
1789
|
+
const stagnationWindowMs = 10 * 1000;
|
|
1790
|
+
if (!runInProgress && behindElapsedMs >= stagnationWindowMs) {
|
|
1791
|
+
throw new Error(
|
|
1792
|
+
[
|
|
1793
|
+
`${label} remained BEHIND for more than 10s with no active workflow progress.`,
|
|
1794
|
+
runCompleted ? `latest workflow conclusion: ${runConclusion || 'n/a'}` : 'latest workflow status: unavailable',
|
|
1795
|
+
workflowRun && workflowRun.url ? `run: ${workflowRun.url}` : '',
|
|
1796
|
+
readiness.url ? `PR: ${readiness.url}` : '',
|
|
1797
|
+
'If changeset workflow is still updating, rerun release-cycle in a moment.'
|
|
1798
|
+
].filter(Boolean).join('\n')
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
throw new Error(
|
|
1806
|
+
[
|
|
1807
|
+
`${label} is not mergeable yet due to branch policy/state.`,
|
|
1808
|
+
`mergeStateStatus: ${readiness.mergeStateStatus}`,
|
|
1809
|
+
readiness.url ? `PR: ${readiness.url}` : ''
|
|
1810
|
+
].filter(Boolean).join('\n')
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const mergeStateReady = readiness.mergeStateStatus === 'CLEAN'
|
|
1815
|
+
|| readiness.mergeStateStatus === 'HAS_HOOKS'
|
|
1816
|
+
|| readiness.mergeStateStatus === 'UNSTABLE';
|
|
1817
|
+
const mergeStateUnknown = !readiness.mergeStateStatus || readiness.mergeStateStatus === 'UNKNOWN' || readiness.mergeStateStatus === 'BLOCKED';
|
|
1818
|
+
if ((mergeStateReady && checks.pending === 0) || (mergeStateUnknown && checks.pending === 0 && !readiness.mergeStateStatus)) {
|
|
1819
|
+
return readiness;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
throw new Error(
|
|
1826
|
+
[
|
|
1827
|
+
`${label} did not become merge-ready after ${timeoutMinutes} minutes.`,
|
|
1828
|
+
`mergeStateStatus: ${lastReadiness ? (lastReadiness.mergeStateStatus || 'n/a') : 'n/a'}`,
|
|
1829
|
+
`reviewDecision: ${lastReadiness ? (lastReadiness.reviewDecision || 'n/a') : 'n/a'}`,
|
|
1830
|
+
`pending checks: ${lastChecks ? lastChecks.pending : 'n/a'}`,
|
|
1831
|
+
allowBehindTransient ? 'Hint: release PR can stay BEHIND while changeset workflow updates its branch. Wait for workflow completion and rerun if needed.' : '',
|
|
1832
|
+
lastReadiness && lastReadiness.url ? `PR: ${lastReadiness.url}` : ''
|
|
1833
|
+
].filter(Boolean).join('\n')
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
async function confirmMergeIfNeeded(args, readiness, label) {
|
|
1838
|
+
if (args.confirmMerges && !args.yes) {
|
|
1839
|
+
await confirmOrThrow(
|
|
1840
|
+
[
|
|
1841
|
+
`${label} is ready for merge.`,
|
|
1842
|
+
`reviewDecision: ${readiness.reviewDecision || 'n/a'}`,
|
|
1843
|
+
`mergeStateStatus: ${readiness.mergeStateStatus || 'n/a'}`,
|
|
1844
|
+
'Proceed with merge now?'
|
|
1845
|
+
].join('\n')
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function getPrMergeState(repo, prNumber, deps) {
|
|
1851
|
+
const view = deps.exec('gh', [
|
|
1852
|
+
'pr',
|
|
1853
|
+
'view',
|
|
1854
|
+
String(prNumber),
|
|
1855
|
+
'--repo',
|
|
1856
|
+
repo,
|
|
1857
|
+
'--json',
|
|
1858
|
+
'state,mergedAt'
|
|
1859
|
+
]);
|
|
1860
|
+
if (view.status !== 0) {
|
|
1861
|
+
throw new Error(`Failed to read PR #${prNumber} merge state: ${view.stderr || view.stdout}`.trim());
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const parsed = parseJsonSafely(view.stdout || '{}', {});
|
|
1865
|
+
return {
|
|
1866
|
+
state: String(parsed.state || '').toUpperCase(),
|
|
1867
|
+
mergedAt: parsed.mergedAt || ''
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function waitForPrMerged(repo, prNumber, timeoutMinutes, deps) {
|
|
1872
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1873
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1874
|
+
const state = getPrMergeState(repo, prNumber, deps);
|
|
1875
|
+
if (state.state === 'MERGED' || state.mergedAt) {
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
if (state.state === 'CLOSED') {
|
|
1879
|
+
throw new Error(`PR #${prNumber} was closed without merge.`);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
throw new Error(`Timed out waiting for PR #${prNumber} merge after ${timeoutMinutes} minutes.`);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function releaseBaseBranchForTrack(track) {
|
|
1889
|
+
return track === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function findReleasePrs(repo, deps, options = {}) {
|
|
1893
|
+
const expectedBase = options.expectedBase || '';
|
|
1894
|
+
const prs = listOpenPullRequests(repo, deps);
|
|
1895
|
+
return prs.filter(
|
|
1896
|
+
(item) => item.headRefName
|
|
1897
|
+
&& item.headRefName.startsWith('changeset-release/')
|
|
1898
|
+
&& (expectedBase
|
|
1899
|
+
? item.baseRefName === expectedBase
|
|
1900
|
+
: (item.baseRefName === DEFAULT_BASE_BRANCH || item.baseRefName === DEFAULT_BETA_BRANCH))
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function waitForReleasePr(repo, timeoutMinutes, deps, options = {}) {
|
|
1905
|
+
const expectedBase = options.expectedBase || '';
|
|
1906
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1907
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1908
|
+
const releasePrs = findReleasePrs(repo, deps, { expectedBase });
|
|
1909
|
+
if (releasePrs.length === 1) {
|
|
1910
|
+
return releasePrs[0];
|
|
1911
|
+
}
|
|
1912
|
+
if (releasePrs.length > 1) {
|
|
1913
|
+
throw new Error(`Multiple release PRs detected: ${releasePrs.map((item) => item.url).join(', ')}`);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const baseHint = expectedBase ? ` targeting ${expectedBase}` : '';
|
|
1920
|
+
throw new Error(`Timed out waiting for release PR${baseHint} after ${timeoutMinutes} minutes.`);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
async function confirmDetectedModeIfNeeded(args, mode, planText) {
|
|
1924
|
+
if (args.yes) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
await confirmOrThrow(
|
|
1929
|
+
[
|
|
1930
|
+
`release-cycle detected mode: ${mode}`,
|
|
1931
|
+
planText
|
|
1932
|
+
].join('\n')
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function ghApiJson(deps, method, endpoint, payload) {
|
|
1937
|
+
const result = ghApi(deps, method, endpoint, payload);
|
|
1938
|
+
if (result.status !== 0) {
|
|
1939
|
+
throw new Error(`GitHub API ${method} ${endpoint} failed: ${result.stderr || result.stdout}`.trim());
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
return parseJsonSafely(result.stdout || '{}', {});
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
function dispatchPromoteStableWorkflow(repo, args, deps) {
|
|
1946
|
+
const endpoint = `/repos/${repo}/actions/workflows/${encodeURIComponent(DEFAULT_PROMOTE_WORKFLOW)}/dispatches`;
|
|
1947
|
+
const payload = {
|
|
1948
|
+
ref: args.head || DEFAULT_BETA_BRANCH,
|
|
1949
|
+
inputs: {
|
|
1950
|
+
promote_type: args.promoteType,
|
|
1951
|
+
summary: args.promoteSummary,
|
|
1952
|
+
target_beta_branch: DEFAULT_BETA_BRANCH
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
ghApiJson(deps, 'POST', endpoint, payload);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function findPromotionPrs(repo, deps) {
|
|
1959
|
+
const prs = listOpenPullRequests(repo, deps);
|
|
1960
|
+
return prs.filter(
|
|
1961
|
+
(item) => item.baseRefName === DEFAULT_BETA_BRANCH
|
|
1962
|
+
&& typeof item.headRefName === 'string'
|
|
1963
|
+
&& item.headRefName.startsWith('promote/stable-')
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function waitForPromotionPr(repo, timeoutMinutes, deps) {
|
|
1968
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1969
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1970
|
+
const promotionPrs = findPromotionPrs(repo, deps);
|
|
1971
|
+
if (promotionPrs.length === 1) {
|
|
1972
|
+
return promotionPrs[0];
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
if (promotionPrs.length > 1) {
|
|
1976
|
+
promotionPrs.sort((a, b) => b.number - a.number);
|
|
1977
|
+
return promotionPrs[0];
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
throw new Error(`Timed out waiting for promotion PR after ${timeoutMinutes} minutes.`);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function getRemotePackageVersion(repo, ref, deps) {
|
|
1987
|
+
const endpoint = `/repos/${repo}/contents/package.json?ref=${encodeURIComponent(ref)}`;
|
|
1988
|
+
const contentResponse = ghApiJson(deps, 'GET', endpoint);
|
|
1989
|
+
if (!contentResponse.content) {
|
|
1990
|
+
throw new Error(`Could not read package.json content from ${repo}@${ref}.`);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const decoded = Buffer.from(String(contentResponse.content).replace(/\n/g, ''), 'base64').toString('utf8');
|
|
1994
|
+
const parsed = parseJsonSafely(decoded, {});
|
|
1995
|
+
if (!parsed.name || !parsed.version) {
|
|
1996
|
+
throw new Error(`package.json from ${repo}@${ref} must include name and version.`);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
return {
|
|
2000
|
+
name: parsed.name,
|
|
2001
|
+
version: parsed.version
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function validateNpmPublishedVersionAndTag(packageName, expectedVersion, expectedTag, timeoutMinutes, deps) {
|
|
2006
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
2007
|
+
let lastObservedVersion = '';
|
|
2008
|
+
let lastObservedTagVersion = '';
|
|
2009
|
+
|
|
2010
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
2011
|
+
const versionResult = deps.exec('npm', ['view', packageName, 'version', '--json']);
|
|
2012
|
+
const tagsResult = deps.exec('npm', ['view', packageName, 'dist-tags', '--json']);
|
|
2013
|
+
if (versionResult.status === 0 && tagsResult.status === 0) {
|
|
2014
|
+
const observedVersion = String(parseJsonSafely(versionResult.stdout || '""', '') || '');
|
|
2015
|
+
const tags = parseJsonSafely(tagsResult.stdout || '{}', {});
|
|
2016
|
+
const observedTagVersion = tags && tags[expectedTag] ? String(tags[expectedTag]) : '';
|
|
2017
|
+
lastObservedVersion = observedVersion;
|
|
2018
|
+
lastObservedTagVersion = observedTagVersion;
|
|
2019
|
+
|
|
2020
|
+
if (observedVersion === expectedVersion && observedTagVersion === expectedVersion) {
|
|
2021
|
+
return {
|
|
2022
|
+
status: 'pass',
|
|
2023
|
+
observedVersion,
|
|
2024
|
+
observedTagVersion
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
waitForNextPoll(timeoutAt, 10000, deps);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
return {
|
|
2033
|
+
status: 'timeout',
|
|
2034
|
+
observedVersion: lastObservedVersion,
|
|
2035
|
+
observedTagVersion: lastObservedTagVersion
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function ensureWorkingTreeClean(deps) {
|
|
2040
|
+
const status = deps.exec('git', ['status', '--porcelain']);
|
|
2041
|
+
if (status.status !== 0) {
|
|
2042
|
+
throw new Error('Failed to inspect working tree status.');
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
return status.stdout.trim() === '';
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function isProtectedOrGeneratedBranch(branchName) {
|
|
2049
|
+
if (!branchName) {
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
return branchName === DEFAULT_BASE_BRANCH
|
|
2054
|
+
|| branchName === DEFAULT_BETA_BRANCH
|
|
2055
|
+
|| branchName.startsWith('changeset-release/')
|
|
2056
|
+
|| branchName.startsWith('promote/');
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
function isCleanupCandidateBranch(branchName) {
|
|
2060
|
+
if (!branchName) {
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
return /^(feat|fix|chore|refactor|test)\//.test(branchName);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
function syncBranchWithBase({
|
|
2068
|
+
deps,
|
|
2069
|
+
headBranch,
|
|
2070
|
+
baseBranch,
|
|
2071
|
+
strategy,
|
|
2072
|
+
reporter,
|
|
2073
|
+
summary,
|
|
2074
|
+
dryRun
|
|
2075
|
+
}) {
|
|
2076
|
+
if (strategy === 'off') {
|
|
2077
|
+
summary.actionsSkipped.push('sync base branch');
|
|
2078
|
+
return {
|
|
2079
|
+
synchronized: false,
|
|
2080
|
+
wasBehind: false
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
reporter.start('release-cycle-sync-fetch', `Fetching origin/${baseBranch}...`);
|
|
2085
|
+
const fetch = deps.exec('git', ['fetch', 'origin', baseBranch]);
|
|
2086
|
+
if (fetch.status !== 0) {
|
|
2087
|
+
throw new Error(`Failed to fetch origin/${baseBranch}: ${(fetch.stderr || fetch.stdout || '').trim()}`);
|
|
2088
|
+
}
|
|
2089
|
+
reporter.ok('release-cycle-sync-fetch', `Fetched origin/${baseBranch}.`);
|
|
2090
|
+
|
|
2091
|
+
const behindCheck = deps.exec('git', ['rev-list', '--left-right', '--count', `${headBranch}...origin/${baseBranch}`]);
|
|
2092
|
+
if (behindCheck.status !== 0) {
|
|
2093
|
+
throw new Error(`Failed to compare ${headBranch} against origin/${baseBranch}.`);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
const parts = (behindCheck.stdout || '').trim().split(/\s+/);
|
|
2097
|
+
const behindCount = Number.parseInt(parts[1] || '0', 10);
|
|
2098
|
+
const isBehind = Number.isInteger(behindCount) && behindCount > 0;
|
|
2099
|
+
if (!isBehind) {
|
|
2100
|
+
summary.actionsPerformed.push(`sync base: ${headBranch} already up to date with origin/${baseBranch}`);
|
|
2101
|
+
return {
|
|
2102
|
+
synchronized: true,
|
|
2103
|
+
wasBehind: false
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
const effectiveStrategy = strategy === 'auto' ? 'rebase' : strategy;
|
|
2108
|
+
if (dryRun) {
|
|
2109
|
+
summary.actionsPerformed.push(`dry-run: would ${effectiveStrategy} ${headBranch} onto origin/${baseBranch}`);
|
|
2110
|
+
return {
|
|
2111
|
+
synchronized: false,
|
|
2112
|
+
wasBehind: true
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (effectiveStrategy === 'rebase') {
|
|
2117
|
+
reporter.start('release-cycle-sync-rebase', `Rebasing ${headBranch} onto origin/${baseBranch}...`);
|
|
2118
|
+
const rebase = deps.exec('git', ['rebase', `origin/${baseBranch}`]);
|
|
2119
|
+
if (rebase.status !== 0) {
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
[
|
|
2122
|
+
`Rebase failed while syncing ${headBranch} with origin/${baseBranch}.`,
|
|
2123
|
+
'Resolve conflicts, then run `git rebase --continue` or `git rebase --abort`.',
|
|
2124
|
+
(rebase.stderr || rebase.stdout || '').trim()
|
|
2125
|
+
].filter(Boolean).join('\n')
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
reporter.ok('release-cycle-sync-rebase', `${headBranch} rebased onto origin/${baseBranch}.`);
|
|
2129
|
+
summary.actionsPerformed.push(`sync base: rebased ${headBranch} onto origin/${baseBranch}`);
|
|
2130
|
+
return {
|
|
2131
|
+
synchronized: true,
|
|
2132
|
+
wasBehind: true
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
reporter.start('release-cycle-sync-merge', `Merging origin/${baseBranch} into ${headBranch}...`);
|
|
2137
|
+
const merge = deps.exec('git', ['merge', '--no-edit', `origin/${baseBranch}`]);
|
|
2138
|
+
if (merge.status !== 0) {
|
|
2139
|
+
throw new Error(
|
|
2140
|
+
[
|
|
2141
|
+
`Merge failed while syncing ${headBranch} with origin/${baseBranch}.`,
|
|
2142
|
+
'Resolve conflicts and commit merge before rerunning.',
|
|
2143
|
+
(merge.stderr || merge.stdout || '').trim()
|
|
2144
|
+
].filter(Boolean).join('\n')
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
reporter.ok('release-cycle-sync-merge', `Merged origin/${baseBranch} into ${headBranch}.`);
|
|
2148
|
+
summary.actionsPerformed.push(`sync base: merged origin/${baseBranch} into ${headBranch}`);
|
|
2149
|
+
return {
|
|
2150
|
+
synchronized: true,
|
|
2151
|
+
wasBehind: true
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function isHeadIntegratedIntoBase(headRef, baseBranch, deps) {
|
|
2156
|
+
const fetch = deps.exec('git', ['fetch', 'origin', baseBranch]);
|
|
2157
|
+
if (fetch.status !== 0) {
|
|
2158
|
+
return false;
|
|
2159
|
+
}
|
|
2160
|
+
const ancestor = deps.exec('git', ['merge-base', '--is-ancestor', headRef, `origin/${baseBranch}`]);
|
|
2161
|
+
return ancestor.status === 0;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function runLocalCleanup({
|
|
2165
|
+
deps,
|
|
2166
|
+
originalBranch,
|
|
2167
|
+
targetBaseBranch,
|
|
2168
|
+
shouldRun,
|
|
2169
|
+
summary,
|
|
2170
|
+
reporter
|
|
2171
|
+
}) {
|
|
2172
|
+
if (!shouldRun) {
|
|
2173
|
+
summary.actionsSkipped.push('cleanup');
|
|
2174
|
+
summary.cleanup = 'skipped';
|
|
2175
|
+
summary.warnings.push('Local cleanup skipped by configuration (--no-cleanup).');
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
if (!isCleanupCandidateBranch(originalBranch)) {
|
|
2180
|
+
summary.actionsSkipped.push('cleanup');
|
|
2181
|
+
summary.cleanup = 'skipped';
|
|
2182
|
+
summary.warnings.push(`Cleanup skipped: branch "${originalBranch}" is not an allowed code branch pattern.`);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
if (isProtectedOrGeneratedBranch(originalBranch)) {
|
|
2187
|
+
summary.actionsSkipped.push('cleanup');
|
|
2188
|
+
summary.cleanup = 'skipped';
|
|
2189
|
+
summary.warnings.push(`Cleanup skipped: branch "${originalBranch}" is protected or generated.`);
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
if (!ensureWorkingTreeClean(deps)) {
|
|
2194
|
+
summary.actionsSkipped.push('cleanup');
|
|
2195
|
+
summary.cleanup = 'skipped';
|
|
2196
|
+
summary.warnings.push('Cleanup skipped: working tree is not clean.');
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
reporter.start('release-cycle-cleanup-checkout', `Checking out ${targetBaseBranch}...`);
|
|
2201
|
+
const checkout = deps.exec('git', ['checkout', targetBaseBranch]);
|
|
2202
|
+
if (checkout.status !== 0) {
|
|
2203
|
+
summary.cleanup = 'failed';
|
|
2204
|
+
summary.warnings.push(`Cleanup failed: could not checkout ${targetBaseBranch}: ${(checkout.stderr || checkout.stdout || '').trim()}`);
|
|
2205
|
+
reporter.warn('release-cycle-cleanup-checkout', `Could not checkout ${targetBaseBranch}.`);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
reporter.ok('release-cycle-cleanup-checkout', `Checked out ${targetBaseBranch}.`);
|
|
2209
|
+
|
|
2210
|
+
reporter.start('release-cycle-cleanup-pull', `Pulling latest ${targetBaseBranch}...`);
|
|
2211
|
+
const pull = deps.exec('git', ['pull']);
|
|
2212
|
+
if (pull.status !== 0) {
|
|
2213
|
+
summary.cleanup = 'failed';
|
|
2214
|
+
summary.warnings.push(`Cleanup warning: could not pull ${targetBaseBranch}: ${(pull.stderr || pull.stdout || '').trim()}`);
|
|
2215
|
+
reporter.warn('release-cycle-cleanup-pull', `Could not pull ${targetBaseBranch}.`);
|
|
2216
|
+
} else {
|
|
2217
|
+
reporter.ok('release-cycle-cleanup-pull', `Pulled ${targetBaseBranch}.`);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
reporter.start('release-cycle-cleanup-delete', `Deleting local branch ${originalBranch}...`);
|
|
2221
|
+
const deleteResult = deps.exec('git', ['branch', '-d', originalBranch]);
|
|
2222
|
+
if (deleteResult.status !== 0) {
|
|
2223
|
+
summary.cleanup = 'failed';
|
|
2224
|
+
summary.warnings.push(`Cleanup warning: could not delete ${originalBranch}: ${(deleteResult.stderr || deleteResult.stdout || '').trim()}`);
|
|
2225
|
+
reporter.warn('release-cycle-cleanup-delete', `Could not delete ${originalBranch}.`);
|
|
2226
|
+
} else {
|
|
2227
|
+
summary.actionsPerformed.push(`cleanup deleted branch: ${originalBranch}`);
|
|
2228
|
+
summary.cleanup = 'completed';
|
|
2229
|
+
reporter.ok('release-cycle-cleanup-delete', `Deleted ${originalBranch}.`);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function ensureFileFromTemplate(targetPath, templatePath, options) {
|
|
2234
|
+
const exists = fs.existsSync(targetPath);
|
|
2235
|
+
|
|
2236
|
+
if (exists && !options.force) {
|
|
2237
|
+
return 'skipped';
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
if (options.dryRun) {
|
|
2241
|
+
return exists ? 'overwritten' : 'created';
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
const source = fs.readFileSync(templatePath, 'utf8');
|
|
2245
|
+
const rendered = renderTemplateString(source, options.variables);
|
|
2246
|
+
|
|
2247
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
2248
|
+
fs.writeFileSync(targetPath, rendered);
|
|
2249
|
+
|
|
2250
|
+
if (exists) {
|
|
2251
|
+
return 'overwritten';
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
return 'created';
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
function ensureCreateOnlyFromTemplate(targetPath, templatePath, options) {
|
|
2258
|
+
if (fs.existsSync(targetPath)) {
|
|
2259
|
+
return 'skipped';
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
return ensureFileFromTemplate(targetPath, templatePath, options);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function appendGitignoreTemplate(targetPath, templatePath, options) {
|
|
2266
|
+
if (!fs.existsSync(targetPath)) {
|
|
2267
|
+
return ensureFileFromTemplate(targetPath, templatePath, options);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
const currentRaw = fs.readFileSync(targetPath, 'utf8');
|
|
2271
|
+
const templateRaw = fs.readFileSync(templatePath, 'utf8');
|
|
2272
|
+
const currentSet = new Set(
|
|
2273
|
+
currentRaw
|
|
2274
|
+
.split(/\r?\n/)
|
|
2275
|
+
.map((line) => line.trim())
|
|
2276
|
+
.filter(Boolean)
|
|
2277
|
+
);
|
|
2278
|
+
|
|
2279
|
+
const missingLines = templateRaw
|
|
2280
|
+
.split(/\r?\n/)
|
|
2281
|
+
.map((line) => line.trim())
|
|
2282
|
+
.filter((line) => line && !currentSet.has(line));
|
|
2283
|
+
|
|
2284
|
+
if (!missingLines.length) {
|
|
2285
|
+
return 'skipped';
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
if (options.dryRun) {
|
|
2289
|
+
return 'updated';
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
const needsSeparator = currentRaw.length > 0 && !currentRaw.endsWith('\n');
|
|
2293
|
+
const prefix = needsSeparator ? '\n' : '';
|
|
2294
|
+
fs.appendFileSync(targetPath, `${prefix}${missingLines.join('\n')}\n`);
|
|
2295
|
+
return 'updated';
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
function ensureReleaseWorkflowBranches(content, defaultBranch, betaBranch) {
|
|
2299
|
+
const lines = content.split('\n');
|
|
2300
|
+
const onIndex = lines.findIndex((line) => line.trim() === 'on:');
|
|
2301
|
+
|
|
2302
|
+
if (onIndex < 0) {
|
|
2303
|
+
return null;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
let onSectionEnd = lines.length;
|
|
2307
|
+
for (let i = onIndex + 1; i < lines.length; i += 1) {
|
|
2308
|
+
const line = lines[i];
|
|
2309
|
+
const isTopLevelKey = line && !line.startsWith(' ') && line.trim().endsWith(':');
|
|
2310
|
+
if (isTopLevelKey) {
|
|
2311
|
+
onSectionEnd = i;
|
|
2312
|
+
break;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const onBlock = lines.slice(onIndex, onSectionEnd);
|
|
2317
|
+
const pushRelativeIndex = onBlock.findIndex((line) => line.trim() === 'push:');
|
|
2318
|
+
if (pushRelativeIndex < 0) {
|
|
2319
|
+
return null;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
const branchesRelativeIndex = onBlock.findIndex((line) => line.trim() === 'branches:');
|
|
2323
|
+
if (branchesRelativeIndex < 0 || branchesRelativeIndex <= pushRelativeIndex) {
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
const listStart = branchesRelativeIndex + 1;
|
|
2328
|
+
let listEnd = listStart;
|
|
2329
|
+
while (listEnd < onBlock.length && onBlock[listEnd].trim().startsWith('- ')) {
|
|
2330
|
+
listEnd += 1;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
if (listEnd === listStart) {
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
const existingBranches = onBlock.slice(listStart, listEnd)
|
|
2338
|
+
.map((line) => line.trim().replace(/^- /, '').trim())
|
|
2339
|
+
.filter(Boolean);
|
|
2340
|
+
|
|
2341
|
+
const desiredBranches = [...new Set([defaultBranch, betaBranch])];
|
|
2342
|
+
const mergedBranches = [...existingBranches];
|
|
2343
|
+
for (const branch of desiredBranches) {
|
|
2344
|
+
if (!mergedBranches.includes(branch)) {
|
|
2345
|
+
mergedBranches.push(branch);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
const changed = mergedBranches.length !== existingBranches.length
|
|
2350
|
+
|| mergedBranches.some((branch, index) => branch !== existingBranches[index]);
|
|
2351
|
+
|
|
2352
|
+
if (!changed) {
|
|
2353
|
+
return {
|
|
2354
|
+
changed: false,
|
|
2355
|
+
content
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
const updatedOnBlock = [
|
|
2360
|
+
...onBlock.slice(0, listStart),
|
|
2361
|
+
...mergedBranches.map((branch) => ` - ${branch}`),
|
|
2362
|
+
...onBlock.slice(listEnd)
|
|
2363
|
+
];
|
|
2364
|
+
|
|
2365
|
+
const updatedLines = [
|
|
2366
|
+
...lines.slice(0, onIndex),
|
|
2367
|
+
...updatedOnBlock,
|
|
2368
|
+
...lines.slice(onSectionEnd)
|
|
2369
|
+
];
|
|
2370
|
+
|
|
2371
|
+
return {
|
|
2372
|
+
changed: true,
|
|
2373
|
+
content: updatedLines.join('\n')
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function upsertReleaseWorkflow(targetPath, templatePath, options) {
|
|
2378
|
+
const exists = fs.existsSync(targetPath);
|
|
2379
|
+
if (!exists || options.force) {
|
|
2380
|
+
if (options.dryRun) {
|
|
2381
|
+
return {
|
|
2382
|
+
result: exists ? 'overwritten' : 'created'
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const result = ensureFileFromTemplate(targetPath, templatePath, {
|
|
2387
|
+
force: options.force,
|
|
2388
|
+
dryRun: options.dryRun,
|
|
2389
|
+
variables: options.variables
|
|
2390
|
+
});
|
|
2391
|
+
return { result };
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
const current = fs.readFileSync(targetPath, 'utf8');
|
|
2395
|
+
const ensured = ensureReleaseWorkflowBranches(
|
|
2396
|
+
current,
|
|
2397
|
+
options.variables.DEFAULT_BRANCH,
|
|
2398
|
+
options.variables.BETA_BRANCH
|
|
2399
|
+
);
|
|
2400
|
+
|
|
2401
|
+
if (!ensured) {
|
|
2402
|
+
return {
|
|
2403
|
+
result: 'skipped',
|
|
2404
|
+
warning: `Could not safely update trigger branches in ${path.basename(targetPath)}. Use --force to overwrite from template.`
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
if (!ensured.changed) {
|
|
2409
|
+
return { result: 'skipped' };
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
if (options.dryRun) {
|
|
2413
|
+
return { result: 'updated' };
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
fs.writeFileSync(targetPath, ensured.content);
|
|
2417
|
+
return { result: 'updated' };
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function detectEquivalentManagedFile(packageDir, targetRelativePath) {
|
|
2421
|
+
if (targetRelativePath !== '.github/PULL_REQUEST_TEMPLATE.md') {
|
|
2422
|
+
return targetRelativePath;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
const canonicalPath = path.join(packageDir, targetRelativePath);
|
|
2426
|
+
if (fs.existsSync(canonicalPath)) {
|
|
2427
|
+
return targetRelativePath;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
const legacyLowercase = '.github/pull_request_template.md';
|
|
2431
|
+
if (fs.existsSync(path.join(packageDir, legacyLowercase))) {
|
|
2432
|
+
return legacyLowercase;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
return targetRelativePath;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
function updateManagedFiles(packageDir, templateDir, options, summary) {
|
|
2439
|
+
for (const [targetRelativePath, templateRelativePath] of MANAGED_FILE_SPECS) {
|
|
2440
|
+
const effectiveTargetRelative = detectEquivalentManagedFile(packageDir, targetRelativePath);
|
|
2441
|
+
const targetPath = path.join(packageDir, effectiveTargetRelative);
|
|
2442
|
+
const templatePath = path.join(templateDir, templateRelativePath);
|
|
2443
|
+
|
|
2444
|
+
if (!fs.existsSync(templatePath)) {
|
|
2445
|
+
throw new Error(`Template not found: ${templatePath}`);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
let result;
|
|
2449
|
+
if (targetRelativePath === '.gitignore') {
|
|
2450
|
+
result = appendGitignoreTemplate(targetPath, templatePath, {
|
|
2451
|
+
force: options.force,
|
|
2452
|
+
dryRun: options.dryRun,
|
|
2453
|
+
variables: options.variables
|
|
2454
|
+
});
|
|
2455
|
+
} else if (INIT_CREATE_ONLY_FILES.has(targetRelativePath)) {
|
|
2456
|
+
result = ensureCreateOnlyFromTemplate(targetPath, templatePath, {
|
|
2457
|
+
force: options.force,
|
|
2458
|
+
dryRun: options.dryRun,
|
|
2459
|
+
variables: options.variables
|
|
2460
|
+
});
|
|
2461
|
+
} else {
|
|
2462
|
+
result = ensureFileFromTemplate(targetPath, templatePath, {
|
|
2463
|
+
force: options.force,
|
|
2464
|
+
dryRun: options.dryRun,
|
|
2465
|
+
variables: options.variables
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
if (result === 'created') {
|
|
2470
|
+
summary.createdFiles.push(targetRelativePath);
|
|
2471
|
+
} else if (result === 'overwritten' || result === 'updated') {
|
|
2472
|
+
summary.overwrittenFiles.push(targetRelativePath);
|
|
2473
|
+
} else {
|
|
2474
|
+
summary.skippedFiles.push(targetRelativePath);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
function removeLegacyReleaseScripts(packageJson, summary) {
|
|
2480
|
+
const keys = Object.keys(packageJson.scripts || {});
|
|
2481
|
+
|
|
2482
|
+
for (const key of keys) {
|
|
2483
|
+
const isLegacy = key === 'release:dist-tags'
|
|
2484
|
+
|| key.startsWith('release:beta')
|
|
2485
|
+
|| key.startsWith('release:stable')
|
|
2486
|
+
|| key.startsWith('release:promote')
|
|
2487
|
+
|| key.startsWith('release:rollback');
|
|
2488
|
+
|
|
2489
|
+
if (!isLegacy) {
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
delete packageJson.scripts[key];
|
|
2494
|
+
summary.removedScriptKeys.push(key);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function configureExistingPackage(packageDir, templateDir, options) {
|
|
2499
|
+
if (!fs.existsSync(packageDir)) {
|
|
2500
|
+
throw new Error(`Directory not found: ${packageDir}`);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
const packageJsonPath = path.join(packageDir, 'package.json');
|
|
2504
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
2505
|
+
throw new Error(`package.json not found in ${packageDir}`);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
2509
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
2510
|
+
packageJson.devDependencies = packageJson.devDependencies || {};
|
|
2511
|
+
|
|
2512
|
+
const summary = createSummary();
|
|
2513
|
+
|
|
2514
|
+
const desiredScripts = {
|
|
2515
|
+
check: 'npm run test',
|
|
2516
|
+
changeset: 'changeset',
|
|
2517
|
+
'version-packages': 'changeset version',
|
|
2518
|
+
release: 'npm run check && changeset publish',
|
|
2519
|
+
'beta:enter': 'changeset pre enter beta',
|
|
2520
|
+
'beta:exit': 'changeset pre exit',
|
|
2521
|
+
'beta:version': 'changeset version',
|
|
2522
|
+
'beta:publish': 'changeset publish',
|
|
2523
|
+
'beta:promote': 'create-package-starter promote-stable --dir .'
|
|
2524
|
+
};
|
|
2525
|
+
|
|
2526
|
+
let packageJsonChanged = false;
|
|
2527
|
+
|
|
2528
|
+
for (const [key, value] of Object.entries(desiredScripts)) {
|
|
2529
|
+
const exists = Object.prototype.hasOwnProperty.call(packageJson.scripts, key);
|
|
2530
|
+
|
|
2531
|
+
if (key === 'check') {
|
|
2532
|
+
if (!exists) {
|
|
2533
|
+
packageJson.scripts[key] = value;
|
|
2534
|
+
packageJsonChanged = true;
|
|
2535
|
+
summary.updatedScriptKeys.push(key);
|
|
2536
|
+
} else if (options.force && packageJson.scripts[key] !== value) {
|
|
2537
|
+
packageJson.scripts[key] = value;
|
|
2538
|
+
packageJsonChanged = true;
|
|
2539
|
+
summary.updatedScriptKeys.push(key);
|
|
2540
|
+
} else {
|
|
2541
|
+
summary.skippedScriptKeys.push(key);
|
|
2542
|
+
}
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
if (!exists || options.force) {
|
|
2547
|
+
if (!exists || packageJson.scripts[key] !== value) {
|
|
2548
|
+
packageJson.scripts[key] = value;
|
|
2549
|
+
packageJsonChanged = true;
|
|
2550
|
+
}
|
|
2551
|
+
summary.updatedScriptKeys.push(key);
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
summary.skippedScriptKeys.push(key);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
const depExists = Object.prototype.hasOwnProperty.call(packageJson.devDependencies, CHANGESETS_DEP);
|
|
2559
|
+
|
|
2560
|
+
if (!depExists || options.force) {
|
|
2561
|
+
if (!depExists || packageJson.devDependencies[CHANGESETS_DEP] !== CHANGESETS_DEP_VERSION) {
|
|
2562
|
+
packageJson.devDependencies[CHANGESETS_DEP] = CHANGESETS_DEP_VERSION;
|
|
2563
|
+
packageJsonChanged = true;
|
|
2564
|
+
}
|
|
2565
|
+
summary.updatedDependencyKeys.push(CHANGESETS_DEP);
|
|
2566
|
+
} else {
|
|
2567
|
+
summary.skippedDependencyKeys.push(CHANGESETS_DEP);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
if (options.cleanupLegacyRelease) {
|
|
2571
|
+
const before = summary.removedScriptKeys.length;
|
|
2572
|
+
removeLegacyReleaseScripts(packageJson, summary);
|
|
2573
|
+
if (summary.removedScriptKeys.length > before) {
|
|
2574
|
+
packageJsonChanged = true;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
const packageName = packageJson.name || packageDirFromName(path.basename(packageDir));
|
|
2579
|
+
|
|
2580
|
+
updateManagedFiles(packageDir, templateDir, {
|
|
2581
|
+
force: options.force,
|
|
2582
|
+
dryRun: options.dryRun,
|
|
2583
|
+
variables: {
|
|
2584
|
+
PACKAGE_NAME: packageName,
|
|
2585
|
+
DEFAULT_BRANCH: options.defaultBranch,
|
|
2586
|
+
BETA_BRANCH: options.betaBranch || DEFAULT_BETA_BRANCH,
|
|
2587
|
+
SCOPE: deriveScope(options.scope, packageName),
|
|
2588
|
+
...buildReleaseAuthVariables(options.releaseAuth || DEFAULT_RELEASE_AUTH)
|
|
2589
|
+
}
|
|
2590
|
+
}, summary);
|
|
2591
|
+
|
|
2592
|
+
if (packageJsonChanged && !options.dryRun) {
|
|
2593
|
+
writeJsonFile(packageJsonPath, packageJson);
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
return summary;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function createNewPackage(args) {
|
|
2600
|
+
if (!validateName(args.name)) {
|
|
2601
|
+
throw new Error('Provide a valid package name with --name (for example: hello-package or @i-santos/swarm).');
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
2605
|
+
const templateDir = path.join(packageRoot, 'template');
|
|
2606
|
+
|
|
2607
|
+
if (!fs.existsSync(templateDir)) {
|
|
2608
|
+
throw new Error(`Template not found in ${templateDir}`);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
const outputDir = path.resolve(args.out);
|
|
2612
|
+
const targetDir = path.join(outputDir, packageDirFromName(args.name));
|
|
2613
|
+
|
|
2614
|
+
if (fs.existsSync(targetDir)) {
|
|
2615
|
+
throw new Error(`Directory already exists: ${targetDir}`);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
const summary = createSummary();
|
|
2619
|
+
|
|
2620
|
+
const createdFiles = copyDirRecursive(templateDir, targetDir, {
|
|
2621
|
+
PACKAGE_NAME: args.name,
|
|
2622
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
2623
|
+
BETA_BRANCH: DEFAULT_BETA_BRANCH,
|
|
2624
|
+
SCOPE: deriveScope('', args.name),
|
|
2625
|
+
...buildReleaseAuthVariables(args.releaseAuth || DEFAULT_RELEASE_AUTH)
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
summary.createdFiles.push(...createdFiles);
|
|
2629
|
+
|
|
2630
|
+
summary.updatedScriptKeys.push('check', 'changeset', 'version-packages', 'release');
|
|
2631
|
+
summary.updatedScriptKeys.push('beta:enter', 'beta:exit', 'beta:version', 'beta:publish', 'beta:promote');
|
|
2632
|
+
summary.updatedScriptKeys.push(`release.auth:${args.releaseAuth}`);
|
|
2633
|
+
summary.updatedDependencyKeys.push(CHANGESETS_DEP);
|
|
2634
|
+
appendReleaseAuthWarnings(summary, args.releaseAuth);
|
|
2635
|
+
|
|
2636
|
+
printSummary(`Package created in ${targetDir}`, summary);
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
async function initExistingPackage(args, dependencies = {}) {
|
|
2640
|
+
const reporter = new StepReporter();
|
|
2641
|
+
const selections = await resolveInitSelections(args);
|
|
2642
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
2643
|
+
const templateDir = path.join(packageRoot, 'template');
|
|
2644
|
+
const targetDir = path.resolve(args.dir);
|
|
2645
|
+
const overallSummary = createSummary();
|
|
2646
|
+
const deps = {
|
|
2647
|
+
exec: dependencies.exec || execCommand
|
|
2648
|
+
};
|
|
2649
|
+
await resolveReleaseAuthSelection(args, overallSummary, { contextLabel: 'Select release auth mode for release workflow' });
|
|
2650
|
+
overallSummary.updatedScriptKeys.push(`release.auth:${args.releaseAuth}`);
|
|
2651
|
+
|
|
2652
|
+
if (!selections.withGithub && !selections.withNpm && !selections.withBeta && !process.stdin.isTTY) {
|
|
2653
|
+
overallSummary.warnings.push('No --with-* flags were provided in non-interactive mode. Only local init was applied.');
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
const context = prevalidateInitExecution(args, selections, dependencies, reporter);
|
|
2657
|
+
appendReleaseAuthWarnings(overallSummary, args.releaseAuth, {
|
|
2658
|
+
missingAppSecrets: context.missingReleaseAuthAppSecrets,
|
|
2659
|
+
appSecretsChecked: selections.withGithub && args.releaseAuth === 'app'
|
|
2660
|
+
});
|
|
2661
|
+
await confirmInitPlan(args, selections, context, overallSummary);
|
|
2662
|
+
|
|
2663
|
+
reporter.start('local-init', 'Applying local package bootstrap...');
|
|
2664
|
+
const localSummary = configureExistingPackage(targetDir, templateDir, {
|
|
2665
|
+
...args,
|
|
2666
|
+
dryRun: args.dryRun,
|
|
2667
|
+
betaBranch: args.betaBranch
|
|
2668
|
+
});
|
|
2669
|
+
mergeSummary(overallSummary, localSummary);
|
|
2670
|
+
reporter.ok('local-init', args.dryRun ? 'Local package bootstrap previewed.' : 'Local package bootstrap applied.');
|
|
2671
|
+
|
|
2672
|
+
if (selections.withGithub && selections.withBeta) {
|
|
2673
|
+
ensureBetaWorkflowTriggers(
|
|
2674
|
+
targetDir,
|
|
2675
|
+
templateDir,
|
|
2676
|
+
{
|
|
2677
|
+
force: args.force,
|
|
2678
|
+
dryRun: args.dryRun,
|
|
2679
|
+
defaultBranch: args.defaultBranch,
|
|
2680
|
+
betaBranch: args.betaBranch,
|
|
2681
|
+
packageName: context.packageName,
|
|
2682
|
+
scope: deriveScope(args.scope, context.packageName),
|
|
2683
|
+
releaseAuth: args.releaseAuth
|
|
2684
|
+
},
|
|
2685
|
+
overallSummary,
|
|
2686
|
+
reporter
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
let repo = context.repo;
|
|
2691
|
+
if (selections.withGithub) {
|
|
2692
|
+
const githubSummary = createSummary();
|
|
2693
|
+
const mainResult = applyGithubMainSetup(
|
|
2694
|
+
{
|
|
2695
|
+
repo: context.repo,
|
|
2696
|
+
defaultBranch: args.defaultBranch,
|
|
2697
|
+
ruleset: args.ruleset,
|
|
2698
|
+
dryRun: args.dryRun
|
|
2699
|
+
},
|
|
2700
|
+
{ exec: deps.exec },
|
|
2701
|
+
githubSummary,
|
|
2702
|
+
reporter
|
|
2703
|
+
);
|
|
2704
|
+
repo = mainResult.repo;
|
|
2705
|
+
|
|
2706
|
+
if (selections.withBeta) {
|
|
2707
|
+
applyGithubBetaSetup(
|
|
2708
|
+
{
|
|
2709
|
+
betaBranch: args.betaBranch,
|
|
2710
|
+
defaultBranch: args.defaultBranch,
|
|
2711
|
+
dryRun: args.dryRun
|
|
2712
|
+
},
|
|
2713
|
+
{ exec: deps.exec },
|
|
2714
|
+
githubSummary,
|
|
2715
|
+
reporter,
|
|
2716
|
+
repo
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
mergeSummary(overallSummary, githubSummary);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
if (selections.withNpm) {
|
|
2724
|
+
const npmSummary = runNpmSetup(
|
|
2725
|
+
{
|
|
2726
|
+
dir: targetDir,
|
|
2727
|
+
dryRun: args.dryRun,
|
|
2728
|
+
publishFirst: false
|
|
2729
|
+
},
|
|
2730
|
+
{ exec: deps.exec },
|
|
2731
|
+
{
|
|
2732
|
+
reporter,
|
|
2733
|
+
publishMissingByDefault: true
|
|
2734
|
+
}
|
|
2735
|
+
);
|
|
2736
|
+
mergeSummary(overallSummary, npmSummary);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
reporter.start('npm-install-final', 'Running npm install...');
|
|
2740
|
+
if (args.dryRun) {
|
|
2741
|
+
overallSummary.warnings.push(`dry-run: would run "npm install" in ${targetDir}`);
|
|
2742
|
+
reporter.warn('npm-install-final', 'Dry-run enabled; npm install was not executed.');
|
|
2743
|
+
} else {
|
|
2744
|
+
const install = deps.exec('npm', ['install'], { cwd: targetDir, stdio: 'inherit' });
|
|
2745
|
+
if (install.status !== 0) {
|
|
2746
|
+
reporter.fail('npm-install-final', 'npm install failed.');
|
|
2747
|
+
throw new Error(`Failed to run npm install in ${targetDir}.`);
|
|
2748
|
+
}
|
|
2749
|
+
reporter.ok('npm-install-final', 'npm install completed.');
|
|
2750
|
+
overallSummary.updatedDependencyKeys.push('npm.install');
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
printSummary(`Project initialized in ${targetDir}`, overallSummary);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
function execCommand(command, args, options = {}) {
|
|
2757
|
+
return spawnSync(command, args, {
|
|
2758
|
+
encoding: 'utf8',
|
|
2759
|
+
...options
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
async function runOpenPrFlow(args, dependencies = {}) {
|
|
2764
|
+
const deps = {
|
|
2765
|
+
exec: dependencies.exec || execCommand
|
|
2766
|
+
};
|
|
2767
|
+
const summary = createOrchestrationSummary();
|
|
2768
|
+
const reporter = new StepReporter();
|
|
2769
|
+
|
|
2770
|
+
reporter.start('open-pr-preflight-gh', 'Validating GitHub CLI and authentication...');
|
|
2771
|
+
ensureGhAvailable(deps);
|
|
2772
|
+
reporter.ok('open-pr-preflight-gh', 'GitHub CLI available and authenticated.');
|
|
2773
|
+
|
|
2774
|
+
reporter.start('open-pr-preflight-git', 'Resolving git context...');
|
|
2775
|
+
const context = resolveGitContext(args, deps);
|
|
2776
|
+
reporter.ok('open-pr-preflight-git', `Using ${context.head} -> ${context.base} in ${context.repo}.`);
|
|
2777
|
+
summary.modeDetected = 'open-pr';
|
|
2778
|
+
summary.repoResolved = context.repo;
|
|
2779
|
+
|
|
2780
|
+
const generatedBody = renderPrBodyDeterministic(context, deps, {
|
|
2781
|
+
cwd: process.cwd(),
|
|
2782
|
+
body: args.body,
|
|
2783
|
+
bodyFile: args.bodyFile,
|
|
2784
|
+
template: args.template
|
|
2785
|
+
});
|
|
2786
|
+
|
|
2787
|
+
const shouldPrintSummary = args.printSummary !== false;
|
|
2788
|
+
|
|
2789
|
+
if (args.dryRun) {
|
|
2790
|
+
summary.branchPushed = `dry-run: would push ${context.head}`;
|
|
2791
|
+
summary.prAction = `dry-run: would create/update PR ${context.head} -> ${context.base}`;
|
|
2792
|
+
summary.prUrl = 'dry-run';
|
|
2793
|
+
summary.autoMerge = args.autoMerge ? 'dry-run: would enable auto-merge' : 'skipped';
|
|
2794
|
+
summary.checks = args.watchChecks ? `dry-run: would watch checks (${args.checkTimeout}m)` : 'skipped';
|
|
2795
|
+
summary.merge = 'skipped';
|
|
2796
|
+
summary.releasePr = 'skipped';
|
|
2797
|
+
summary.actionsPerformed.push('rendered deterministic PR body', 'prepared push/create/update plan');
|
|
2798
|
+
if (!args.body && !args.bodyFile && !args.template) {
|
|
2799
|
+
summary.warnings.push('No body inputs provided; deterministic generated body would be used.');
|
|
2800
|
+
}
|
|
2801
|
+
if (shouldPrintSummary) {
|
|
2802
|
+
printOrchestrationSummary(`open-pr dry-run for ${context.repo}`, summary);
|
|
2803
|
+
}
|
|
2804
|
+
return {
|
|
2805
|
+
summary,
|
|
2806
|
+
context,
|
|
2807
|
+
pr: null
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
if (args.skipPush) {
|
|
2812
|
+
summary.branchPushed = `skipped (${context.head})`;
|
|
2813
|
+
summary.actionsSkipped.push(`push skipped: ${context.head}`);
|
|
2814
|
+
} else {
|
|
2815
|
+
reporter.start('open-pr-push', `Pushing branch "${context.head}"...`);
|
|
2816
|
+
const pushResult = ensureBranchPushed(context.repo, context.head, deps);
|
|
2817
|
+
reporter.ok('open-pr-push', `Branch "${context.head}" pushed (${pushResult.status}).`);
|
|
2818
|
+
summary.branchPushed = `${context.head} (${pushResult.status})`;
|
|
2819
|
+
summary.actionsPerformed.push(`branch pushed: ${context.head}`);
|
|
2820
|
+
if (pushResult.status === 'up-to-date') {
|
|
2821
|
+
summary.warnings.push(`Branch "${context.head}" had no new commits to push.`);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
reporter.start('open-pr-upsert', 'Creating or updating pull request...');
|
|
2826
|
+
const prResult = createOrUpdatePr(context, generatedBody, args, deps);
|
|
2827
|
+
reporter.ok('open-pr-upsert', `PR ${prResult.action}: #${prResult.number}`);
|
|
2828
|
+
summary.prAction = `${prResult.action} (#${prResult.number})`;
|
|
2829
|
+
summary.prUrl = prResult.url || 'n/a';
|
|
2830
|
+
summary.actionsPerformed.push(`pr ${prResult.action}: #${prResult.number}`);
|
|
2831
|
+
if (prResult.action === 'reused') {
|
|
2832
|
+
summary.warnings.push('Existing PR reused without body/title changes. Use --update-pr-description to refresh PR content.');
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
if (args.autoMerge) {
|
|
2836
|
+
reporter.start('open-pr-auto-merge', `Enabling auto-merge for PR #${prResult.number}...`);
|
|
2837
|
+
enablePrAutoMerge(context.repo, prResult.number, args.mergeMethod || 'squash', deps);
|
|
2838
|
+
reporter.ok('open-pr-auto-merge', `Auto-merge enabled for PR #${prResult.number}.`);
|
|
2839
|
+
summary.autoMerge = 'enabled';
|
|
2840
|
+
summary.actionsPerformed.push(`auto-merge enabled for #${prResult.number}`);
|
|
2841
|
+
} else {
|
|
2842
|
+
summary.autoMerge = 'skipped';
|
|
2843
|
+
summary.actionsSkipped.push('auto-merge');
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
if (args.watchChecks) {
|
|
2847
|
+
reporter.start('open-pr-checks', `Watching checks for PR #${prResult.number}...`);
|
|
2848
|
+
watchPrChecks(context.repo, prResult.number, args.checkTimeout, deps);
|
|
2849
|
+
reporter.ok('open-pr-checks', `Checks green for PR #${prResult.number}.`);
|
|
2850
|
+
summary.checks = 'green';
|
|
2851
|
+
summary.actionsPerformed.push(`checks watched for #${prResult.number}`);
|
|
2852
|
+
} else {
|
|
2853
|
+
summary.checks = 'skipped';
|
|
2854
|
+
summary.actionsSkipped.push('watch-checks');
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
if (!args.body && !args.bodyFile && !args.template) {
|
|
2858
|
+
summary.warnings.push('PR body used deterministic generated markdown (no body/template inputs).');
|
|
2859
|
+
}
|
|
2860
|
+
summary.merge = 'skipped';
|
|
2861
|
+
summary.releasePr = 'skipped';
|
|
2862
|
+
if (shouldPrintSummary) {
|
|
2863
|
+
printOrchestrationSummary(`open-pr completed for ${context.repo}`, summary);
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
return {
|
|
2867
|
+
summary,
|
|
2868
|
+
context,
|
|
2869
|
+
pr: prResult
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
async function runReleaseCycle(args, dependencies = {}) {
|
|
2874
|
+
const deps = {
|
|
2875
|
+
exec: dependencies.exec || execCommand
|
|
2876
|
+
};
|
|
2877
|
+
const summary = createOrchestrationSummary();
|
|
2878
|
+
const reporter = new StepReporter();
|
|
2879
|
+
const originalBranch = deps.exec('git', ['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
|
2880
|
+
const useAutoMerge = args.autoMerge;
|
|
2881
|
+
|
|
2882
|
+
reporter.start('release-cycle-preflight-gh', 'Validating GitHub CLI and authentication...');
|
|
2883
|
+
ensureGhAvailable(deps);
|
|
2884
|
+
reporter.ok('release-cycle-preflight-gh', 'GitHub CLI available and authenticated.');
|
|
2885
|
+
|
|
2886
|
+
const gitContext = resolveGitContext(args, deps);
|
|
2887
|
+
summary.repoResolved = gitContext.repo;
|
|
2888
|
+
const effectivePhase = args.phase;
|
|
2889
|
+
const requestedTrack = args.track === 'auto' ? (args.promoteStable ? 'stable' : 'beta') : args.track;
|
|
2890
|
+
if (args.promoteStable && gitContext.head !== DEFAULT_BETA_BRANCH) {
|
|
2891
|
+
throw new Error(`--promote-stable is only allowed when running from "${DEFAULT_BETA_BRANCH}".`);
|
|
2892
|
+
}
|
|
2893
|
+
if (requestedTrack === 'stable' && !args.promoteStable) {
|
|
2894
|
+
throw new Error('Stable track requires --promote-stable for explicit promotion.');
|
|
2895
|
+
}
|
|
2896
|
+
if (gitContext.head !== DEFAULT_BETA_BRANCH && requestedTrack === 'stable') {
|
|
2897
|
+
throw new Error(`Stable track is only supported from "${DEFAULT_BETA_BRANCH}".`);
|
|
2898
|
+
}
|
|
2899
|
+
summary.actionsPerformed.push(`release track: ${requestedTrack}`);
|
|
2900
|
+
summary.releaseTrack = requestedTrack;
|
|
2901
|
+
let detectedMode = args.mode;
|
|
2902
|
+
if (args.promoteStable) {
|
|
2903
|
+
detectedMode = 'open-pr';
|
|
2904
|
+
} else if (detectedMode === 'auto') {
|
|
2905
|
+
const releasePrs = findReleasePrs(gitContext.repo, deps, {
|
|
2906
|
+
expectedBase: releaseBaseBranchForTrack(requestedTrack)
|
|
2907
|
+
});
|
|
2908
|
+
if (gitContext.head.startsWith('changeset-release/')) {
|
|
2909
|
+
detectedMode = 'publish';
|
|
2910
|
+
} else if (gitContext.head !== DEFAULT_BETA_BRANCH) {
|
|
2911
|
+
detectedMode = 'open-pr';
|
|
2912
|
+
} else if (releasePrs.length === 1) {
|
|
2913
|
+
detectedMode = 'publish';
|
|
2914
|
+
} else if (releasePrs.length > 1) {
|
|
2915
|
+
throw new Error(`Multiple candidate release PRs detected: ${releasePrs.map((item) => item.url).join(', ')}`);
|
|
2916
|
+
} else {
|
|
2917
|
+
detectedMode = 'open-pr';
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
summary.modeDetected = detectedMode;
|
|
2921
|
+
|
|
2922
|
+
await confirmDetectedModeIfNeeded(
|
|
2923
|
+
args,
|
|
2924
|
+
detectedMode,
|
|
2925
|
+
detectedMode === 'open-pr'
|
|
2926
|
+
? 'Will create/update code PR, watch checks, merge, then wait/merge release PR.'
|
|
2927
|
+
: 'Will operate on release PR (changeset-release/*), watch checks, and merge when green.'
|
|
2928
|
+
);
|
|
2929
|
+
|
|
2930
|
+
if (detectedMode === 'open-pr') {
|
|
2931
|
+
if (args.promoteStable) {
|
|
2932
|
+
reporter.start('release-cycle-promote-dispatch', `Dispatching ${DEFAULT_PROMOTE_WORKFLOW}...`);
|
|
2933
|
+
if (args.dryRun) {
|
|
2934
|
+
reporter.warn('release-cycle-promote-dispatch', `Dry-run: would dispatch ${DEFAULT_PROMOTE_WORKFLOW}.`);
|
|
2935
|
+
summary.actionsPerformed.push(`dry-run: dispatch ${DEFAULT_PROMOTE_WORKFLOW}`);
|
|
2936
|
+
summary.promotionWorkflow = `dry-run: ${DEFAULT_PROMOTE_WORKFLOW}`;
|
|
2937
|
+
} else {
|
|
2938
|
+
dispatchPromoteStableWorkflow(gitContext.repo, {
|
|
2939
|
+
...args,
|
|
2940
|
+
head: DEFAULT_BETA_BRANCH
|
|
2941
|
+
}, deps);
|
|
2942
|
+
reporter.ok('release-cycle-promote-dispatch', `Dispatched ${DEFAULT_PROMOTE_WORKFLOW}.`);
|
|
2943
|
+
summary.actionsPerformed.push(`promotion workflow dispatched: ${DEFAULT_PROMOTE_WORKFLOW}`);
|
|
2944
|
+
summary.promotionWorkflow = `dispatched: ${DEFAULT_PROMOTE_WORKFLOW}`;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
if (!args.dryRun) {
|
|
2948
|
+
reporter.start('release-cycle-promote-pr', 'Waiting for promotion PR...');
|
|
2949
|
+
const promotionPr = waitForPromotionPr(gitContext.repo, args.releasePrTimeout, deps);
|
|
2950
|
+
reporter.ok('release-cycle-promote-pr', `Promotion PR found: #${promotionPr.number}`);
|
|
2951
|
+
summary.actionsPerformed.push(`promotion pr discovered: #${promotionPr.number}`);
|
|
2952
|
+
summary.promotionPr = `found (#${promotionPr.number})`;
|
|
2953
|
+
|
|
2954
|
+
if (args.watchChecks) {
|
|
2955
|
+
reporter.start('release-cycle-promote-checks', `Watching promotion PR checks #${promotionPr.number}...`);
|
|
2956
|
+
watchPrChecks(gitContext.repo, promotionPr.number, args.checkTimeout, deps);
|
|
2957
|
+
reporter.ok('release-cycle-promote-checks', `Promotion PR checks green (#${promotionPr.number}).`);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
if (args.mergeWhenGreen) {
|
|
2961
|
+
reporter.start('release-cycle-promote-merge', `Merging promotion PR #${promotionPr.number}...`);
|
|
2962
|
+
mergePrWhenGreen(gitContext.repo, promotionPr.number, args.mergeMethod, deps);
|
|
2963
|
+
reporter.ok('release-cycle-promote-merge', `Promotion PR #${promotionPr.number} merged.`);
|
|
2964
|
+
summary.actionsPerformed.push(`promotion pr merged: #${promotionPr.number}`);
|
|
2965
|
+
summary.promotionPr = `merged (#${promotionPr.number})`;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
reporter.start('release-cycle-sync-beta', `Syncing local ${DEFAULT_BETA_BRANCH} branch...`);
|
|
2969
|
+
const checkoutBeta = deps.exec('git', ['checkout', DEFAULT_BETA_BRANCH]);
|
|
2970
|
+
if (checkoutBeta.status !== 0) {
|
|
2971
|
+
throw new Error(`Could not checkout ${DEFAULT_BETA_BRANCH}: ${(checkoutBeta.stderr || checkoutBeta.stdout || '').trim()}`);
|
|
2972
|
+
}
|
|
2973
|
+
const pullBeta = deps.exec('git', ['pull']);
|
|
2974
|
+
if (pullBeta.status !== 0) {
|
|
2975
|
+
throw new Error(`Could not pull ${DEFAULT_BETA_BRANCH}: ${(pullBeta.stderr || pullBeta.stdout || '').trim()}`);
|
|
2976
|
+
}
|
|
2977
|
+
reporter.ok('release-cycle-sync-beta', `${DEFAULT_BETA_BRANCH} synced.`);
|
|
2978
|
+
}
|
|
2979
|
+
} else {
|
|
2980
|
+
summary.promotionWorkflow = 'skipped';
|
|
2981
|
+
summary.promotionPr = 'skipped';
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
const canResumeFromMergedCode = args.resume
|
|
2985
|
+
&& !args.promoteStable
|
|
2986
|
+
&& gitContext.head !== DEFAULT_BETA_BRANCH
|
|
2987
|
+
&& !gitContext.head.startsWith('changeset-release/')
|
|
2988
|
+
&& isHeadIntegratedIntoBase('HEAD', DEFAULT_BETA_BRANCH, deps);
|
|
2989
|
+
|
|
2990
|
+
let codePr = null;
|
|
2991
|
+
if (canResumeFromMergedCode) {
|
|
2992
|
+
summary.prAction = 'skipped (resume: code already merged)';
|
|
2993
|
+
summary.prUrl = 'n/a';
|
|
2994
|
+
summary.branchPushed = 'skipped (resume)';
|
|
2995
|
+
summary.autoMerge = 'skipped (resume)';
|
|
2996
|
+
summary.checks = 'skipped (resume)';
|
|
2997
|
+
summary.merge = 'skipped (resume: already merged)';
|
|
2998
|
+
summary.actionsPerformed.push(`resume detected: ${gitContext.head} already integrated into ${DEFAULT_BETA_BRANCH}`);
|
|
2999
|
+
summary.actionsSkipped.push('open/update code pr (resume)');
|
|
3000
|
+
} else {
|
|
3001
|
+
if (!args.promoteStable && gitContext.head !== DEFAULT_BETA_BRANCH && !gitContext.head.startsWith('changeset-release/')) {
|
|
3002
|
+
syncBranchWithBase({
|
|
3003
|
+
deps,
|
|
3004
|
+
headBranch: gitContext.head,
|
|
3005
|
+
baseBranch: DEFAULT_BETA_BRANCH,
|
|
3006
|
+
strategy: args.syncBase,
|
|
3007
|
+
reporter,
|
|
3008
|
+
summary,
|
|
3009
|
+
dryRun: args.dryRun
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
const openPrResult = await runOpenPrFlow(
|
|
3014
|
+
{
|
|
3015
|
+
...args,
|
|
3016
|
+
head: args.promoteStable ? DEFAULT_BETA_BRANCH : args.head,
|
|
3017
|
+
base: args.promoteStable ? DEFAULT_BASE_BRANCH : args.base,
|
|
3018
|
+
autoMerge: useAutoMerge,
|
|
3019
|
+
watchChecks: args.watchChecks,
|
|
3020
|
+
checkTimeout: args.checkTimeout,
|
|
3021
|
+
mergeMethod: args.mergeMethod,
|
|
3022
|
+
updateExistingPr: args.updatePrDescription,
|
|
3023
|
+
skipPush: args.promoteStable,
|
|
3024
|
+
printSummary: false
|
|
3025
|
+
},
|
|
3026
|
+
dependencies
|
|
3027
|
+
);
|
|
3028
|
+
|
|
3029
|
+
codePr = openPrResult.pr;
|
|
3030
|
+
summary.prAction = openPrResult.summary.prAction;
|
|
3031
|
+
summary.prUrl = openPrResult.summary.prUrl;
|
|
3032
|
+
summary.branchPushed = openPrResult.summary.branchPushed;
|
|
3033
|
+
summary.autoMerge = openPrResult.summary.autoMerge;
|
|
3034
|
+
summary.checks = openPrResult.summary.checks;
|
|
3035
|
+
summary.actionsPerformed.push(...openPrResult.summary.actionsPerformed);
|
|
3036
|
+
summary.actionsSkipped.push(...openPrResult.summary.actionsSkipped);
|
|
3037
|
+
summary.warnings.push(...openPrResult.summary.warnings);
|
|
3038
|
+
|
|
3039
|
+
if (args.mergeWhenGreen && codePr && !args.dryRun) {
|
|
3040
|
+
reporter.start('release-cycle-merge-code-ready', `Checking merge readiness for code PR #${codePr.number}...`);
|
|
3041
|
+
const codeReadiness = waitForPrMergeReadinessOrThrow(
|
|
3042
|
+
gitContext.repo,
|
|
3043
|
+
codePr.number,
|
|
3044
|
+
`Code PR #${codePr.number}`,
|
|
3045
|
+
args.checkTimeout,
|
|
3046
|
+
deps
|
|
3047
|
+
);
|
|
3048
|
+
await confirmMergeIfNeeded(args, codeReadiness, `Code PR #${codePr.number}`);
|
|
3049
|
+
reporter.ok('release-cycle-merge-code-ready', `Code PR #${codePr.number} is ready for merge.`);
|
|
3050
|
+
reporter.start('release-cycle-code-auto-merge', `Enabling auto-merge for code PR #${codePr.number}...`);
|
|
3051
|
+
enablePrAutoMerge(gitContext.repo, codePr.number, args.mergeMethod, deps);
|
|
3052
|
+
reporter.ok('release-cycle-code-auto-merge', `Auto-merge enabled for code PR #${codePr.number}.`);
|
|
3053
|
+
reporter.start('release-cycle-wait-code-merge', `Waiting for code PR #${codePr.number} merge...`);
|
|
3054
|
+
waitForPrMerged(gitContext.repo, codePr.number, args.releasePrTimeout, deps);
|
|
3055
|
+
reporter.ok('release-cycle-wait-code-merge', `Code PR #${codePr.number} merged.`);
|
|
3056
|
+
summary.actionsPerformed.push(`code pr merged: #${codePr.number}`);
|
|
3057
|
+
summary.merge = `code pr merged (#${codePr.number})`;
|
|
3058
|
+
} else {
|
|
3059
|
+
summary.merge = args.dryRun ? 'dry-run: would merge code PR' : 'skipped';
|
|
3060
|
+
summary.actionsSkipped.push('merge code pr');
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
if (effectivePhase === 'code') {
|
|
3065
|
+
summary.releasePr = 'skipped (phase=code)';
|
|
3066
|
+
summary.npmValidation = 'skipped (phase=code)';
|
|
3067
|
+
summary.cleanup = 'skipped (phase=code; requires npm validation)';
|
|
3068
|
+
summary.actionsSkipped.push('wait release pr (phase=code)');
|
|
3069
|
+
summary.actionsSkipped.push('verify npm (phase=code)');
|
|
3070
|
+
summary.actionsSkipped.push('cleanup (phase=code)');
|
|
3071
|
+
printOrchestrationSummary(`release-cycle completed in ${detectedMode} mode`, summary);
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
let mergedReleasePr = null;
|
|
3076
|
+
if (args.waitReleasePr) {
|
|
3077
|
+
if (args.dryRun) {
|
|
3078
|
+
summary.releasePr = `dry-run: would wait release PR (${args.releasePrTimeout}m)`;
|
|
3079
|
+
} else {
|
|
3080
|
+
reporter.start('release-cycle-wait-release-pr', 'Waiting for release PR (changeset-release/*)...');
|
|
3081
|
+
const releasePr = waitForReleasePr(gitContext.repo, args.releasePrTimeout, deps, {
|
|
3082
|
+
expectedBase: releaseBaseBranchForTrack(requestedTrack)
|
|
3083
|
+
});
|
|
3084
|
+
reporter.ok('release-cycle-wait-release-pr', `Release PR found: #${releasePr.number}`);
|
|
3085
|
+
summary.releasePr = `found (#${releasePr.number})`;
|
|
3086
|
+
summary.actionsPerformed.push(`release pr discovered: #${releasePr.number}`);
|
|
3087
|
+
|
|
3088
|
+
if (args.watchChecks) {
|
|
3089
|
+
reporter.start('release-cycle-watch-release-checks', `Watching release PR checks #${releasePr.number}...`);
|
|
3090
|
+
watchPrChecks(gitContext.repo, releasePr.number, args.checkTimeout, deps);
|
|
3091
|
+
reporter.ok('release-cycle-watch-release-checks', `Release PR checks green (#${releasePr.number}).`);
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
if (args.mergeReleasePr) {
|
|
3095
|
+
reporter.start('release-cycle-merge-release-ready', `Checking merge readiness for release PR #${releasePr.number}...`);
|
|
3096
|
+
const releaseReadiness = waitForPrMergeReadinessOrThrow(
|
|
3097
|
+
gitContext.repo,
|
|
3098
|
+
releasePr.number,
|
|
3099
|
+
`Release PR #${releasePr.number}`,
|
|
3100
|
+
args.checkTimeout,
|
|
3101
|
+
deps,
|
|
3102
|
+
{ allowBehindTransient: true }
|
|
3103
|
+
);
|
|
3104
|
+
await confirmMergeIfNeeded(args, releaseReadiness, `Release PR #${releasePr.number}`);
|
|
3105
|
+
reporter.ok('release-cycle-merge-release-ready', `Release PR #${releasePr.number} is ready for merge.`);
|
|
3106
|
+
reporter.start('release-cycle-release-auto-merge', `Enabling auto-merge for release PR #${releasePr.number}...`);
|
|
3107
|
+
enablePrAutoMerge(gitContext.repo, releasePr.number, args.mergeMethod, deps);
|
|
3108
|
+
reporter.ok('release-cycle-release-auto-merge', `Auto-merge enabled for release PR #${releasePr.number}.`);
|
|
3109
|
+
reporter.start('release-cycle-release-wait-merge', `Waiting for release PR #${releasePr.number} merge...`);
|
|
3110
|
+
waitForPrMerged(gitContext.repo, releasePr.number, args.releasePrTimeout, deps);
|
|
3111
|
+
reporter.ok('release-cycle-release-wait-merge', `Release PR #${releasePr.number} merged.`);
|
|
3112
|
+
summary.releasePr = `merged (#${releasePr.number})`;
|
|
3113
|
+
summary.actionsPerformed.push(`release pr merged: #${releasePr.number}`);
|
|
3114
|
+
summary.autoMerge = 'enabled (code + release)';
|
|
3115
|
+
mergedReleasePr = releasePr;
|
|
3116
|
+
} else {
|
|
3117
|
+
summary.actionsSkipped.push('merge release pr');
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
} else {
|
|
3121
|
+
summary.releasePr = 'skipped';
|
|
3122
|
+
summary.actionsSkipped.push('wait release pr');
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
let npmValidationPassed = false;
|
|
3126
|
+
if (args.verifyNpm && !args.dryRun && mergedReleasePr) {
|
|
3127
|
+
reporter.start('release-cycle-verify-npm', 'Validating npm publish and dist-tag...');
|
|
3128
|
+
const targetRef = args.promoteStable ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
|
|
3129
|
+
const expectedTag = requestedTrack === 'stable' ? 'latest' : 'beta';
|
|
3130
|
+
const remotePackage = getRemotePackageVersion(gitContext.repo, targetRef, deps);
|
|
3131
|
+
const npmValidation = validateNpmPublishedVersionAndTag(
|
|
3132
|
+
remotePackage.name,
|
|
3133
|
+
remotePackage.version,
|
|
3134
|
+
expectedTag,
|
|
3135
|
+
args.releasePrTimeout,
|
|
3136
|
+
deps
|
|
3137
|
+
);
|
|
3138
|
+
if (npmValidation.status !== 'pass') {
|
|
3139
|
+
summary.npmValidation = `failed (${expectedTag})`;
|
|
3140
|
+
throw new Error(
|
|
3141
|
+
[
|
|
3142
|
+
'npm validation failed after release merge.',
|
|
3143
|
+
`Expected: ${remotePackage.name}@${remotePackage.version} with dist-tag ${expectedTag}`,
|
|
3144
|
+
`Observed version: ${npmValidation.observedVersion || 'n/a'}`,
|
|
3145
|
+
`Observed tag (${expectedTag}): ${npmValidation.observedTagVersion || 'n/a'}`
|
|
3146
|
+
].join('\n')
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
reporter.ok('release-cycle-verify-npm', `${remotePackage.name}@${remotePackage.version} validated on tag ${expectedTag}.`);
|
|
3150
|
+
summary.actionsPerformed.push(`npm validation: ${remotePackage.name}@${remotePackage.version} (${expectedTag})`);
|
|
3151
|
+
summary.npmValidation = `pass (${expectedTag} -> ${remotePackage.version})`;
|
|
3152
|
+
npmValidationPassed = true;
|
|
3153
|
+
} else if (!args.verifyNpm) {
|
|
3154
|
+
summary.actionsSkipped.push('verify npm');
|
|
3155
|
+
summary.npmValidation = 'skipped';
|
|
3156
|
+
} else if (args.dryRun) {
|
|
3157
|
+
summary.npmValidation = 'skipped (dry-run)';
|
|
3158
|
+
} else if (!mergedReleasePr) {
|
|
3159
|
+
summary.npmValidation = 'skipped (release pr not merged)';
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
if (!args.dryRun && npmValidationPassed) {
|
|
3163
|
+
if (args.confirmCleanup && !args.yes) {
|
|
3164
|
+
await confirmOrThrow('Release completed and npm validation passed.\nProceed with local cleanup now?');
|
|
3165
|
+
}
|
|
3166
|
+
runLocalCleanup({
|
|
3167
|
+
deps,
|
|
3168
|
+
originalBranch,
|
|
3169
|
+
targetBaseBranch: requestedTrack === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH,
|
|
3170
|
+
shouldRun: !args.noCleanup,
|
|
3171
|
+
summary,
|
|
3172
|
+
reporter
|
|
3173
|
+
});
|
|
3174
|
+
} else if (!args.dryRun && !npmValidationPassed) {
|
|
3175
|
+
summary.actionsSkipped.push('cleanup (npm validation did not pass)');
|
|
3176
|
+
summary.cleanup = 'skipped (requires npm validation pass)';
|
|
3177
|
+
} else {
|
|
3178
|
+
summary.actionsSkipped.push('cleanup (dry-run)');
|
|
3179
|
+
summary.cleanup = 'skipped (dry-run)';
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
printOrchestrationSummary(`release-cycle completed in ${detectedMode} mode`, summary);
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
const releasePrCandidates = findReleasePrs(gitContext.repo, deps, {
|
|
3187
|
+
expectedBase: args.track === 'auto' ? '' : releaseBaseBranchForTrack(requestedTrack)
|
|
3188
|
+
});
|
|
3189
|
+
const explicitHeadCandidates = args.head
|
|
3190
|
+
? releasePrCandidates.filter((item) => item.headRefName === args.head)
|
|
3191
|
+
: releasePrCandidates;
|
|
3192
|
+
if (explicitHeadCandidates.length !== 1) {
|
|
3193
|
+
throw new Error(
|
|
3194
|
+
explicitHeadCandidates.length === 0
|
|
3195
|
+
? 'No release PR found. Expected an open PR with head changeset-release/*.'
|
|
3196
|
+
: `Ambiguous release PR selection: ${explicitHeadCandidates.map((item) => item.url).join(', ')}`
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
const releasePr = explicitHeadCandidates[0];
|
|
3201
|
+
const effectivePublishTrack = releasePr.baseRefName === DEFAULT_BASE_BRANCH ? 'stable' : 'beta';
|
|
3202
|
+
summary.releaseTrack = effectivePublishTrack;
|
|
3203
|
+
summary.prAction = `selected release pr (#${releasePr.number})`;
|
|
3204
|
+
summary.prUrl = releasePr.url;
|
|
3205
|
+
summary.branchPushed = 'skipped';
|
|
3206
|
+
summary.autoMerge = 'skipped';
|
|
3207
|
+
|
|
3208
|
+
if (args.watchChecks) {
|
|
3209
|
+
if (args.dryRun) {
|
|
3210
|
+
summary.checks = `dry-run: would watch checks (${args.checkTimeout}m)`;
|
|
3211
|
+
} else {
|
|
3212
|
+
reporter.start('release-cycle-publish-checks', `Watching release PR checks #${releasePr.number}...`);
|
|
3213
|
+
watchPrChecks(gitContext.repo, releasePr.number, args.checkTimeout, deps);
|
|
3214
|
+
reporter.ok('release-cycle-publish-checks', `Release PR checks green (#${releasePr.number}).`);
|
|
3215
|
+
summary.checks = 'green';
|
|
3216
|
+
summary.actionsPerformed.push(`release checks watched: #${releasePr.number}`);
|
|
3217
|
+
}
|
|
3218
|
+
} else {
|
|
3219
|
+
summary.checks = 'skipped';
|
|
3220
|
+
summary.actionsSkipped.push('watch release checks');
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
if (args.mergeReleasePr || args.mergeWhenGreen) {
|
|
3224
|
+
if (args.dryRun) {
|
|
3225
|
+
summary.merge = `dry-run: would merge release PR #${releasePr.number}`;
|
|
3226
|
+
summary.releasePr = `dry-run: would merge (#${releasePr.number})`;
|
|
3227
|
+
} else {
|
|
3228
|
+
reporter.start('release-cycle-publish-merge-ready', `Checking merge readiness for release PR #${releasePr.number}...`);
|
|
3229
|
+
const publishReadiness = waitForPrMergeReadinessOrThrow(
|
|
3230
|
+
gitContext.repo,
|
|
3231
|
+
releasePr.number,
|
|
3232
|
+
`Release PR #${releasePr.number}`,
|
|
3233
|
+
args.checkTimeout,
|
|
3234
|
+
deps,
|
|
3235
|
+
{ allowBehindTransient: true }
|
|
3236
|
+
);
|
|
3237
|
+
await confirmMergeIfNeeded(args, publishReadiness, `Release PR #${releasePr.number}`);
|
|
3238
|
+
reporter.ok('release-cycle-publish-merge-ready', `Release PR #${releasePr.number} is ready for merge.`);
|
|
3239
|
+
reporter.start('release-cycle-publish-auto-merge', `Enabling auto-merge for release PR #${releasePr.number}...`);
|
|
3240
|
+
enablePrAutoMerge(gitContext.repo, releasePr.number, args.mergeMethod, deps);
|
|
3241
|
+
reporter.ok('release-cycle-publish-auto-merge', `Auto-merge enabled for release PR #${releasePr.number}.`);
|
|
3242
|
+
reporter.start('release-cycle-publish-wait-merge', `Waiting for release PR #${releasePr.number} merge...`);
|
|
3243
|
+
waitForPrMerged(gitContext.repo, releasePr.number, args.releasePrTimeout, deps);
|
|
3244
|
+
reporter.ok('release-cycle-publish-wait-merge', `Release PR #${releasePr.number} merged.`);
|
|
3245
|
+
summary.merge = `merged release pr (#${releasePr.number})`;
|
|
3246
|
+
summary.releasePr = `merged (#${releasePr.number})`;
|
|
3247
|
+
summary.autoMerge = 'enabled (release)';
|
|
3248
|
+
summary.actionsPerformed.push(`release pr merged: #${releasePr.number}`);
|
|
3249
|
+
}
|
|
3250
|
+
} else {
|
|
3251
|
+
summary.merge = 'skipped';
|
|
3252
|
+
summary.releasePr = `discovered (#${releasePr.number})`;
|
|
3253
|
+
summary.actionsSkipped.push('merge release pr');
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
let npmValidationPassed = false;
|
|
3257
|
+
if (args.verifyNpm && !args.dryRun && (args.mergeReleasePr || args.mergeWhenGreen)) {
|
|
3258
|
+
reporter.start('release-cycle-verify-npm', 'Validating npm publish and dist-tag...');
|
|
3259
|
+
const targetRef = effectivePublishTrack === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
|
|
3260
|
+
const expectedTag = effectivePublishTrack === 'stable' ? 'latest' : 'beta';
|
|
3261
|
+
const remotePackage = getRemotePackageVersion(gitContext.repo, targetRef, deps);
|
|
3262
|
+
const npmValidation = validateNpmPublishedVersionAndTag(
|
|
3263
|
+
remotePackage.name,
|
|
3264
|
+
remotePackage.version,
|
|
3265
|
+
expectedTag,
|
|
3266
|
+
args.releasePrTimeout,
|
|
3267
|
+
deps
|
|
3268
|
+
);
|
|
3269
|
+
if (npmValidation.status !== 'pass') {
|
|
3270
|
+
summary.npmValidation = `failed (${expectedTag})`;
|
|
3271
|
+
throw new Error(
|
|
3272
|
+
[
|
|
3273
|
+
'npm validation failed after release merge.',
|
|
3274
|
+
`Expected: ${remotePackage.name}@${remotePackage.version} with dist-tag ${expectedTag}`,
|
|
3275
|
+
`Observed version: ${npmValidation.observedVersion || 'n/a'}`,
|
|
3276
|
+
`Observed tag (${expectedTag}): ${npmValidation.observedTagVersion || 'n/a'}`
|
|
3277
|
+
].join('\n')
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
reporter.ok('release-cycle-verify-npm', `${remotePackage.name}@${remotePackage.version} validated on tag ${expectedTag}.`);
|
|
3281
|
+
summary.actionsPerformed.push(`npm validation: ${remotePackage.name}@${remotePackage.version} (${expectedTag})`);
|
|
3282
|
+
summary.npmValidation = `pass (${expectedTag} -> ${remotePackage.version})`;
|
|
3283
|
+
npmValidationPassed = true;
|
|
3284
|
+
} else if (!args.verifyNpm) {
|
|
3285
|
+
summary.npmValidation = 'skipped';
|
|
3286
|
+
} else if (args.dryRun) {
|
|
3287
|
+
summary.npmValidation = 'skipped (dry-run)';
|
|
3288
|
+
} else {
|
|
3289
|
+
summary.npmValidation = 'skipped (release pr not merged)';
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
if (!args.dryRun && npmValidationPassed) {
|
|
3293
|
+
if (args.confirmCleanup && !args.yes) {
|
|
3294
|
+
await confirmOrThrow('Release completed and npm validation passed.\nProceed with local cleanup now?');
|
|
3295
|
+
}
|
|
3296
|
+
runLocalCleanup({
|
|
3297
|
+
deps,
|
|
3298
|
+
originalBranch,
|
|
3299
|
+
targetBaseBranch: effectivePublishTrack === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH,
|
|
3300
|
+
shouldRun: !args.noCleanup,
|
|
3301
|
+
summary,
|
|
3302
|
+
reporter
|
|
3303
|
+
});
|
|
3304
|
+
} else if (!args.dryRun && !npmValidationPassed) {
|
|
3305
|
+
summary.actionsSkipped.push('cleanup (npm validation did not pass)');
|
|
3306
|
+
summary.cleanup = 'skipped (requires npm validation pass)';
|
|
3307
|
+
} else {
|
|
3308
|
+
summary.cleanup = 'skipped (dry-run)';
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
printOrchestrationSummary(`release-cycle completed in ${detectedMode} mode`, summary);
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
function parseRepoFromRemote(remoteUrl) {
|
|
3315
|
+
const trimmed = remoteUrl.trim();
|
|
3316
|
+
const httpsMatch = trimmed.match(/github\.com[/:]([^/]+\/[^/.]+)(?:\.git)?$/);
|
|
3317
|
+
|
|
3318
|
+
if (httpsMatch) {
|
|
3319
|
+
return httpsMatch[1];
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
return '';
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
function resolveRepo(args, deps) {
|
|
3326
|
+
if (args.repo) {
|
|
3327
|
+
return args.repo;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
const remote = deps.exec('git', ['config', '--get', 'remote.origin.url']);
|
|
3331
|
+
if (remote.status !== 0 || !remote.stdout.trim()) {
|
|
3332
|
+
throw new Error('Could not infer repository. Use --repo <owner/repo>.');
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
const repo = parseRepoFromRemote(remote.stdout);
|
|
3336
|
+
if (!repo) {
|
|
3337
|
+
throw new Error('Could not parse GitHub repository from remote.origin.url. Use --repo <owner/repo>.');
|
|
607
3338
|
}
|
|
608
3339
|
|
|
609
3340
|
return repo;
|
|
610
3341
|
}
|
|
611
3342
|
|
|
612
|
-
function createBaseRulesetPayload(defaultBranch) {
|
|
613
|
-
return {
|
|
614
|
-
name: DEFAULT_RULESET_NAME,
|
|
615
|
-
target: 'branch',
|
|
616
|
-
enforcement: 'active',
|
|
617
|
-
conditions: {
|
|
618
|
-
ref_name: {
|
|
619
|
-
include: [`refs/heads/${defaultBranch}`],
|
|
620
|
-
exclude: []
|
|
3343
|
+
function createBaseRulesetPayload(defaultBranch) {
|
|
3344
|
+
return {
|
|
3345
|
+
name: DEFAULT_RULESET_NAME,
|
|
3346
|
+
target: 'branch',
|
|
3347
|
+
enforcement: 'active',
|
|
3348
|
+
conditions: {
|
|
3349
|
+
ref_name: {
|
|
3350
|
+
include: [`refs/heads/${defaultBranch}`],
|
|
3351
|
+
exclude: []
|
|
3352
|
+
}
|
|
3353
|
+
},
|
|
3354
|
+
bypass_actors: [],
|
|
3355
|
+
rules: [
|
|
3356
|
+
{ type: 'deletion' },
|
|
3357
|
+
{ type: 'non_fast_forward' },
|
|
3358
|
+
{
|
|
3359
|
+
type: 'pull_request',
|
|
3360
|
+
parameters: {
|
|
3361
|
+
required_approving_review_count: 0,
|
|
3362
|
+
dismiss_stale_reviews_on_push: true,
|
|
3363
|
+
require_code_owner_review: false,
|
|
3364
|
+
require_last_push_approval: false,
|
|
3365
|
+
required_review_thread_resolution: true
|
|
3366
|
+
}
|
|
3367
|
+
},
|
|
3368
|
+
{
|
|
3369
|
+
type: 'required_status_checks',
|
|
3370
|
+
parameters: {
|
|
3371
|
+
strict_required_status_checks_policy: true,
|
|
3372
|
+
required_status_checks: [
|
|
3373
|
+
{
|
|
3374
|
+
context: REQUIRED_CHECK_CONTEXT
|
|
3375
|
+
}
|
|
3376
|
+
]
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
]
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
function createBetaRulesetPayload(betaBranch) {
|
|
3384
|
+
return {
|
|
3385
|
+
name: `Beta branch protection (${betaBranch})`,
|
|
3386
|
+
target: 'branch',
|
|
3387
|
+
enforcement: 'active',
|
|
3388
|
+
conditions: {
|
|
3389
|
+
ref_name: {
|
|
3390
|
+
include: [`refs/heads/${betaBranch}`],
|
|
3391
|
+
exclude: []
|
|
3392
|
+
}
|
|
3393
|
+
},
|
|
3394
|
+
bypass_actors: [],
|
|
3395
|
+
rules: [
|
|
3396
|
+
{ type: 'deletion' },
|
|
3397
|
+
{ type: 'non_fast_forward' },
|
|
3398
|
+
{
|
|
3399
|
+
type: 'pull_request',
|
|
3400
|
+
parameters: {
|
|
3401
|
+
required_approving_review_count: 0,
|
|
3402
|
+
dismiss_stale_reviews_on_push: true,
|
|
3403
|
+
require_code_owner_review: false,
|
|
3404
|
+
require_last_push_approval: false,
|
|
3405
|
+
required_review_thread_resolution: true
|
|
3406
|
+
}
|
|
3407
|
+
},
|
|
3408
|
+
{
|
|
3409
|
+
type: 'required_status_checks',
|
|
3410
|
+
parameters: {
|
|
3411
|
+
strict_required_status_checks_policy: true,
|
|
3412
|
+
required_status_checks: [
|
|
3413
|
+
{
|
|
3414
|
+
context: REQUIRED_CHECK_CONTEXT
|
|
3415
|
+
}
|
|
3416
|
+
]
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
]
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
function createRulesetPayload(args) {
|
|
3424
|
+
if (!args.ruleset) {
|
|
3425
|
+
return createBaseRulesetPayload(args.defaultBranch);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
const rulesetPath = path.resolve(args.ruleset);
|
|
3429
|
+
if (!fs.existsSync(rulesetPath)) {
|
|
3430
|
+
throw new Error(`Ruleset file not found: ${rulesetPath}`);
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
return readJsonFile(rulesetPath);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
function ghApi(deps, method, endpoint, payload) {
|
|
3437
|
+
const args = ['api', '--method', method, endpoint];
|
|
3438
|
+
|
|
3439
|
+
if (payload !== undefined) {
|
|
3440
|
+
args.push('--input', '-');
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
return deps.exec('gh', args, {
|
|
3444
|
+
input: payload !== undefined ? `${JSON.stringify(payload)}\n` : undefined
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
function ensureGhAvailable(deps) {
|
|
3449
|
+
const version = deps.exec('gh', ['--version']);
|
|
3450
|
+
if (version.status !== 0) {
|
|
3451
|
+
throw new Error('GitHub CLI (gh) is required. Install it from https://cli.github.com/ and rerun.');
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
const auth = deps.exec('gh', ['auth', 'status']);
|
|
3455
|
+
if (auth.status !== 0) {
|
|
3456
|
+
throw new Error('GitHub CLI is not authenticated. Run "gh auth login" and rerun.');
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
function parseJsonOutput(output, fallbackError) {
|
|
3461
|
+
try {
|
|
3462
|
+
return JSON.parse(output);
|
|
3463
|
+
} catch (error) {
|
|
3464
|
+
throw new Error(fallbackError);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
function upsertRuleset(deps, repo, rulesetPayload) {
|
|
3469
|
+
const listResult = ghApi(deps, 'GET', `/repos/${repo}/rulesets`);
|
|
3470
|
+
if (listResult.status !== 0) {
|
|
3471
|
+
throw new Error(`Failed to list rulesets: ${listResult.stderr || listResult.stdout}`.trim());
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
const rulesets = parseJsonOutput(listResult.stdout || '[]', 'Failed to parse rulesets response from GitHub API.');
|
|
3475
|
+
const existing = rulesets.find((ruleset) => ruleset.name === rulesetPayload.name);
|
|
3476
|
+
|
|
3477
|
+
if (!existing) {
|
|
3478
|
+
const createResult = ghApi(deps, 'POST', `/repos/${repo}/rulesets`, rulesetPayload);
|
|
3479
|
+
if (createResult.status !== 0) {
|
|
3480
|
+
throw new Error(`Failed to create ruleset: ${createResult.stderr || createResult.stdout}`.trim());
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
return 'created';
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
const updateResult = ghApi(deps, 'PUT', `/repos/${repo}/rulesets/${existing.id}`, rulesetPayload);
|
|
3487
|
+
if (updateResult.status !== 0) {
|
|
3488
|
+
throw new Error(`Failed to update ruleset: ${updateResult.stderr || updateResult.stdout}`.trim());
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
return 'updated';
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
function updateWorkflowPermissions(deps, repo) {
|
|
3495
|
+
const workflowPermissionsPayload = {
|
|
3496
|
+
default_workflow_permissions: 'write',
|
|
3497
|
+
can_approve_pull_request_reviews: true
|
|
3498
|
+
};
|
|
3499
|
+
|
|
3500
|
+
const result = ghApi(
|
|
3501
|
+
deps,
|
|
3502
|
+
'PUT',
|
|
3503
|
+
`/repos/${repo}/actions/permissions/workflow`,
|
|
3504
|
+
workflowPermissionsPayload
|
|
3505
|
+
);
|
|
3506
|
+
|
|
3507
|
+
if (result.status !== 0) {
|
|
3508
|
+
throw new Error(
|
|
3509
|
+
`Failed to update workflow permissions: ${result.stderr || result.stdout}`.trim()
|
|
3510
|
+
);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
function isNotFoundResponse(result) {
|
|
3515
|
+
const output = `${result.stderr || ''}\n${result.stdout || ''}`.toLowerCase();
|
|
3516
|
+
return output.includes('404') || output.includes('not found');
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
function ensureBranchExists(deps, repo, defaultBranch, targetBranch) {
|
|
3520
|
+
const encodedTarget = encodeURIComponent(targetBranch);
|
|
3521
|
+
const getTarget = ghApi(deps, 'GET', `/repos/${repo}/branches/${encodedTarget}`);
|
|
3522
|
+
if (getTarget.status === 0) {
|
|
3523
|
+
return 'exists';
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
if (!isNotFoundResponse(getTarget)) {
|
|
3527
|
+
throw new Error(`Failed to check branch "${targetBranch}": ${getTarget.stderr || getTarget.stdout}`.trim());
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
const encodedDefault = encodeURIComponent(defaultBranch);
|
|
3531
|
+
const getDefaultRef = ghApi(deps, 'GET', `/repos/${repo}/git/ref/heads/${encodedDefault}`);
|
|
3532
|
+
if (getDefaultRef.status !== 0) {
|
|
3533
|
+
throw new Error(`Failed to resolve default branch "${defaultBranch}": ${getDefaultRef.stderr || getDefaultRef.stdout}`.trim());
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
const parsed = parseJsonOutput(getDefaultRef.stdout || '{}', 'Failed to parse default branch ref from GitHub API.');
|
|
3537
|
+
const sha = parsed && parsed.object && parsed.object.sha;
|
|
3538
|
+
if (!sha) {
|
|
3539
|
+
throw new Error(`Could not determine SHA for default branch "${defaultBranch}".`);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
const createRef = ghApi(deps, 'POST', `/repos/${repo}/git/refs`, {
|
|
3543
|
+
ref: `refs/heads/${targetBranch}`,
|
|
3544
|
+
sha
|
|
3545
|
+
});
|
|
3546
|
+
if (createRef.status !== 0) {
|
|
3547
|
+
throw new Error(`Failed to create branch "${targetBranch}": ${createRef.stderr || createRef.stdout}`.trim());
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
return 'created';
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
function branchExists(deps, repo, targetBranch) {
|
|
3554
|
+
const encodedTarget = encodeURIComponent(targetBranch);
|
|
3555
|
+
const getTarget = ghApi(deps, 'GET', `/repos/${repo}/branches/${encodedTarget}`);
|
|
3556
|
+
if (getTarget.status === 0) {
|
|
3557
|
+
return true;
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
if (isNotFoundResponse(getTarget)) {
|
|
3561
|
+
return false;
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
throw new Error(`Failed to check branch "${targetBranch}": ${getTarget.stderr || getTarget.stdout}`.trim());
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
function findRulesetByName(deps, repo, name) {
|
|
3568
|
+
const listResult = ghApi(deps, 'GET', `/repos/${repo}/rulesets`);
|
|
3569
|
+
if (listResult.status !== 0) {
|
|
3570
|
+
throw new Error(`Failed to list rulesets: ${listResult.stderr || listResult.stdout}`.trim());
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
const rulesets = parseJsonOutput(listResult.stdout || '[]', 'Failed to parse rulesets response from GitHub API.');
|
|
3574
|
+
return rulesets.find((ruleset) => ruleset.name === name) || null;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
function listRulesets(deps, repo) {
|
|
3578
|
+
const listResult = ghApi(deps, 'GET', `/repos/${repo}/rulesets`);
|
|
3579
|
+
if (listResult.status !== 0) {
|
|
3580
|
+
throw new Error(`Failed to list rulesets: ${listResult.stderr || listResult.stdout}`.trim());
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
return parseJsonOutput(listResult.stdout || '[]', 'Failed to parse rulesets response from GitHub API.');
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
function listActionsSecretNames(deps, repo) {
|
|
3587
|
+
const listResult = ghApi(deps, 'GET', `/repos/${repo}/actions/secrets?per_page=100`);
|
|
3588
|
+
if (listResult.status !== 0) {
|
|
3589
|
+
throw new Error(`Failed to list Actions secrets: ${listResult.stderr || listResult.stdout}`.trim());
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
const parsed = parseJsonOutput(listResult.stdout || '{}', 'Failed to parse Actions secrets response from GitHub API.');
|
|
3593
|
+
const secrets = Array.isArray(parsed.secrets) ? parsed.secrets : [];
|
|
3594
|
+
return secrets
|
|
3595
|
+
.map((item) => item && item.name)
|
|
3596
|
+
.filter(Boolean);
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
function findMissingReleaseAuthAppSecrets(existingNames) {
|
|
3600
|
+
const nameSet = new Set(existingNames || []);
|
|
3601
|
+
const missing = [...RELEASE_AUTH_APP_REQUIRED_SECRETS].filter((name) => !nameSet.has(name));
|
|
3602
|
+
const hasClientOrAppId = RELEASE_AUTH_APP_ID_SECRETS.some((name) => nameSet.has(name));
|
|
3603
|
+
if (!hasClientOrAppId) {
|
|
3604
|
+
missing.push('GH_APP_CLIENT_ID or GH_APP_ID');
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
return missing;
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
function ensureNpmAvailable(deps) {
|
|
3611
|
+
const version = deps.exec('npm', ['--version']);
|
|
3612
|
+
if (version.status !== 0) {
|
|
3613
|
+
throw new Error('npm CLI is required. Install npm and rerun.');
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
function ensureNpmAuthenticated(deps) {
|
|
3618
|
+
const whoami = deps.exec('npm', ['whoami']);
|
|
3619
|
+
if (whoami.status !== 0) {
|
|
3620
|
+
throw new Error('npm CLI is not authenticated. Run "npm login" and rerun.');
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
function packageExistsOnNpm(deps, packageName) {
|
|
3625
|
+
const view = deps.exec('npm', ['view', packageName, 'version', '--json']);
|
|
3626
|
+
if (view.status === 0) {
|
|
3627
|
+
return true;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
const output = `${view.stderr || ''}\n${view.stdout || ''}`.toLowerCase();
|
|
3631
|
+
if (output.includes('e404') || output.includes('not found') || output.includes('404')) {
|
|
3632
|
+
return false;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
throw new Error(`Failed to check package on npm: ${view.stderr || view.stdout}`.trim());
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
async function resolveInitSelections(args) {
|
|
3639
|
+
const explicit = args.withGithub || args.withNpm || args.withBeta;
|
|
3640
|
+
const selected = {
|
|
3641
|
+
withGithub: args.withGithub,
|
|
3642
|
+
withNpm: args.withNpm,
|
|
3643
|
+
withBeta: args.withBeta
|
|
3644
|
+
};
|
|
3645
|
+
|
|
3646
|
+
if (!explicit) {
|
|
3647
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3648
|
+
selected.withGithub = await askYesNo('Enable GitHub repository setup (rulesets/settings)?', false);
|
|
3649
|
+
selected.withNpm = await askYesNo('Enable npm setup (auth + package check + first publish if needed)?', false);
|
|
3650
|
+
selected.withBeta = selected.withGithub
|
|
3651
|
+
? await askYesNo(`Enable beta flow setup using branch "${args.betaBranch}"?`, true)
|
|
3652
|
+
: false;
|
|
3653
|
+
} else {
|
|
3654
|
+
selected.withGithub = false;
|
|
3655
|
+
selected.withNpm = false;
|
|
3656
|
+
selected.withBeta = false;
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
if (selected.withBeta) {
|
|
3661
|
+
selected.withGithub = true;
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
return selected;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
function summarizePlannedInitActions(selections, args, context) {
|
|
3668
|
+
const lines = [
|
|
3669
|
+
'This init execution will apply:',
|
|
3670
|
+
'- local managed files/scripts/dependencies bootstrap'
|
|
3671
|
+
];
|
|
3672
|
+
|
|
3673
|
+
if (selections.withGithub) {
|
|
3674
|
+
lines.push(`- GitHub main settings/ruleset for ${context.repo}`);
|
|
3675
|
+
}
|
|
3676
|
+
if (selections.withBeta) {
|
|
3677
|
+
lines.push(`- beta branch flow for ${args.betaBranch} (create branch if missing + ruleset + workflow triggers)`);
|
|
3678
|
+
}
|
|
3679
|
+
if (selections.withNpm) {
|
|
3680
|
+
if (context.existsOnNpm) {
|
|
3681
|
+
lines.push(`- npm setup for ${context.packageName} (already published; no first publish)`);
|
|
3682
|
+
} else {
|
|
3683
|
+
lines.push(`- npm setup for ${context.packageName} (first publish will run automatically)`);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
return lines.join('\n');
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
function upsertCiWorkflow(targetPath, templatePath, options) {
|
|
3691
|
+
return upsertReleaseWorkflow(targetPath, templatePath, options);
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
function ensureBetaWorkflowTriggers(targetDir, templateDir, options, summary, reporter) {
|
|
3695
|
+
const workflowRelativePath = '.github/workflows/release.yml';
|
|
3696
|
+
const workflowTemplatePath = path.join(templateDir, workflowRelativePath);
|
|
3697
|
+
const workflowTargetPath = path.join(targetDir, workflowRelativePath);
|
|
3698
|
+
|
|
3699
|
+
const ciWorkflowRelativePath = '.github/workflows/ci.yml';
|
|
3700
|
+
const ciWorkflowTemplatePath = path.join(templateDir, ciWorkflowRelativePath);
|
|
3701
|
+
const ciWorkflowTargetPath = path.join(targetDir, ciWorkflowRelativePath);
|
|
3702
|
+
|
|
3703
|
+
const variables = {
|
|
3704
|
+
PACKAGE_NAME: options.packageName,
|
|
3705
|
+
DEFAULT_BRANCH: options.defaultBranch,
|
|
3706
|
+
BETA_BRANCH: options.betaBranch,
|
|
3707
|
+
SCOPE: options.scope,
|
|
3708
|
+
...buildReleaseAuthVariables(options.releaseAuth || DEFAULT_RELEASE_AUTH)
|
|
3709
|
+
};
|
|
3710
|
+
|
|
3711
|
+
reporter.start('workflow-release', `Ensuring ${workflowRelativePath} includes stable+beta triggers...`);
|
|
3712
|
+
const workflowUpsert = upsertReleaseWorkflow(workflowTargetPath, workflowTemplatePath, {
|
|
3713
|
+
force: options.force,
|
|
3714
|
+
dryRun: options.dryRun,
|
|
3715
|
+
variables
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
if (workflowUpsert.result === 'created') {
|
|
3719
|
+
summary.createdFiles.push(workflowRelativePath);
|
|
3720
|
+
reporter.ok('workflow-release', `${workflowRelativePath} created.`);
|
|
3721
|
+
} else if (workflowUpsert.result === 'overwritten' || workflowUpsert.result === 'updated') {
|
|
3722
|
+
summary.overwrittenFiles.push(workflowRelativePath);
|
|
3723
|
+
reporter.ok('workflow-release', `${workflowRelativePath} updated.`);
|
|
3724
|
+
} else {
|
|
3725
|
+
summary.skippedFiles.push(workflowRelativePath);
|
|
3726
|
+
if (workflowUpsert.warning) {
|
|
3727
|
+
summary.warnings.push(workflowUpsert.warning);
|
|
3728
|
+
reporter.warn('workflow-release', workflowUpsert.warning);
|
|
3729
|
+
} else {
|
|
3730
|
+
reporter.warn('workflow-release', `${workflowRelativePath} already configured; kept as-is.`);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
reporter.start('workflow-ci', `Ensuring ${ciWorkflowRelativePath} includes stable+beta triggers...`);
|
|
3735
|
+
const ciWorkflowUpsert = upsertCiWorkflow(ciWorkflowTargetPath, ciWorkflowTemplatePath, {
|
|
3736
|
+
force: options.force,
|
|
3737
|
+
dryRun: options.dryRun,
|
|
3738
|
+
variables
|
|
3739
|
+
});
|
|
3740
|
+
|
|
3741
|
+
if (ciWorkflowUpsert.result === 'created') {
|
|
3742
|
+
summary.createdFiles.push(ciWorkflowRelativePath);
|
|
3743
|
+
reporter.ok('workflow-ci', `${ciWorkflowRelativePath} created.`);
|
|
3744
|
+
} else if (ciWorkflowUpsert.result === 'overwritten' || ciWorkflowUpsert.result === 'updated') {
|
|
3745
|
+
summary.overwrittenFiles.push(ciWorkflowRelativePath);
|
|
3746
|
+
reporter.ok('workflow-ci', `${ciWorkflowRelativePath} updated.`);
|
|
3747
|
+
} else {
|
|
3748
|
+
summary.skippedFiles.push(ciWorkflowRelativePath);
|
|
3749
|
+
if (ciWorkflowUpsert.warning) {
|
|
3750
|
+
summary.warnings.push(ciWorkflowUpsert.warning);
|
|
3751
|
+
reporter.warn('workflow-ci', ciWorkflowUpsert.warning);
|
|
3752
|
+
} else {
|
|
3753
|
+
reporter.warn('workflow-ci', `${ciWorkflowRelativePath} already configured; kept as-is.`);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
function prevalidateInitExecution(args, selections, dependencies = {}, reporter = new StepReporter()) {
|
|
3759
|
+
const deps = {
|
|
3760
|
+
exec: dependencies.exec || execCommand
|
|
3761
|
+
};
|
|
3762
|
+
|
|
3763
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
3764
|
+
const templateDir = path.join(packageRoot, 'template');
|
|
3765
|
+
const targetDir = path.resolve(args.dir);
|
|
3766
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
3767
|
+
const result = {
|
|
3768
|
+
deps,
|
|
3769
|
+
targetDir,
|
|
3770
|
+
templateDir,
|
|
3771
|
+
packageJsonPath,
|
|
3772
|
+
repo: '',
|
|
3773
|
+
packageName: '',
|
|
3774
|
+
existsOnNpm: true,
|
|
3775
|
+
betaBranchExists: false,
|
|
3776
|
+
existingMainRuleset: null,
|
|
3777
|
+
existingBetaRuleset: null,
|
|
3778
|
+
mainRulesetPayload: null,
|
|
3779
|
+
betaRulesetPayload: createBetaRulesetPayload(args.betaBranch),
|
|
3780
|
+
missingReleaseAuthAppSecrets: []
|
|
3781
|
+
};
|
|
3782
|
+
|
|
3783
|
+
reporter.start('validate-local', 'Validating local project and templates...');
|
|
3784
|
+
if (!fs.existsSync(targetDir)) {
|
|
3785
|
+
reporter.fail('validate-local', `Directory not found: ${targetDir}`);
|
|
3786
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
3790
|
+
reporter.fail('validate-local', `package.json not found in ${targetDir}`);
|
|
3791
|
+
throw new Error(`package.json not found in ${targetDir}`);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
if (!fs.existsSync(templateDir)) {
|
|
3795
|
+
reporter.fail('validate-local', `Template not found in ${templateDir}`);
|
|
3796
|
+
throw new Error(`Template not found in ${templateDir}`);
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
3800
|
+
result.packageName = packageJson.name || packageDirFromName(path.basename(targetDir));
|
|
3801
|
+
reporter.ok('validate-local', 'Local project validation complete.');
|
|
3802
|
+
|
|
3803
|
+
if (selections.withGithub) {
|
|
3804
|
+
reporter.start('validate-gh', 'Validating GitHub CLI and authentication...');
|
|
3805
|
+
ensureGhAvailable(deps);
|
|
3806
|
+
reporter.ok('validate-gh', 'GitHub CLI available and authenticated.');
|
|
3807
|
+
|
|
3808
|
+
reporter.start('resolve-repo', 'Resolving repository target...');
|
|
3809
|
+
result.repo = resolveRepo({ repo: args.repo }, deps);
|
|
3810
|
+
reporter.ok('resolve-repo', `Using repository ${result.repo}.`);
|
|
3811
|
+
|
|
3812
|
+
reporter.start('validate-main-branch', `Checking default branch "${args.defaultBranch}"...`);
|
|
3813
|
+
if (!branchExists(deps, result.repo, args.defaultBranch)) {
|
|
3814
|
+
reporter.fail('validate-main-branch', `Default branch "${args.defaultBranch}" was not found in ${result.repo}.`);
|
|
3815
|
+
throw new Error(`Default branch "${args.defaultBranch}" not found in ${result.repo}.`);
|
|
3816
|
+
}
|
|
3817
|
+
reporter.ok('validate-main-branch', `Default branch "${args.defaultBranch}" found.`);
|
|
3818
|
+
|
|
3819
|
+
reporter.start('validate-rulesets', 'Loading existing GitHub rulesets...');
|
|
3820
|
+
const rulesets = listRulesets(deps, result.repo);
|
|
3821
|
+
result.mainRulesetPayload = createRulesetPayload(args);
|
|
3822
|
+
result.existingMainRuleset = rulesets.find((item) => item.name === result.mainRulesetPayload.name) || null;
|
|
3823
|
+
if (selections.withBeta) {
|
|
3824
|
+
result.existingBetaRuleset = rulesets.find((item) => item.name === result.betaRulesetPayload.name) || null;
|
|
3825
|
+
}
|
|
3826
|
+
reporter.ok('validate-rulesets', 'Ruleset scan completed.');
|
|
3827
|
+
|
|
3828
|
+
if (selections.withBeta) {
|
|
3829
|
+
reporter.start('validate-beta-branch', `Checking beta branch "${args.betaBranch}"...`);
|
|
3830
|
+
result.betaBranchExists = branchExists(deps, result.repo, args.betaBranch);
|
|
3831
|
+
reporter.ok(
|
|
3832
|
+
'validate-beta-branch',
|
|
3833
|
+
result.betaBranchExists
|
|
3834
|
+
? `Beta branch "${args.betaBranch}" already exists.`
|
|
3835
|
+
: `Beta branch "${args.betaBranch}" will be created from "${args.defaultBranch}".`
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
if (args.releaseAuth === 'app') {
|
|
3840
|
+
reporter.start('validate-release-auth-app', 'Checking repository secrets for release-auth app mode...');
|
|
3841
|
+
const secretNames = listActionsSecretNames(deps, result.repo);
|
|
3842
|
+
result.missingReleaseAuthAppSecrets = findMissingReleaseAuthAppSecrets(secretNames);
|
|
3843
|
+
if (result.missingReleaseAuthAppSecrets.length > 0) {
|
|
3844
|
+
reporter.warn('validate-release-auth-app', `Missing app secrets: ${result.missingReleaseAuthAppSecrets.join(', ')}`);
|
|
3845
|
+
} else {
|
|
3846
|
+
reporter.ok('validate-release-auth-app', 'Required app secrets detected.');
|
|
621
3847
|
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
if (selections.withNpm) {
|
|
3852
|
+
reporter.start('validate-npm', 'Validating npm CLI and authentication...');
|
|
3853
|
+
ensureNpmAvailable(deps);
|
|
3854
|
+
ensureNpmAuthenticated(deps);
|
|
3855
|
+
reporter.ok('validate-npm', 'npm CLI available and authenticated.');
|
|
3856
|
+
|
|
3857
|
+
reporter.start('validate-package-publish', `Checking npm package status for ${result.packageName}...`);
|
|
3858
|
+
result.existsOnNpm = packageExistsOnNpm(deps, result.packageName);
|
|
3859
|
+
reporter.ok(
|
|
3860
|
+
'validate-package-publish',
|
|
3861
|
+
result.existsOnNpm
|
|
3862
|
+
? `Package ${result.packageName} already exists on npm.`
|
|
3863
|
+
: `Package ${result.packageName} does not exist on npm; first publish will run.`
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
return result;
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
async function confirmInitPlan(args, selections, context, summary) {
|
|
3871
|
+
const hasExternalActions = selections.withGithub || selections.withNpm || selections.withBeta;
|
|
3872
|
+
const needsLocalForceConfirm = false;
|
|
3873
|
+
|
|
3874
|
+
if (!hasExternalActions && !needsLocalForceConfirm) {
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
if (args.yes) {
|
|
3879
|
+
summary.warnings.push('Confirmation prompts skipped due to --yes.');
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
const details = [summarizePlannedInitActions(selections, args, context)];
|
|
3883
|
+
|
|
3884
|
+
if (args.force) {
|
|
3885
|
+
details.push('- --force will overwrite managed files/scripts/dependencies when applicable.');
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
if (selections.withGithub && context.existingMainRuleset) {
|
|
3889
|
+
details.push(`- Ruleset "${context.mainRulesetPayload.name}" already exists and will be overwritten.`);
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
if (selections.withBeta && context.betaBranchExists) {
|
|
3893
|
+
details.push(`- Branch "${args.betaBranch}" already exists and will be used as beta release flow branch.`);
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
if (selections.withBeta && context.existingBetaRuleset) {
|
|
3897
|
+
details.push(`- Ruleset "${context.betaRulesetPayload.name}" already exists and will be overwritten.`);
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
if (args.releaseAuth === 'app' && context.missingReleaseAuthAppSecrets.length > 0) {
|
|
3901
|
+
details.push(`- release-auth app mode is missing secrets: ${context.missingReleaseAuthAppSecrets.join(', ')}`);
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
await confirmOrThrow(details.join('\n'));
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
function runNpmSetup(args, dependencies = {}, options = {}) {
|
|
3908
|
+
const deps = {
|
|
3909
|
+
exec: dependencies.exec || execCommand
|
|
3910
|
+
};
|
|
3911
|
+
const reporter = options.reporter || new StepReporter();
|
|
3912
|
+
const summary = options.summary || createSummary();
|
|
3913
|
+
|
|
3914
|
+
const targetDir = path.resolve(args.dir);
|
|
3915
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
3916
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
3917
|
+
const publishMissingByDefault = Boolean(options.publishMissingByDefault);
|
|
3918
|
+
const shouldPublishFirst = args.publishFirst || publishMissingByDefault;
|
|
3919
|
+
|
|
3920
|
+
reporter.start('npm-auth', 'Checking npm authentication...');
|
|
3921
|
+
ensureNpmAvailable(deps);
|
|
3922
|
+
ensureNpmAuthenticated(deps);
|
|
3923
|
+
reporter.ok('npm-auth', 'npm authentication validated.');
|
|
3924
|
+
|
|
3925
|
+
summary.updatedScriptKeys.push('npm.auth', 'npm.package.lookup');
|
|
3926
|
+
|
|
3927
|
+
if (!packageJson.publishConfig || packageJson.publishConfig.access !== 'public') {
|
|
3928
|
+
summary.warnings.push('package.json publishConfig.access is not "public". First publish may fail for public packages.');
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
reporter.start('npm-exists', `Checking whether ${packageJson.name} exists on npm...`);
|
|
3932
|
+
const existsOnNpm = packageExistsOnNpm(deps, packageJson.name);
|
|
3933
|
+
reporter.ok(
|
|
3934
|
+
'npm-exists',
|
|
3935
|
+
existsOnNpm
|
|
3936
|
+
? `Package ${packageJson.name} already exists on npm.`
|
|
3937
|
+
: `Package ${packageJson.name} is not published on npm yet.`
|
|
3938
|
+
);
|
|
3939
|
+
|
|
3940
|
+
if (existsOnNpm) {
|
|
3941
|
+
summary.skippedScriptKeys.push('npm.first_publish');
|
|
3942
|
+
summary.warnings.push(`Package "${packageJson.name}" already exists. First publish is not required.`);
|
|
3943
|
+
} else {
|
|
3944
|
+
summary.updatedScriptKeys.push('npm.first_publish_required');
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
if (!existsOnNpm && !shouldPublishFirst) {
|
|
3948
|
+
summary.warnings.push(`package "${packageJson.name}" was not found on npm. Run "create-package-starter setup-npm --dir ${targetDir} --publish-first" to perform first publish.`);
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
if (!existsOnNpm && shouldPublishFirst) {
|
|
3952
|
+
if (args.dryRun) {
|
|
3953
|
+
summary.warnings.push(`dry-run: would run "npm publish --access public" in ${targetDir}`);
|
|
3954
|
+
} else {
|
|
3955
|
+
reporter.start('npm-publish', `Publishing first version of ${packageJson.name}...`);
|
|
3956
|
+
const publish = deps.exec('npm', ['publish', '--access', 'public'], { cwd: targetDir, stdio: 'inherit' });
|
|
3957
|
+
if (publish.status !== 0) {
|
|
3958
|
+
reporter.fail('npm-publish', 'First publish failed.');
|
|
3959
|
+
const publishOutput = `${publish.stderr || ''}\n${publish.stdout || ''}`.toLowerCase();
|
|
3960
|
+
const isOtpError = publishOutput.includes('eotp') || publishOutput.includes('one-time password');
|
|
3961
|
+
|
|
3962
|
+
if (isOtpError) {
|
|
3963
|
+
throw new Error(
|
|
3964
|
+
[
|
|
3965
|
+
'First publish failed due to npm 2FA/OTP requirements.',
|
|
3966
|
+
'This command already delegates to the standard npm publish flow.',
|
|
3967
|
+
'If npm still requires manual OTP entry, complete publish manually:',
|
|
3968
|
+
` (cd ${targetDir} && npm publish --access public)`
|
|
3969
|
+
].join('\n')
|
|
3970
|
+
);
|
|
635
3971
|
}
|
|
3972
|
+
|
|
3973
|
+
throw new Error('First publish failed. Check npm output above and try again.');
|
|
636
3974
|
}
|
|
637
|
-
]
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
3975
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
3976
|
+
reporter.ok('npm-publish', `First publish for ${packageJson.name} completed.`);
|
|
3977
|
+
summary.updatedScriptKeys.push('npm.first_publish_done');
|
|
3978
|
+
}
|
|
644
3979
|
}
|
|
645
3980
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
throw new Error(`Ruleset file not found: ${rulesetPath}`);
|
|
649
|
-
}
|
|
3981
|
+
summary.warnings.push('Configure npm Trusted Publisher manually in npm package settings after first publish.');
|
|
3982
|
+
summary.warnings.push('Trusted Publisher requires owner, repository, workflow file (.github/workflows/release.yml), and branch (main by default).');
|
|
650
3983
|
|
|
651
|
-
return
|
|
3984
|
+
return summary;
|
|
652
3985
|
}
|
|
653
3986
|
|
|
654
|
-
function
|
|
655
|
-
const
|
|
3987
|
+
function setupNpm(args, dependencies = {}) {
|
|
3988
|
+
const targetDir = path.resolve(args.dir);
|
|
3989
|
+
if (!fs.existsSync(targetDir)) {
|
|
3990
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
3991
|
+
}
|
|
656
3992
|
|
|
657
|
-
|
|
658
|
-
|
|
3993
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
3994
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
3995
|
+
throw new Error(`package.json not found in ${targetDir}`);
|
|
659
3996
|
}
|
|
660
3997
|
|
|
661
|
-
|
|
662
|
-
|
|
3998
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
3999
|
+
if (!packageJson.name) {
|
|
4000
|
+
throw new Error(`package.json in ${targetDir} must define "name".`);
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
const summary = runNpmSetup(args, dependencies, {
|
|
4004
|
+
reporter: new StepReporter(),
|
|
4005
|
+
publishMissingByDefault: false
|
|
663
4006
|
});
|
|
4007
|
+
printSummary(`npm setup completed for ${packageJson.name}`, summary);
|
|
664
4008
|
}
|
|
665
4009
|
|
|
666
|
-
function
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
4010
|
+
async function setupBeta(args, dependencies = {}) {
|
|
4011
|
+
const deps = {
|
|
4012
|
+
exec: dependencies.exec || execCommand
|
|
4013
|
+
};
|
|
4014
|
+
|
|
4015
|
+
const targetDir = path.resolve(args.dir);
|
|
4016
|
+
if (!fs.existsSync(targetDir)) {
|
|
4017
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
670
4018
|
}
|
|
671
4019
|
|
|
672
|
-
const
|
|
673
|
-
if (
|
|
674
|
-
throw new Error(
|
|
4020
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
4021
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
4022
|
+
throw new Error(`package.json not found in ${targetDir}`);
|
|
675
4023
|
}
|
|
676
|
-
}
|
|
677
4024
|
|
|
678
|
-
|
|
4025
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
4026
|
+
const templateDir = path.join(packageRoot, 'template');
|
|
4027
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
4028
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
4029
|
+
|
|
4030
|
+
logStep('run', 'Checking GitHub CLI availability and authentication...');
|
|
679
4031
|
try {
|
|
680
|
-
|
|
4032
|
+
ensureGhAvailable(deps);
|
|
4033
|
+
logStep('ok', 'GitHub CLI is available and authenticated.');
|
|
681
4034
|
} catch (error) {
|
|
682
|
-
|
|
4035
|
+
logStep('err', error.message);
|
|
4036
|
+
if (error.message.includes('not authenticated')) {
|
|
4037
|
+
logStep('warn', 'Run "gh auth login" and retry.');
|
|
4038
|
+
}
|
|
4039
|
+
throw error;
|
|
683
4040
|
}
|
|
684
|
-
}
|
|
685
4041
|
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
throw new Error(`Failed to list rulesets: ${listResult.stderr || listResult.stdout}`.trim());
|
|
690
|
-
}
|
|
4042
|
+
logStep('run', 'Resolving repository target...');
|
|
4043
|
+
const repo = resolveRepo(args, deps);
|
|
4044
|
+
logStep('ok', `Using repository ${repo}.`);
|
|
691
4045
|
|
|
692
|
-
const
|
|
693
|
-
|
|
4046
|
+
const summary = createSummary();
|
|
4047
|
+
await resolveReleaseAuthSelection(args, summary, { contextLabel: 'Select release auth mode for beta flow' });
|
|
4048
|
+
summary.updatedScriptKeys.push('github.beta_branch', 'github.beta_ruleset', 'actions.default_workflow_permissions');
|
|
4049
|
+
summary.updatedScriptKeys.push(`release.auth:${args.releaseAuth}`);
|
|
4050
|
+
const releaseAuthVariables = buildReleaseAuthVariables(args.releaseAuth || DEFAULT_RELEASE_AUTH);
|
|
4051
|
+
const desiredScripts = {
|
|
4052
|
+
'beta:enter': 'changeset pre enter beta',
|
|
4053
|
+
'beta:exit': 'changeset pre exit',
|
|
4054
|
+
'beta:version': 'changeset version',
|
|
4055
|
+
'beta:publish': 'changeset publish',
|
|
4056
|
+
'beta:promote': 'create-package-starter promote-stable --dir .'
|
|
4057
|
+
};
|
|
694
4058
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
4059
|
+
let packageJsonChanged = false;
|
|
4060
|
+
for (const [key, value] of Object.entries(desiredScripts)) {
|
|
4061
|
+
const exists = Object.prototype.hasOwnProperty.call(packageJson.scripts, key);
|
|
4062
|
+
if (!exists || args.force) {
|
|
4063
|
+
if (!exists || packageJson.scripts[key] !== value) {
|
|
4064
|
+
packageJson.scripts[key] = value;
|
|
4065
|
+
packageJsonChanged = true;
|
|
4066
|
+
}
|
|
4067
|
+
summary.updatedScriptKeys.push(key);
|
|
4068
|
+
} else {
|
|
4069
|
+
summary.skippedScriptKeys.push(key);
|
|
699
4070
|
}
|
|
4071
|
+
}
|
|
700
4072
|
|
|
701
|
-
|
|
4073
|
+
const workflowRelativePath = '.github/workflows/release.yml';
|
|
4074
|
+
const workflowTemplatePath = path.join(templateDir, workflowRelativePath);
|
|
4075
|
+
const workflowTargetPath = path.join(targetDir, workflowRelativePath);
|
|
4076
|
+
const ciWorkflowRelativePath = '.github/workflows/ci.yml';
|
|
4077
|
+
const ciWorkflowTemplatePath = path.join(templateDir, ciWorkflowRelativePath);
|
|
4078
|
+
const ciWorkflowTargetPath = path.join(targetDir, ciWorkflowRelativePath);
|
|
4079
|
+
if (!fs.existsSync(workflowTemplatePath)) {
|
|
4080
|
+
throw new Error(`Template not found: ${workflowTemplatePath}`);
|
|
4081
|
+
}
|
|
4082
|
+
if (!fs.existsSync(ciWorkflowTemplatePath)) {
|
|
4083
|
+
throw new Error(`Template not found: ${ciWorkflowTemplatePath}`);
|
|
702
4084
|
}
|
|
703
4085
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
4086
|
+
let missingAppSecrets = [];
|
|
4087
|
+
let appSecretsChecked = false;
|
|
4088
|
+
if (args.releaseAuth === 'app') {
|
|
4089
|
+
try {
|
|
4090
|
+
const secretNames = listActionsSecretNames(deps, repo);
|
|
4091
|
+
missingAppSecrets = findMissingReleaseAuthAppSecrets(secretNames);
|
|
4092
|
+
appSecretsChecked = true;
|
|
4093
|
+
} catch (error) {
|
|
4094
|
+
summary.warnings.push(`Could not validate release-auth app secrets: ${error.message}`);
|
|
4095
|
+
}
|
|
707
4096
|
}
|
|
4097
|
+
appendReleaseAuthWarnings(summary, args.releaseAuth, { missingAppSecrets, appSecretsChecked });
|
|
708
4098
|
|
|
709
|
-
|
|
710
|
-
|
|
4099
|
+
if (args.dryRun) {
|
|
4100
|
+
logStep('warn', 'Dry-run mode enabled. No remote or file changes will be applied.');
|
|
4101
|
+
const workflowPreview = upsertReleaseWorkflow(workflowTargetPath, workflowTemplatePath, {
|
|
4102
|
+
force: args.force,
|
|
4103
|
+
dryRun: true,
|
|
4104
|
+
variables: {
|
|
4105
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
4106
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
4107
|
+
BETA_BRANCH: args.betaBranch,
|
|
4108
|
+
SCOPE: deriveScope('', packageJson.name || ''),
|
|
4109
|
+
...releaseAuthVariables
|
|
4110
|
+
}
|
|
4111
|
+
});
|
|
4112
|
+
if (workflowPreview.result === 'created') {
|
|
4113
|
+
summary.warnings.push(`dry-run: would create ${workflowRelativePath}`);
|
|
4114
|
+
} else if (workflowPreview.result === 'overwritten') {
|
|
4115
|
+
summary.warnings.push(`dry-run: would overwrite ${workflowRelativePath}`);
|
|
4116
|
+
} else if (workflowPreview.result === 'updated') {
|
|
4117
|
+
summary.warnings.push(`dry-run: would update ${workflowRelativePath} trigger branches`);
|
|
4118
|
+
} else {
|
|
4119
|
+
summary.warnings.push(`dry-run: would keep existing ${workflowRelativePath}`);
|
|
4120
|
+
if (workflowPreview.warning) {
|
|
4121
|
+
summary.warnings.push(`dry-run: ${workflowPreview.warning}`);
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
const ciWorkflowPreview = upsertReleaseWorkflow(ciWorkflowTargetPath, ciWorkflowTemplatePath, {
|
|
4125
|
+
force: args.force,
|
|
4126
|
+
dryRun: true,
|
|
4127
|
+
variables: {
|
|
4128
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
4129
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
4130
|
+
BETA_BRANCH: args.betaBranch,
|
|
4131
|
+
SCOPE: deriveScope('', packageJson.name || ''),
|
|
4132
|
+
...releaseAuthVariables
|
|
4133
|
+
}
|
|
4134
|
+
});
|
|
4135
|
+
if (ciWorkflowPreview.result === 'created') {
|
|
4136
|
+
summary.warnings.push(`dry-run: would create ${ciWorkflowRelativePath}`);
|
|
4137
|
+
} else if (ciWorkflowPreview.result === 'overwritten') {
|
|
4138
|
+
summary.warnings.push(`dry-run: would overwrite ${ciWorkflowRelativePath}`);
|
|
4139
|
+
} else if (ciWorkflowPreview.result === 'updated') {
|
|
4140
|
+
summary.warnings.push(`dry-run: would update ${ciWorkflowRelativePath} trigger branches`);
|
|
4141
|
+
} else {
|
|
4142
|
+
summary.warnings.push(`dry-run: would keep existing ${ciWorkflowRelativePath}`);
|
|
4143
|
+
if (ciWorkflowPreview.warning) {
|
|
4144
|
+
summary.warnings.push(`dry-run: ${ciWorkflowPreview.warning}`);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
if (packageJsonChanged) {
|
|
4148
|
+
summary.warnings.push('dry-run: would update package.json beta scripts');
|
|
4149
|
+
}
|
|
4150
|
+
summary.warnings.push(`dry-run: would ensure branch "${args.betaBranch}" exists in ${repo}`);
|
|
4151
|
+
summary.warnings.push(`dry-run: would upsert ruleset for refs/heads/${args.betaBranch}`);
|
|
4152
|
+
summary.warnings.push(`dry-run: would set Actions workflow permissions to write for ${repo}`);
|
|
4153
|
+
summary.warnings.push(`dry-run: beta branch configured as ${args.betaBranch}`);
|
|
4154
|
+
} else {
|
|
4155
|
+
const betaRulesetPayload = createBetaRulesetPayload(args.betaBranch);
|
|
4156
|
+
const doesBranchExist = branchExists(deps, repo, args.betaBranch);
|
|
4157
|
+
const existingRuleset = findRulesetByName(deps, repo, betaRulesetPayload.name);
|
|
711
4158
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
4159
|
+
if (args.yes) {
|
|
4160
|
+
logStep('warn', 'Confirmation prompts skipped due to --yes.');
|
|
4161
|
+
} else {
|
|
4162
|
+
const confirmDetails = [
|
|
4163
|
+
`This will modify GitHub repository settings for ${repo}:`,
|
|
4164
|
+
'- set Actions workflow permissions to write',
|
|
4165
|
+
`- ensure branch "${args.betaBranch}" exists${doesBranchExist ? ' (already exists)' : ' (will be created)'}`,
|
|
4166
|
+
`- apply branch protection ruleset "${betaRulesetPayload.name}"`,
|
|
4167
|
+
`- require CI status check "${REQUIRED_CHECK_CONTEXT}" on beta branch`,
|
|
4168
|
+
`- update local ${workflowRelativePath} and package.json beta scripts`
|
|
4169
|
+
];
|
|
4170
|
+
|
|
4171
|
+
if (existingRuleset) {
|
|
4172
|
+
confirmDetails.push(`- existing ruleset "${betaRulesetPayload.name}" will be overwritten`);
|
|
4173
|
+
}
|
|
717
4174
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
`/repos/${repo}/actions/permissions/workflow`,
|
|
722
|
-
workflowPermissionsPayload
|
|
723
|
-
);
|
|
4175
|
+
if (missingAppSecrets.length > 0) {
|
|
4176
|
+
confirmDetails.push(`- release-auth app mode is missing secrets: ${missingAppSecrets.join(', ')}`);
|
|
4177
|
+
}
|
|
724
4178
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
`Failed to update workflow permissions: ${result.stderr || result.stdout}`.trim()
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
4179
|
+
await confirmOrThrow(confirmDetails.join('\n'));
|
|
4180
|
+
}
|
|
731
4181
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
4182
|
+
logStep('run', `Ensuring ${workflowRelativePath} includes stable+beta triggers...`);
|
|
4183
|
+
const workflowUpsert = upsertReleaseWorkflow(workflowTargetPath, workflowTemplatePath, {
|
|
4184
|
+
force: args.force,
|
|
4185
|
+
dryRun: false,
|
|
4186
|
+
variables: {
|
|
4187
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
4188
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
4189
|
+
BETA_BRANCH: args.betaBranch,
|
|
4190
|
+
SCOPE: deriveScope('', packageJson.name || ''),
|
|
4191
|
+
...releaseAuthVariables
|
|
4192
|
+
}
|
|
4193
|
+
});
|
|
4194
|
+
const workflowResult = workflowUpsert.result;
|
|
4195
|
+
|
|
4196
|
+
if (workflowResult === 'created') {
|
|
4197
|
+
summary.createdFiles.push(workflowRelativePath);
|
|
4198
|
+
logStep('ok', `${workflowRelativePath} created.`);
|
|
4199
|
+
} else if (workflowResult === 'overwritten') {
|
|
4200
|
+
summary.overwrittenFiles.push(workflowRelativePath);
|
|
4201
|
+
logStep('ok', `${workflowRelativePath} overwritten.`);
|
|
4202
|
+
} else if (workflowResult === 'updated') {
|
|
4203
|
+
summary.overwrittenFiles.push(workflowRelativePath);
|
|
4204
|
+
logStep('ok', `${workflowRelativePath} updated with missing branch triggers.`);
|
|
4205
|
+
} else {
|
|
4206
|
+
summary.skippedFiles.push(workflowRelativePath);
|
|
4207
|
+
if (workflowUpsert.warning) {
|
|
4208
|
+
summary.warnings.push(workflowUpsert.warning);
|
|
4209
|
+
logStep('warn', workflowUpsert.warning);
|
|
4210
|
+
} else {
|
|
4211
|
+
logStep('warn', `${workflowRelativePath} already configured; kept as-is.`);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
738
4214
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
4215
|
+
logStep('run', `Ensuring ${ciWorkflowRelativePath} includes stable+beta triggers...`);
|
|
4216
|
+
const ciWorkflowUpsert = upsertReleaseWorkflow(ciWorkflowTargetPath, ciWorkflowTemplatePath, {
|
|
4217
|
+
force: args.force,
|
|
4218
|
+
dryRun: false,
|
|
4219
|
+
variables: {
|
|
4220
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
4221
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
4222
|
+
BETA_BRANCH: args.betaBranch,
|
|
4223
|
+
SCOPE: deriveScope('', packageJson.name || ''),
|
|
4224
|
+
...releaseAuthVariables
|
|
4225
|
+
}
|
|
4226
|
+
});
|
|
4227
|
+
const ciWorkflowResult = ciWorkflowUpsert.result;
|
|
4228
|
+
if (ciWorkflowResult === 'created') {
|
|
4229
|
+
summary.createdFiles.push(ciWorkflowRelativePath);
|
|
4230
|
+
logStep('ok', `${ciWorkflowRelativePath} created.`);
|
|
4231
|
+
} else if (ciWorkflowResult === 'overwritten') {
|
|
4232
|
+
summary.overwrittenFiles.push(ciWorkflowRelativePath);
|
|
4233
|
+
logStep('ok', `${ciWorkflowRelativePath} overwritten.`);
|
|
4234
|
+
} else if (ciWorkflowResult === 'updated') {
|
|
4235
|
+
summary.overwrittenFiles.push(ciWorkflowRelativePath);
|
|
4236
|
+
logStep('ok', `${ciWorkflowRelativePath} updated with missing branch triggers.`);
|
|
4237
|
+
} else {
|
|
4238
|
+
summary.skippedFiles.push(ciWorkflowRelativePath);
|
|
4239
|
+
if (ciWorkflowUpsert.warning) {
|
|
4240
|
+
summary.warnings.push(ciWorkflowUpsert.warning);
|
|
4241
|
+
logStep('warn', ciWorkflowUpsert.warning);
|
|
4242
|
+
} else {
|
|
4243
|
+
logStep('warn', `${ciWorkflowRelativePath} already configured; kept as-is.`);
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
745
4246
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
4247
|
+
if (packageJsonChanged) {
|
|
4248
|
+
logStep('run', 'Updating package.json beta scripts...');
|
|
4249
|
+
writeJsonFile(packageJsonPath, packageJson);
|
|
4250
|
+
logStep('ok', 'package.json beta scripts updated.');
|
|
4251
|
+
} else {
|
|
4252
|
+
logStep('warn', 'package.json beta scripts already present; no changes needed.');
|
|
4253
|
+
}
|
|
751
4254
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
4255
|
+
logStep('run', 'Applying GitHub Actions workflow permissions...');
|
|
4256
|
+
updateWorkflowPermissions(deps, repo);
|
|
4257
|
+
logStep('ok', 'Workflow permissions configured.');
|
|
4258
|
+
|
|
4259
|
+
logStep('run', `Ensuring branch "${args.betaBranch}" exists...`);
|
|
4260
|
+
const branchResult = ensureBranchExists(deps, repo, args.defaultBranch, args.betaBranch);
|
|
4261
|
+
if (branchResult === 'created') {
|
|
4262
|
+
summary.createdFiles.push(`github-branch:${args.betaBranch}`);
|
|
4263
|
+
logStep('ok', `Branch "${args.betaBranch}" created from "${args.defaultBranch}".`);
|
|
4264
|
+
} else {
|
|
4265
|
+
summary.skippedFiles.push(`github-branch:${args.betaBranch}`);
|
|
4266
|
+
logStep('warn', `Branch "${args.betaBranch}" already exists.`);
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
logStep('run', `Applying protection ruleset to "${args.betaBranch}"...`);
|
|
4270
|
+
const upsertResult = upsertRuleset(deps, repo, betaRulesetPayload);
|
|
4271
|
+
summary.overwrittenFiles.push(`github-beta-ruleset:${upsertResult}`);
|
|
4272
|
+
logStep('ok', `Beta branch ruleset ${upsertResult}.`);
|
|
755
4273
|
}
|
|
756
4274
|
|
|
757
|
-
|
|
4275
|
+
summary.warnings.push(`Trusted Publisher supports a single workflow file per package. Keep publishing on .github/workflows/release.yml for both stable and beta.`);
|
|
4276
|
+
summary.warnings.push(`Next step: run "npm run beta:enter" once on "${args.betaBranch}", commit .changeset/pre.json, and push.`);
|
|
4277
|
+
printSummary(`beta setup completed for ${targetDir}`, summary);
|
|
758
4278
|
}
|
|
759
4279
|
|
|
760
|
-
function
|
|
4280
|
+
function createChangesetFile(targetDir, packageName, bumpType, summaryText) {
|
|
4281
|
+
const changesetDir = path.join(targetDir, '.changeset');
|
|
4282
|
+
fs.mkdirSync(changesetDir, { recursive: true });
|
|
4283
|
+
const fileName = `promote-stable-${Date.now()}.md`;
|
|
4284
|
+
const filePath = path.join(changesetDir, fileName);
|
|
4285
|
+
const content = [
|
|
4286
|
+
'---',
|
|
4287
|
+
`"${packageName}": ${bumpType}`,
|
|
4288
|
+
'---',
|
|
4289
|
+
'',
|
|
4290
|
+
summaryText
|
|
4291
|
+
].join('\n');
|
|
4292
|
+
fs.writeFileSync(filePath, `${content}\n`);
|
|
4293
|
+
return path.posix.join('.changeset', fileName);
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
function promoteStable(args, dependencies = {}) {
|
|
761
4297
|
const deps = {
|
|
762
4298
|
exec: dependencies.exec || execCommand
|
|
763
4299
|
};
|
|
@@ -772,62 +4308,44 @@ function setupNpm(args, dependencies = {}) {
|
|
|
772
4308
|
throw new Error(`package.json not found in ${targetDir}`);
|
|
773
4309
|
}
|
|
774
4310
|
|
|
4311
|
+
const prePath = path.join(targetDir, '.changeset', 'pre.json');
|
|
4312
|
+
if (!fs.existsSync(prePath)) {
|
|
4313
|
+
throw new Error(`No prerelease state found in ${targetDir}. Run "changeset pre enter beta" first.`);
|
|
4314
|
+
}
|
|
4315
|
+
|
|
775
4316
|
const packageJson = readJsonFile(packageJsonPath);
|
|
776
4317
|
if (!packageJson.name) {
|
|
777
4318
|
throw new Error(`package.json in ${targetDir} must define "name".`);
|
|
778
4319
|
}
|
|
779
4320
|
|
|
780
|
-
ensureNpmAvailable(deps);
|
|
781
|
-
ensureNpmAuthenticated(deps);
|
|
782
|
-
|
|
783
4321
|
const summary = createSummary();
|
|
784
|
-
summary.updatedScriptKeys.push('
|
|
785
|
-
|
|
786
|
-
if (!packageJson.publishConfig || packageJson.publishConfig.access !== 'public') {
|
|
787
|
-
summary.warnings.push('package.json publishConfig.access is not "public". First publish may fail for public packages.');
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const existsOnNpm = packageExistsOnNpm(deps, packageJson.name);
|
|
791
|
-
if (existsOnNpm) {
|
|
792
|
-
summary.skippedScriptKeys.push('npm.first_publish');
|
|
793
|
-
} else {
|
|
794
|
-
summary.updatedScriptKeys.push('npm.first_publish_required');
|
|
795
|
-
}
|
|
4322
|
+
summary.updatedScriptKeys.push('changeset.pre_exit', 'changeset.promote_stable');
|
|
796
4323
|
|
|
797
|
-
if (
|
|
798
|
-
summary.warnings.push(`
|
|
4324
|
+
if (args.dryRun) {
|
|
4325
|
+
summary.warnings.push(`dry-run: would run "npx @changesets/cli pre exit" in ${targetDir}`);
|
|
4326
|
+
summary.warnings.push(`dry-run: would create promotion changeset for ${packageJson.name} (${args.type})`);
|
|
4327
|
+
summary.warnings.push(`dry-run: promote flow targets stable branch ${DEFAULT_BASE_BRANCH}`);
|
|
4328
|
+
printSummary(`stable promotion dry-run for ${targetDir}`, summary);
|
|
4329
|
+
return;
|
|
799
4330
|
}
|
|
800
4331
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
} else if (args.dryRun) {
|
|
805
|
-
summary.warnings.push(`dry-run: would run "npm publish --access public" in ${targetDir}`);
|
|
806
|
-
} else {
|
|
807
|
-
const publish = deps.exec('npm', ['publish', '--access', 'public'], { cwd: targetDir });
|
|
808
|
-
if (publish.status !== 0) {
|
|
809
|
-
throw new Error(`First publish failed: ${(publish.stderr || publish.stdout || '').trim()}`);
|
|
810
|
-
}
|
|
811
|
-
summary.updatedScriptKeys.push('npm.first_publish_done');
|
|
812
|
-
}
|
|
4332
|
+
const preExit = deps.exec('npx', ['@changesets/cli', 'pre', 'exit'], { cwd: targetDir });
|
|
4333
|
+
if (preExit.status !== 0) {
|
|
4334
|
+
throw new Error(`Failed to exit prerelease mode: ${(preExit.stderr || preExit.stdout || '').trim()}`);
|
|
813
4335
|
}
|
|
814
4336
|
|
|
815
|
-
|
|
816
|
-
summary.
|
|
817
|
-
|
|
818
|
-
printSummary(`
|
|
4337
|
+
const createdChangeset = createChangesetFile(targetDir, packageJson.name, args.type, args.summary);
|
|
4338
|
+
summary.createdFiles.push(createdChangeset);
|
|
4339
|
+
summary.warnings.push('Next step: open PR from beta branch to main and merge to publish stable.');
|
|
4340
|
+
printSummary(`stable promotion prepared for ${targetDir}`, summary);
|
|
819
4341
|
}
|
|
820
4342
|
|
|
821
|
-
function
|
|
4343
|
+
function applyGithubMainSetup(args, dependencies, summary, reporter) {
|
|
822
4344
|
const deps = {
|
|
823
4345
|
exec: dependencies.exec || execCommand
|
|
824
4346
|
};
|
|
825
|
-
|
|
826
|
-
ensureGhAvailable(deps);
|
|
827
|
-
|
|
828
4347
|
const repo = resolveRepo(args, deps);
|
|
829
4348
|
const rulesetPayload = createRulesetPayload(args);
|
|
830
|
-
const summary = createSummary();
|
|
831
4349
|
|
|
832
4350
|
summary.updatedScriptKeys.push(
|
|
833
4351
|
'repository.default_branch',
|
|
@@ -841,10 +4359,10 @@ function setupGithub(args, dependencies = {}) {
|
|
|
841
4359
|
summary.warnings.push(`dry-run: would update repository settings for ${repo}`);
|
|
842
4360
|
summary.warnings.push(`dry-run: would set actions workflow permissions to write for ${repo}`);
|
|
843
4361
|
summary.warnings.push(`dry-run: would upsert ruleset "${rulesetPayload.name}" for refs/heads/${args.defaultBranch}`);
|
|
844
|
-
|
|
845
|
-
return;
|
|
4362
|
+
return { repo, rulesetPayload };
|
|
846
4363
|
}
|
|
847
4364
|
|
|
4365
|
+
reporter.start('github-main-settings', 'Applying GitHub repository settings...');
|
|
848
4366
|
const repoPayload = {
|
|
849
4367
|
default_branch: args.defaultBranch,
|
|
850
4368
|
delete_branch_on_merge: true,
|
|
@@ -856,15 +4374,62 @@ function setupGithub(args, dependencies = {}) {
|
|
|
856
4374
|
|
|
857
4375
|
const patchRepo = ghApi(deps, 'PATCH', `/repos/${repo}`, repoPayload);
|
|
858
4376
|
if (patchRepo.status !== 0) {
|
|
4377
|
+
reporter.fail('github-main-settings', 'Failed to update repository settings.');
|
|
859
4378
|
throw new Error(`Failed to update repository settings: ${patchRepo.stderr || patchRepo.stdout}`.trim());
|
|
860
4379
|
}
|
|
4380
|
+
reporter.ok('github-main-settings', 'Repository settings updated.');
|
|
861
4381
|
|
|
4382
|
+
reporter.start('github-workflow-permissions', 'Applying GitHub Actions workflow permissions...');
|
|
862
4383
|
updateWorkflowPermissions(deps, repo);
|
|
4384
|
+
reporter.ok('github-workflow-permissions', 'Workflow permissions configured.');
|
|
863
4385
|
|
|
4386
|
+
reporter.start('github-main-ruleset', `Applying ruleset "${rulesetPayload.name}"...`);
|
|
864
4387
|
const upsertResult = upsertRuleset(deps, repo, rulesetPayload);
|
|
4388
|
+
reporter.ok('github-main-ruleset', `Ruleset ${upsertResult}.`);
|
|
865
4389
|
summary.overwrittenFiles.push(`github-ruleset:${upsertResult}`);
|
|
4390
|
+
return { repo, rulesetPayload };
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
function applyGithubBetaSetup(args, dependencies, summary, reporter, repo) {
|
|
4394
|
+
const deps = {
|
|
4395
|
+
exec: dependencies.exec || execCommand
|
|
4396
|
+
};
|
|
4397
|
+
const betaRulesetPayload = createBetaRulesetPayload(args.betaBranch);
|
|
4398
|
+
|
|
4399
|
+
summary.updatedScriptKeys.push('github.beta_branch', 'github.beta_ruleset');
|
|
4400
|
+
|
|
4401
|
+
if (args.dryRun) {
|
|
4402
|
+
summary.warnings.push(`dry-run: would ensure branch "${args.betaBranch}" exists in ${repo}`);
|
|
4403
|
+
summary.warnings.push(`dry-run: would upsert ruleset "${betaRulesetPayload.name}" for refs/heads/${args.betaBranch}`);
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
reporter.start('github-beta-branch', `Ensuring branch "${args.betaBranch}" exists...`);
|
|
4408
|
+
const branchResult = ensureBranchExists(deps, repo, args.defaultBranch, args.betaBranch);
|
|
4409
|
+
if (branchResult === 'created') {
|
|
4410
|
+
summary.createdFiles.push(`github-branch:${args.betaBranch}`);
|
|
4411
|
+
reporter.ok('github-beta-branch', `Branch "${args.betaBranch}" created.`);
|
|
4412
|
+
} else {
|
|
4413
|
+
summary.skippedFiles.push(`github-branch:${args.betaBranch}`);
|
|
4414
|
+
reporter.warn('github-beta-branch', `Branch "${args.betaBranch}" already exists.`);
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
reporter.start('github-beta-ruleset', `Applying beta ruleset "${betaRulesetPayload.name}"...`);
|
|
4418
|
+
const upsertResult = upsertRuleset(deps, repo, betaRulesetPayload);
|
|
4419
|
+
summary.overwrittenFiles.push(`github-beta-ruleset:${upsertResult}`);
|
|
4420
|
+
reporter.ok('github-beta-ruleset', `Beta ruleset ${upsertResult}.`);
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
function setupGithub(args, dependencies = {}) {
|
|
4424
|
+
const summary = createSummary();
|
|
4425
|
+
const deps = {
|
|
4426
|
+
exec: dependencies.exec || execCommand
|
|
4427
|
+
};
|
|
4428
|
+
ensureGhAvailable(deps);
|
|
866
4429
|
|
|
867
|
-
|
|
4430
|
+
const reporter = new StepReporter();
|
|
4431
|
+
const { repo } = applyGithubMainSetup(args, dependencies, summary, reporter);
|
|
4432
|
+
printSummary(args.dryRun ? `GitHub settings dry-run for ${repo}` : `GitHub settings applied to ${repo}`, summary);
|
|
868
4433
|
}
|
|
869
4434
|
|
|
870
4435
|
async function run(argv, dependencies = {}) {
|
|
@@ -876,7 +4441,7 @@ async function run(argv, dependencies = {}) {
|
|
|
876
4441
|
}
|
|
877
4442
|
|
|
878
4443
|
if (parsed.mode === 'init') {
|
|
879
|
-
initExistingPackage(parsed.args);
|
|
4444
|
+
await initExistingPackage(parsed.args, dependencies);
|
|
880
4445
|
return;
|
|
881
4446
|
}
|
|
882
4447
|
|
|
@@ -885,11 +4450,31 @@ async function run(argv, dependencies = {}) {
|
|
|
885
4450
|
return;
|
|
886
4451
|
}
|
|
887
4452
|
|
|
4453
|
+
if (parsed.mode === 'setup-beta') {
|
|
4454
|
+
setupBeta(parsed.args, dependencies);
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
if (parsed.mode === 'promote-stable') {
|
|
4459
|
+
promoteStable(parsed.args, dependencies);
|
|
4460
|
+
return;
|
|
4461
|
+
}
|
|
4462
|
+
|
|
888
4463
|
if (parsed.mode === 'setup-npm') {
|
|
889
4464
|
setupNpm(parsed.args, dependencies);
|
|
890
4465
|
return;
|
|
891
4466
|
}
|
|
892
4467
|
|
|
4468
|
+
if (parsed.mode === 'open-pr') {
|
|
4469
|
+
await runOpenPrFlow(parsed.args, dependencies);
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
if (parsed.mode === 'release-cycle') {
|
|
4474
|
+
await runReleaseCycle(parsed.args, dependencies);
|
|
4475
|
+
return;
|
|
4476
|
+
}
|
|
4477
|
+
|
|
893
4478
|
createNewPackage(parsed.args);
|
|
894
4479
|
}
|
|
895
4480
|
|
|
@@ -897,6 +4482,12 @@ module.exports = {
|
|
|
897
4482
|
run,
|
|
898
4483
|
parseRepoFromRemote,
|
|
899
4484
|
createBaseRulesetPayload,
|
|
4485
|
+
createBetaRulesetPayload,
|
|
900
4486
|
setupGithub,
|
|
901
|
-
setupNpm
|
|
4487
|
+
setupNpm,
|
|
4488
|
+
setupBeta,
|
|
4489
|
+
promoteStable,
|
|
4490
|
+
runOpenPrFlow,
|
|
4491
|
+
runReleaseCycle,
|
|
4492
|
+
renderPrBodyDeterministic
|
|
902
4493
|
};
|