@i-santos/create-package-starter 1.4.0 → 1.5.0-beta.2
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 +43 -0
- package/lib/run.js +740 -4
- package/package.json +1 -1
- package/template/.github/workflows/ci.yml +1 -0
- package/template/.github/workflows/release.yml +1 -0
- package/template/CONTRIBUTING.md +8 -0
- package/template/README.md +11 -0
- package/template/package.json +6 -1
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ npx @i-santos/create-package-starter --name hello-package
|
|
|
9
9
|
npx @i-santos/create-package-starter --name @i-santos/swarm --default-branch main
|
|
10
10
|
npx @i-santos/create-package-starter init --dir ./existing-package
|
|
11
11
|
npx @i-santos/create-package-starter setup-github --repo i-santos/firestack --dry-run
|
|
12
|
+
npx @i-santos/create-package-starter setup-beta --dir . --beta-branch release/beta
|
|
13
|
+
npx @i-santos/create-package-starter promote-stable --dir . --type patch --summary "Promote beta to stable"
|
|
12
14
|
npx @i-santos/create-package-starter setup-npm --dir ./existing-package --publish-first
|
|
13
15
|
```
|
|
14
16
|
|
|
@@ -37,6 +39,25 @@ Configure GitHub repository settings:
|
|
|
37
39
|
- `--ruleset <path>` (optional JSON override)
|
|
38
40
|
- `--dry-run` (prints intended operations only)
|
|
39
41
|
|
|
42
|
+
Bootstrap beta release flow:
|
|
43
|
+
|
|
44
|
+
- `setup-beta`
|
|
45
|
+
- `--dir <directory>` (default: current directory)
|
|
46
|
+
- `--beta-branch <branch>` (default: `release/beta`)
|
|
47
|
+
- `--default-branch <branch>` (default: `main`)
|
|
48
|
+
- `--repo <owner/repo>` (optional; inferred from `remote.origin.url` when omitted)
|
|
49
|
+
- `--force` (overwrite managed scripts/workflow)
|
|
50
|
+
- `--dry-run` (prints intended operations only)
|
|
51
|
+
- `--yes` (skip interactive confirmations)
|
|
52
|
+
|
|
53
|
+
Prepare stable promotion from beta track:
|
|
54
|
+
|
|
55
|
+
- `promote-stable`
|
|
56
|
+
- `--dir <directory>` (default: current directory)
|
|
57
|
+
- `--type <patch|minor|major>` (default: `patch`)
|
|
58
|
+
- `--summary <text>` (default: `Promote beta track to stable release.`)
|
|
59
|
+
- `--dry-run` (prints intended operations only)
|
|
60
|
+
|
|
40
61
|
Bootstrap npm publishing:
|
|
41
62
|
|
|
42
63
|
- `setup-npm`
|
|
@@ -92,6 +113,28 @@ All commands print a deterministic summary with:
|
|
|
92
113
|
|
|
93
114
|
If `gh` is missing or unauthenticated, command exits non-zero with actionable guidance.
|
|
94
115
|
|
|
116
|
+
## setup-beta Behavior
|
|
117
|
+
|
|
118
|
+
`setup-beta` configures prerelease automation:
|
|
119
|
+
|
|
120
|
+
- adds beta scripts to `package.json`
|
|
121
|
+
- creates/preserves `.github/workflows/release.yml` with beta+stable branch triggers
|
|
122
|
+
- creates/preserves `.github/workflows/ci.yml` with beta+stable branch triggers
|
|
123
|
+
- ensures `release/beta` branch exists remotely (created from default branch if missing)
|
|
124
|
+
- applies beta branch protection ruleset on GitHub (including required CI matrix checks for Node 18 and 20)
|
|
125
|
+
- asks for confirmation before mutating repository settings and again before overwriting existing beta ruleset
|
|
126
|
+
- supports safe-merge by default and `--force` overwrite
|
|
127
|
+
- supports configurable beta branch (`release/beta` by default)
|
|
128
|
+
|
|
129
|
+
## promote-stable Behavior
|
|
130
|
+
|
|
131
|
+
`promote-stable` prepares stable promotion from prerelease mode:
|
|
132
|
+
|
|
133
|
+
- validates `.changeset/pre.json` exists
|
|
134
|
+
- runs `changeset pre exit`
|
|
135
|
+
- creates a promotion changeset (`patch|minor|major`)
|
|
136
|
+
- prints next step guidance for opening beta->main PR
|
|
137
|
+
|
|
95
138
|
## setup-npm Behavior
|
|
96
139
|
|
|
97
140
|
`setup-npm` validates npm publish readiness:
|
package/lib/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
@@ -25,6 +26,8 @@ function usage() {
|
|
|
25
26
|
' create-package-starter --name <name> [--out <directory>] [--default-branch <branch>]',
|
|
26
27
|
' create-package-starter init [--dir <directory>] [--force] [--cleanup-legacy-release] [--scope <scope>] [--default-branch <branch>]',
|
|
27
28
|
' create-package-starter setup-github [--repo <owner/repo>] [--default-branch <branch>] [--ruleset <path>] [--dry-run]',
|
|
29
|
+
' create-package-starter setup-beta [--dir <directory>] [--repo <owner/repo>] [--beta-branch <branch>] [--default-branch <branch>] [--force] [--dry-run] [--yes]',
|
|
30
|
+
' create-package-starter promote-stable [--dir <directory>] [--type patch|minor|major] [--summary <text>] [--dry-run]',
|
|
28
31
|
' create-package-starter setup-npm [--dir <directory>] [--publish-first] [--dry-run]',
|
|
29
32
|
'',
|
|
30
33
|
'Examples:',
|
|
@@ -33,6 +36,8 @@ function usage() {
|
|
|
33
36
|
' create-package-starter init --dir ./my-package',
|
|
34
37
|
' create-package-starter init --cleanup-legacy-release',
|
|
35
38
|
' create-package-starter setup-github --repo i-santos/firestack --dry-run',
|
|
39
|
+
' create-package-starter setup-beta --dir . --beta-branch release/beta',
|
|
40
|
+
' create-package-starter promote-stable --dir . --type patch --summary "Promote beta to stable"',
|
|
36
41
|
' create-package-starter setup-npm --dir . --publish-first'
|
|
37
42
|
].join('\n');
|
|
38
43
|
}
|
|
@@ -215,6 +220,118 @@ function parseSetupNpmArgs(argv) {
|
|
|
215
220
|
return args;
|
|
216
221
|
}
|
|
217
222
|
|
|
223
|
+
function parseSetupBetaArgs(argv) {
|
|
224
|
+
const args = {
|
|
225
|
+
dir: process.cwd(),
|
|
226
|
+
betaBranch: 'release/beta',
|
|
227
|
+
defaultBranch: DEFAULT_BASE_BRANCH,
|
|
228
|
+
force: false,
|
|
229
|
+
yes: false,
|
|
230
|
+
dryRun: false
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
234
|
+
const token = argv[i];
|
|
235
|
+
|
|
236
|
+
if (token === '--dir') {
|
|
237
|
+
args.dir = parseValueFlag(argv, i, '--dir');
|
|
238
|
+
i += 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (token === '--repo') {
|
|
243
|
+
args.repo = parseValueFlag(argv, i, '--repo');
|
|
244
|
+
i += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (token === '--beta-branch') {
|
|
249
|
+
args.betaBranch = parseValueFlag(argv, i, '--beta-branch');
|
|
250
|
+
i += 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (token === '--default-branch') {
|
|
255
|
+
args.defaultBranch = parseValueFlag(argv, i, '--default-branch');
|
|
256
|
+
i += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (token === '--force') {
|
|
261
|
+
args.force = true;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (token === '--yes') {
|
|
266
|
+
args.yes = true;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (token === '--dry-run') {
|
|
271
|
+
args.dryRun = true;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (token === '--help' || token === '-h') {
|
|
276
|
+
args.help = true;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return args;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parsePromoteStableArgs(argv) {
|
|
287
|
+
const args = {
|
|
288
|
+
dir: process.cwd(),
|
|
289
|
+
type: 'patch',
|
|
290
|
+
summary: 'Promote beta track to stable release.',
|
|
291
|
+
dryRun: false
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
295
|
+
const token = argv[i];
|
|
296
|
+
|
|
297
|
+
if (token === '--dir') {
|
|
298
|
+
args.dir = parseValueFlag(argv, i, '--dir');
|
|
299
|
+
i += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (token === '--type') {
|
|
304
|
+
args.type = parseValueFlag(argv, i, '--type');
|
|
305
|
+
i += 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (token === '--summary') {
|
|
310
|
+
args.summary = parseValueFlag(argv, i, '--summary');
|
|
311
|
+
i += 1;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (token === '--dry-run') {
|
|
316
|
+
args.dryRun = true;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (token === '--help' || token === '-h') {
|
|
321
|
+
args.help = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!['patch', 'minor', 'major'].includes(args.type)) {
|
|
329
|
+
throw new Error(`Invalid --type value: ${args.type}. Expected patch, minor, or major.`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return args;
|
|
333
|
+
}
|
|
334
|
+
|
|
218
335
|
function parseArgs(argv) {
|
|
219
336
|
if (argv[0] === 'init') {
|
|
220
337
|
return {
|
|
@@ -237,6 +354,20 @@ function parseArgs(argv) {
|
|
|
237
354
|
};
|
|
238
355
|
}
|
|
239
356
|
|
|
357
|
+
if (argv[0] === 'setup-beta') {
|
|
358
|
+
return {
|
|
359
|
+
mode: 'setup-beta',
|
|
360
|
+
args: parseSetupBetaArgs(argv.slice(1))
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (argv[0] === 'promote-stable') {
|
|
365
|
+
return {
|
|
366
|
+
mode: 'promote-stable',
|
|
367
|
+
args: parsePromoteStableArgs(argv.slice(1))
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
240
371
|
return {
|
|
241
372
|
mode: 'create',
|
|
242
373
|
args: parseCreateArgs(argv)
|
|
@@ -354,6 +485,38 @@ function printSummary(title, summary) {
|
|
|
354
485
|
console.log(`warnings: ${list(summary.warnings)}`);
|
|
355
486
|
}
|
|
356
487
|
|
|
488
|
+
function logStep(status, message) {
|
|
489
|
+
const labels = {
|
|
490
|
+
run: '[RUN ]',
|
|
491
|
+
ok: '[OK ]',
|
|
492
|
+
warn: '[WARN]',
|
|
493
|
+
err: '[ERR ]'
|
|
494
|
+
};
|
|
495
|
+
const prefix = labels[status] || '[INFO]';
|
|
496
|
+
const writer = status === 'err' ? console.error : console.log;
|
|
497
|
+
writer(`${prefix} ${message}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function confirmOrThrow(questionText) {
|
|
501
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
502
|
+
throw new Error(`Confirmation required but no interactive terminal was detected. Re-run with --yes if you want to proceed non-interactively.`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const rl = readline.createInterface({
|
|
506
|
+
input: process.stdin,
|
|
507
|
+
output: process.stdout
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const answer = await rl.question(`${questionText}\nType "yes" to continue: `);
|
|
512
|
+
if (answer.trim().toLowerCase() !== 'yes') {
|
|
513
|
+
throw new Error('Operation cancelled by user.');
|
|
514
|
+
}
|
|
515
|
+
} finally {
|
|
516
|
+
rl.close();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
357
520
|
function ensureFileFromTemplate(targetPath, templatePath, options) {
|
|
358
521
|
const exists = fs.existsSync(targetPath);
|
|
359
522
|
|
|
@@ -374,6 +537,127 @@ function ensureFileFromTemplate(targetPath, templatePath, options) {
|
|
|
374
537
|
return 'created';
|
|
375
538
|
}
|
|
376
539
|
|
|
540
|
+
function ensureReleaseWorkflowBranches(content, defaultBranch, betaBranch) {
|
|
541
|
+
const lines = content.split('\n');
|
|
542
|
+
const onIndex = lines.findIndex((line) => line.trim() === 'on:');
|
|
543
|
+
|
|
544
|
+
if (onIndex < 0) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let onSectionEnd = lines.length;
|
|
549
|
+
for (let i = onIndex + 1; i < lines.length; i += 1) {
|
|
550
|
+
const line = lines[i];
|
|
551
|
+
const isTopLevelKey = line && !line.startsWith(' ') && line.trim().endsWith(':');
|
|
552
|
+
if (isTopLevelKey) {
|
|
553
|
+
onSectionEnd = i;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const onBlock = lines.slice(onIndex, onSectionEnd);
|
|
559
|
+
const pushRelativeIndex = onBlock.findIndex((line) => line.trim() === 'push:');
|
|
560
|
+
if (pushRelativeIndex < 0) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const branchesRelativeIndex = onBlock.findIndex((line) => line.trim() === 'branches:');
|
|
565
|
+
if (branchesRelativeIndex < 0 || branchesRelativeIndex <= pushRelativeIndex) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const listStart = branchesRelativeIndex + 1;
|
|
570
|
+
let listEnd = listStart;
|
|
571
|
+
while (listEnd < onBlock.length && onBlock[listEnd].trim().startsWith('- ')) {
|
|
572
|
+
listEnd += 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (listEnd === listStart) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const existingBranches = onBlock.slice(listStart, listEnd)
|
|
580
|
+
.map((line) => line.trim().replace(/^- /, '').trim())
|
|
581
|
+
.filter(Boolean);
|
|
582
|
+
|
|
583
|
+
const desiredBranches = [...new Set([defaultBranch, betaBranch])];
|
|
584
|
+
const mergedBranches = [...existingBranches];
|
|
585
|
+
for (const branch of desiredBranches) {
|
|
586
|
+
if (!mergedBranches.includes(branch)) {
|
|
587
|
+
mergedBranches.push(branch);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const changed = mergedBranches.length !== existingBranches.length
|
|
592
|
+
|| mergedBranches.some((branch, index) => branch !== existingBranches[index]);
|
|
593
|
+
|
|
594
|
+
if (!changed) {
|
|
595
|
+
return {
|
|
596
|
+
changed: false,
|
|
597
|
+
content
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const updatedOnBlock = [
|
|
602
|
+
...onBlock.slice(0, listStart),
|
|
603
|
+
...mergedBranches.map((branch) => ` - ${branch}`),
|
|
604
|
+
...onBlock.slice(listEnd)
|
|
605
|
+
];
|
|
606
|
+
|
|
607
|
+
const updatedLines = [
|
|
608
|
+
...lines.slice(0, onIndex),
|
|
609
|
+
...updatedOnBlock,
|
|
610
|
+
...lines.slice(onSectionEnd)
|
|
611
|
+
];
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
changed: true,
|
|
615
|
+
content: updatedLines.join('\n')
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function upsertReleaseWorkflow(targetPath, templatePath, options) {
|
|
620
|
+
const exists = fs.existsSync(targetPath);
|
|
621
|
+
if (!exists || options.force) {
|
|
622
|
+
if (options.dryRun) {
|
|
623
|
+
return {
|
|
624
|
+
result: exists ? 'overwritten' : 'created'
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const result = ensureFileFromTemplate(targetPath, templatePath, {
|
|
629
|
+
force: options.force,
|
|
630
|
+
variables: options.variables
|
|
631
|
+
});
|
|
632
|
+
return { result };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const current = fs.readFileSync(targetPath, 'utf8');
|
|
636
|
+
const ensured = ensureReleaseWorkflowBranches(
|
|
637
|
+
current,
|
|
638
|
+
options.variables.DEFAULT_BRANCH,
|
|
639
|
+
options.variables.BETA_BRANCH
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
if (!ensured) {
|
|
643
|
+
return {
|
|
644
|
+
result: 'skipped',
|
|
645
|
+
warning: `Could not safely update trigger branches in ${path.basename(targetPath)}. Use --force to overwrite from template.`
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (!ensured.changed) {
|
|
650
|
+
return { result: 'skipped' };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (options.dryRun) {
|
|
654
|
+
return { result: 'updated' };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
fs.writeFileSync(targetPath, ensured.content);
|
|
658
|
+
return { result: 'updated' };
|
|
659
|
+
}
|
|
660
|
+
|
|
377
661
|
function detectEquivalentManagedFile(packageDir, targetRelativePath) {
|
|
378
662
|
if (targetRelativePath !== '.github/PULL_REQUEST_TEMPLATE.md') {
|
|
379
663
|
return targetRelativePath;
|
|
@@ -456,7 +740,12 @@ function configureExistingPackage(packageDir, templateDir, options) {
|
|
|
456
740
|
check: 'npm run test',
|
|
457
741
|
changeset: 'changeset',
|
|
458
742
|
'version-packages': 'changeset version',
|
|
459
|
-
release: 'npm run check && changeset publish'
|
|
743
|
+
release: 'npm run check && changeset publish',
|
|
744
|
+
'beta:enter': 'changeset pre enter beta',
|
|
745
|
+
'beta:exit': 'changeset pre exit',
|
|
746
|
+
'beta:version': 'changeset version',
|
|
747
|
+
'beta:publish': 'changeset publish',
|
|
748
|
+
'beta:promote': 'create-package-starter promote-stable --dir .'
|
|
460
749
|
};
|
|
461
750
|
|
|
462
751
|
let packageJsonChanged = false;
|
|
@@ -518,6 +807,7 @@ function configureExistingPackage(packageDir, templateDir, options) {
|
|
|
518
807
|
variables: {
|
|
519
808
|
PACKAGE_NAME: packageName,
|
|
520
809
|
DEFAULT_BRANCH: options.defaultBranch,
|
|
810
|
+
BETA_BRANCH: options.betaBranch || 'release/beta',
|
|
521
811
|
SCOPE: deriveScope(options.scope, packageName)
|
|
522
812
|
}
|
|
523
813
|
}, summary);
|
|
@@ -553,12 +843,14 @@ function createNewPackage(args) {
|
|
|
553
843
|
const createdFiles = copyDirRecursive(templateDir, targetDir, {
|
|
554
844
|
PACKAGE_NAME: args.name,
|
|
555
845
|
DEFAULT_BRANCH: args.defaultBranch,
|
|
846
|
+
BETA_BRANCH: 'release/beta',
|
|
556
847
|
SCOPE: deriveScope('', args.name)
|
|
557
848
|
});
|
|
558
849
|
|
|
559
850
|
summary.createdFiles.push(...createdFiles);
|
|
560
851
|
|
|
561
852
|
summary.updatedScriptKeys.push('check', 'changeset', 'version-packages', 'release');
|
|
853
|
+
summary.updatedScriptKeys.push('beta:enter', 'beta:exit', 'beta:version', 'beta:publish', 'beta:promote');
|
|
562
854
|
summary.updatedDependencyKeys.push(CHANGESETS_DEP);
|
|
563
855
|
|
|
564
856
|
printSummary(`Package created in ${targetDir}`, summary);
|
|
@@ -638,6 +930,49 @@ function createBaseRulesetPayload(defaultBranch) {
|
|
|
638
930
|
};
|
|
639
931
|
}
|
|
640
932
|
|
|
933
|
+
function createBetaRulesetPayload(betaBranch) {
|
|
934
|
+
return {
|
|
935
|
+
name: `Beta branch protection (${betaBranch})`,
|
|
936
|
+
target: 'branch',
|
|
937
|
+
enforcement: 'active',
|
|
938
|
+
conditions: {
|
|
939
|
+
ref_name: {
|
|
940
|
+
include: [`refs/heads/${betaBranch}`],
|
|
941
|
+
exclude: []
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
bypass_actors: [],
|
|
945
|
+
rules: [
|
|
946
|
+
{ type: 'deletion' },
|
|
947
|
+
{ type: 'non_fast_forward' },
|
|
948
|
+
{
|
|
949
|
+
type: 'pull_request',
|
|
950
|
+
parameters: {
|
|
951
|
+
required_approving_review_count: 0,
|
|
952
|
+
dismiss_stale_reviews_on_push: true,
|
|
953
|
+
require_code_owner_review: false,
|
|
954
|
+
require_last_push_approval: false,
|
|
955
|
+
required_review_thread_resolution: true
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
type: 'required_status_checks',
|
|
960
|
+
parameters: {
|
|
961
|
+
strict_required_status_checks_policy: true,
|
|
962
|
+
required_status_checks: [
|
|
963
|
+
{
|
|
964
|
+
context: 'CI / check (18) (pull_request)'
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
context: 'CI / check (20) (pull_request)'
|
|
968
|
+
}
|
|
969
|
+
]
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
]
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
641
976
|
function createRulesetPayload(args) {
|
|
642
977
|
if (!args.ruleset) {
|
|
643
978
|
return createBaseRulesetPayload(args.defaultBranch);
|
|
@@ -729,6 +1064,69 @@ function updateWorkflowPermissions(deps, repo) {
|
|
|
729
1064
|
}
|
|
730
1065
|
}
|
|
731
1066
|
|
|
1067
|
+
function isNotFoundResponse(result) {
|
|
1068
|
+
const output = `${result.stderr || ''}\n${result.stdout || ''}`.toLowerCase();
|
|
1069
|
+
return output.includes('404') || output.includes('not found');
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function ensureBranchExists(deps, repo, defaultBranch, targetBranch) {
|
|
1073
|
+
const encodedTarget = encodeURIComponent(targetBranch);
|
|
1074
|
+
const getTarget = ghApi(deps, 'GET', `/repos/${repo}/branches/${encodedTarget}`);
|
|
1075
|
+
if (getTarget.status === 0) {
|
|
1076
|
+
return 'exists';
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (!isNotFoundResponse(getTarget)) {
|
|
1080
|
+
throw new Error(`Failed to check branch "${targetBranch}": ${getTarget.stderr || getTarget.stdout}`.trim());
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const encodedDefault = encodeURIComponent(defaultBranch);
|
|
1084
|
+
const getDefaultRef = ghApi(deps, 'GET', `/repos/${repo}/git/ref/heads/${encodedDefault}`);
|
|
1085
|
+
if (getDefaultRef.status !== 0) {
|
|
1086
|
+
throw new Error(`Failed to resolve default branch "${defaultBranch}": ${getDefaultRef.stderr || getDefaultRef.stdout}`.trim());
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const parsed = parseJsonOutput(getDefaultRef.stdout || '{}', 'Failed to parse default branch ref from GitHub API.');
|
|
1090
|
+
const sha = parsed && parsed.object && parsed.object.sha;
|
|
1091
|
+
if (!sha) {
|
|
1092
|
+
throw new Error(`Could not determine SHA for default branch "${defaultBranch}".`);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const createRef = ghApi(deps, 'POST', `/repos/${repo}/git/refs`, {
|
|
1096
|
+
ref: `refs/heads/${targetBranch}`,
|
|
1097
|
+
sha
|
|
1098
|
+
});
|
|
1099
|
+
if (createRef.status !== 0) {
|
|
1100
|
+
throw new Error(`Failed to create branch "${targetBranch}": ${createRef.stderr || createRef.stdout}`.trim());
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return 'created';
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function branchExists(deps, repo, targetBranch) {
|
|
1107
|
+
const encodedTarget = encodeURIComponent(targetBranch);
|
|
1108
|
+
const getTarget = ghApi(deps, 'GET', `/repos/${repo}/branches/${encodedTarget}`);
|
|
1109
|
+
if (getTarget.status === 0) {
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (isNotFoundResponse(getTarget)) {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
throw new Error(`Failed to check branch "${targetBranch}": ${getTarget.stderr || getTarget.stdout}`.trim());
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function findRulesetByName(deps, repo, name) {
|
|
1121
|
+
const listResult = ghApi(deps, 'GET', `/repos/${repo}/rulesets`);
|
|
1122
|
+
if (listResult.status !== 0) {
|
|
1123
|
+
throw new Error(`Failed to list rulesets: ${listResult.stderr || listResult.stdout}`.trim());
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const rulesets = parseJsonOutput(listResult.stdout || '[]', 'Failed to parse rulesets response from GitHub API.');
|
|
1127
|
+
return rulesets.find((ruleset) => ruleset.name === name) || null;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
732
1130
|
function ensureNpmAvailable(deps) {
|
|
733
1131
|
const version = deps.exec('npm', ['--version']);
|
|
734
1132
|
if (version.status !== 0) {
|
|
@@ -804,9 +1202,23 @@ function setupNpm(args, dependencies = {}) {
|
|
|
804
1202
|
} else if (args.dryRun) {
|
|
805
1203
|
summary.warnings.push(`dry-run: would run "npm publish --access public" in ${targetDir}`);
|
|
806
1204
|
} else {
|
|
807
|
-
const publish = deps.exec('npm', ['publish', '--access', 'public'], { cwd: targetDir });
|
|
1205
|
+
const publish = deps.exec('npm', ['publish', '--access', 'public'], { cwd: targetDir, stdio: 'inherit' });
|
|
808
1206
|
if (publish.status !== 0) {
|
|
809
|
-
|
|
1207
|
+
const publishOutput = `${publish.stderr || ''}\n${publish.stdout || ''}`.toLowerCase();
|
|
1208
|
+
const isOtpError = publishOutput.includes('eotp') || publishOutput.includes('one-time password');
|
|
1209
|
+
|
|
1210
|
+
if (isOtpError) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
[
|
|
1213
|
+
'First publish failed due to npm 2FA/OTP requirements.',
|
|
1214
|
+
'This command already delegates to the standard npm publish flow.',
|
|
1215
|
+
'If npm still requires manual OTP entry, complete publish manually:',
|
|
1216
|
+
` (cd ${targetDir} && npm publish --access public)`
|
|
1217
|
+
].join('\n')
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
throw new Error('First publish failed. Check npm output above and try again.');
|
|
810
1222
|
}
|
|
811
1223
|
summary.updatedScriptKeys.push('npm.first_publish_done');
|
|
812
1224
|
}
|
|
@@ -818,6 +1230,317 @@ function setupNpm(args, dependencies = {}) {
|
|
|
818
1230
|
printSummary(`npm setup completed for ${packageJson.name}`, summary);
|
|
819
1231
|
}
|
|
820
1232
|
|
|
1233
|
+
async function setupBeta(args, dependencies = {}) {
|
|
1234
|
+
const deps = {
|
|
1235
|
+
exec: dependencies.exec || execCommand
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
const targetDir = path.resolve(args.dir);
|
|
1239
|
+
if (!fs.existsSync(targetDir)) {
|
|
1240
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
1244
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1245
|
+
throw new Error(`package.json not found in ${targetDir}`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
1249
|
+
const templateDir = path.join(packageRoot, 'template');
|
|
1250
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
1251
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
1252
|
+
|
|
1253
|
+
logStep('run', 'Checking GitHub CLI availability and authentication...');
|
|
1254
|
+
try {
|
|
1255
|
+
ensureGhAvailable(deps);
|
|
1256
|
+
logStep('ok', 'GitHub CLI is available and authenticated.');
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
logStep('err', error.message);
|
|
1259
|
+
if (error.message.includes('not authenticated')) {
|
|
1260
|
+
logStep('warn', 'Run "gh auth login" and retry.');
|
|
1261
|
+
}
|
|
1262
|
+
throw error;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
logStep('run', 'Resolving repository target...');
|
|
1266
|
+
const repo = resolveRepo(args, deps);
|
|
1267
|
+
logStep('ok', `Using repository ${repo}.`);
|
|
1268
|
+
|
|
1269
|
+
const summary = createSummary();
|
|
1270
|
+
summary.updatedScriptKeys.push('github.beta_branch', 'github.beta_ruleset', 'actions.default_workflow_permissions');
|
|
1271
|
+
const desiredScripts = {
|
|
1272
|
+
'beta:enter': 'changeset pre enter beta',
|
|
1273
|
+
'beta:exit': 'changeset pre exit',
|
|
1274
|
+
'beta:version': 'changeset version',
|
|
1275
|
+
'beta:publish': 'changeset publish',
|
|
1276
|
+
'beta:promote': 'create-package-starter promote-stable --dir .'
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
let packageJsonChanged = false;
|
|
1280
|
+
for (const [key, value] of Object.entries(desiredScripts)) {
|
|
1281
|
+
const exists = Object.prototype.hasOwnProperty.call(packageJson.scripts, key);
|
|
1282
|
+
if (!exists || args.force) {
|
|
1283
|
+
if (!exists || packageJson.scripts[key] !== value) {
|
|
1284
|
+
packageJson.scripts[key] = value;
|
|
1285
|
+
packageJsonChanged = true;
|
|
1286
|
+
}
|
|
1287
|
+
summary.updatedScriptKeys.push(key);
|
|
1288
|
+
} else {
|
|
1289
|
+
summary.skippedScriptKeys.push(key);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const workflowRelativePath = '.github/workflows/release.yml';
|
|
1294
|
+
const workflowTemplatePath = path.join(templateDir, workflowRelativePath);
|
|
1295
|
+
const workflowTargetPath = path.join(targetDir, workflowRelativePath);
|
|
1296
|
+
const ciWorkflowRelativePath = '.github/workflows/ci.yml';
|
|
1297
|
+
const ciWorkflowTemplatePath = path.join(templateDir, ciWorkflowRelativePath);
|
|
1298
|
+
const ciWorkflowTargetPath = path.join(targetDir, ciWorkflowRelativePath);
|
|
1299
|
+
if (!fs.existsSync(workflowTemplatePath)) {
|
|
1300
|
+
throw new Error(`Template not found: ${workflowTemplatePath}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (!fs.existsSync(ciWorkflowTemplatePath)) {
|
|
1303
|
+
throw new Error(`Template not found: ${ciWorkflowTemplatePath}`);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (args.dryRun) {
|
|
1307
|
+
logStep('warn', 'Dry-run mode enabled. No remote or file changes will be applied.');
|
|
1308
|
+
const workflowPreview = upsertReleaseWorkflow(workflowTargetPath, workflowTemplatePath, {
|
|
1309
|
+
force: args.force,
|
|
1310
|
+
dryRun: true,
|
|
1311
|
+
variables: {
|
|
1312
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
1313
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
1314
|
+
BETA_BRANCH: args.betaBranch,
|
|
1315
|
+
SCOPE: deriveScope('', packageJson.name || '')
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
if (workflowPreview.result === 'created') {
|
|
1319
|
+
summary.warnings.push(`dry-run: would create ${workflowRelativePath}`);
|
|
1320
|
+
} else if (workflowPreview.result === 'overwritten') {
|
|
1321
|
+
summary.warnings.push(`dry-run: would overwrite ${workflowRelativePath}`);
|
|
1322
|
+
} else if (workflowPreview.result === 'updated') {
|
|
1323
|
+
summary.warnings.push(`dry-run: would update ${workflowRelativePath} trigger branches`);
|
|
1324
|
+
} else {
|
|
1325
|
+
summary.warnings.push(`dry-run: would keep existing ${workflowRelativePath}`);
|
|
1326
|
+
if (workflowPreview.warning) {
|
|
1327
|
+
summary.warnings.push(`dry-run: ${workflowPreview.warning}`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const ciWorkflowPreview = upsertReleaseWorkflow(ciWorkflowTargetPath, ciWorkflowTemplatePath, {
|
|
1331
|
+
force: args.force,
|
|
1332
|
+
dryRun: true,
|
|
1333
|
+
variables: {
|
|
1334
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
1335
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
1336
|
+
BETA_BRANCH: args.betaBranch,
|
|
1337
|
+
SCOPE: deriveScope('', packageJson.name || '')
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
if (ciWorkflowPreview.result === 'created') {
|
|
1341
|
+
summary.warnings.push(`dry-run: would create ${ciWorkflowRelativePath}`);
|
|
1342
|
+
} else if (ciWorkflowPreview.result === 'overwritten') {
|
|
1343
|
+
summary.warnings.push(`dry-run: would overwrite ${ciWorkflowRelativePath}`);
|
|
1344
|
+
} else if (ciWorkflowPreview.result === 'updated') {
|
|
1345
|
+
summary.warnings.push(`dry-run: would update ${ciWorkflowRelativePath} trigger branches`);
|
|
1346
|
+
} else {
|
|
1347
|
+
summary.warnings.push(`dry-run: would keep existing ${ciWorkflowRelativePath}`);
|
|
1348
|
+
if (ciWorkflowPreview.warning) {
|
|
1349
|
+
summary.warnings.push(`dry-run: ${ciWorkflowPreview.warning}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (packageJsonChanged) {
|
|
1353
|
+
summary.warnings.push('dry-run: would update package.json beta scripts');
|
|
1354
|
+
}
|
|
1355
|
+
summary.warnings.push(`dry-run: would ensure branch "${args.betaBranch}" exists in ${repo}`);
|
|
1356
|
+
summary.warnings.push(`dry-run: would upsert ruleset for refs/heads/${args.betaBranch}`);
|
|
1357
|
+
summary.warnings.push(`dry-run: would set Actions workflow permissions to write for ${repo}`);
|
|
1358
|
+
summary.warnings.push(`dry-run: beta branch configured as ${args.betaBranch}`);
|
|
1359
|
+
} else {
|
|
1360
|
+
const betaRulesetPayload = createBetaRulesetPayload(args.betaBranch);
|
|
1361
|
+
const doesBranchExist = branchExists(deps, repo, args.betaBranch);
|
|
1362
|
+
const existingRuleset = findRulesetByName(deps, repo, betaRulesetPayload.name);
|
|
1363
|
+
|
|
1364
|
+
if (args.yes) {
|
|
1365
|
+
logStep('warn', 'Confirmation prompts skipped due to --yes.');
|
|
1366
|
+
} else {
|
|
1367
|
+
await confirmOrThrow(
|
|
1368
|
+
[
|
|
1369
|
+
`This will modify GitHub repository settings for ${repo}:`,
|
|
1370
|
+
`- set Actions workflow permissions to write`,
|
|
1371
|
+
`- ensure branch "${args.betaBranch}" exists${doesBranchExist ? ' (already exists)' : ' (will be created)'}`,
|
|
1372
|
+
`- apply branch protection ruleset "${betaRulesetPayload.name}"`,
|
|
1373
|
+
'- require CI status checks "CI / check (18) (pull_request)" and "CI / check (20) (pull_request)" on beta branch',
|
|
1374
|
+
`- update local ${workflowRelativePath} and package.json beta scripts`
|
|
1375
|
+
].join('\n')
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
if (existingRuleset) {
|
|
1379
|
+
await confirmOrThrow(
|
|
1380
|
+
`Ruleset "${betaRulesetPayload.name}" already exists and will be overwritten.`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
logStep('run', `Ensuring ${workflowRelativePath} includes stable+beta triggers...`);
|
|
1386
|
+
const workflowUpsert = upsertReleaseWorkflow(workflowTargetPath, workflowTemplatePath, {
|
|
1387
|
+
force: args.force,
|
|
1388
|
+
dryRun: false,
|
|
1389
|
+
variables: {
|
|
1390
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
1391
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
1392
|
+
BETA_BRANCH: args.betaBranch,
|
|
1393
|
+
SCOPE: deriveScope('', packageJson.name || '')
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
const workflowResult = workflowUpsert.result;
|
|
1397
|
+
|
|
1398
|
+
if (workflowResult === 'created') {
|
|
1399
|
+
summary.createdFiles.push(workflowRelativePath);
|
|
1400
|
+
logStep('ok', `${workflowRelativePath} created.`);
|
|
1401
|
+
} else if (workflowResult === 'overwritten') {
|
|
1402
|
+
summary.overwrittenFiles.push(workflowRelativePath);
|
|
1403
|
+
logStep('ok', `${workflowRelativePath} overwritten.`);
|
|
1404
|
+
} else if (workflowResult === 'updated') {
|
|
1405
|
+
summary.overwrittenFiles.push(workflowRelativePath);
|
|
1406
|
+
logStep('ok', `${workflowRelativePath} updated with missing branch triggers.`);
|
|
1407
|
+
} else {
|
|
1408
|
+
summary.skippedFiles.push(workflowRelativePath);
|
|
1409
|
+
if (workflowUpsert.warning) {
|
|
1410
|
+
summary.warnings.push(workflowUpsert.warning);
|
|
1411
|
+
logStep('warn', workflowUpsert.warning);
|
|
1412
|
+
} else {
|
|
1413
|
+
logStep('warn', `${workflowRelativePath} already configured; kept as-is.`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
logStep('run', `Ensuring ${ciWorkflowRelativePath} includes stable+beta triggers...`);
|
|
1418
|
+
const ciWorkflowUpsert = upsertReleaseWorkflow(ciWorkflowTargetPath, ciWorkflowTemplatePath, {
|
|
1419
|
+
force: args.force,
|
|
1420
|
+
dryRun: false,
|
|
1421
|
+
variables: {
|
|
1422
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
1423
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
1424
|
+
BETA_BRANCH: args.betaBranch,
|
|
1425
|
+
SCOPE: deriveScope('', packageJson.name || '')
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
const ciWorkflowResult = ciWorkflowUpsert.result;
|
|
1429
|
+
if (ciWorkflowResult === 'created') {
|
|
1430
|
+
summary.createdFiles.push(ciWorkflowRelativePath);
|
|
1431
|
+
logStep('ok', `${ciWorkflowRelativePath} created.`);
|
|
1432
|
+
} else if (ciWorkflowResult === 'overwritten') {
|
|
1433
|
+
summary.overwrittenFiles.push(ciWorkflowRelativePath);
|
|
1434
|
+
logStep('ok', `${ciWorkflowRelativePath} overwritten.`);
|
|
1435
|
+
} else if (ciWorkflowResult === 'updated') {
|
|
1436
|
+
summary.overwrittenFiles.push(ciWorkflowRelativePath);
|
|
1437
|
+
logStep('ok', `${ciWorkflowRelativePath} updated with missing branch triggers.`);
|
|
1438
|
+
} else {
|
|
1439
|
+
summary.skippedFiles.push(ciWorkflowRelativePath);
|
|
1440
|
+
if (ciWorkflowUpsert.warning) {
|
|
1441
|
+
summary.warnings.push(ciWorkflowUpsert.warning);
|
|
1442
|
+
logStep('warn', ciWorkflowUpsert.warning);
|
|
1443
|
+
} else {
|
|
1444
|
+
logStep('warn', `${ciWorkflowRelativePath} already configured; kept as-is.`);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (packageJsonChanged) {
|
|
1449
|
+
logStep('run', 'Updating package.json beta scripts...');
|
|
1450
|
+
writeJsonFile(packageJsonPath, packageJson);
|
|
1451
|
+
logStep('ok', 'package.json beta scripts updated.');
|
|
1452
|
+
} else {
|
|
1453
|
+
logStep('warn', 'package.json beta scripts already present; no changes needed.');
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
logStep('run', 'Applying GitHub Actions workflow permissions...');
|
|
1457
|
+
updateWorkflowPermissions(deps, repo);
|
|
1458
|
+
logStep('ok', 'Workflow permissions configured.');
|
|
1459
|
+
|
|
1460
|
+
logStep('run', `Ensuring branch "${args.betaBranch}" exists...`);
|
|
1461
|
+
const branchResult = ensureBranchExists(deps, repo, args.defaultBranch, args.betaBranch);
|
|
1462
|
+
if (branchResult === 'created') {
|
|
1463
|
+
summary.createdFiles.push(`github-branch:${args.betaBranch}`);
|
|
1464
|
+
logStep('ok', `Branch "${args.betaBranch}" created from "${args.defaultBranch}".`);
|
|
1465
|
+
} else {
|
|
1466
|
+
summary.skippedFiles.push(`github-branch:${args.betaBranch}`);
|
|
1467
|
+
logStep('warn', `Branch "${args.betaBranch}" already exists.`);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
logStep('run', `Applying protection ruleset to "${args.betaBranch}"...`);
|
|
1471
|
+
const upsertResult = upsertRuleset(deps, repo, betaRulesetPayload);
|
|
1472
|
+
summary.overwrittenFiles.push(`github-beta-ruleset:${upsertResult}`);
|
|
1473
|
+
logStep('ok', `Beta branch ruleset ${upsertResult}.`);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
summary.warnings.push(`Trusted Publisher supports a single workflow file per package. Keep publishing on .github/workflows/release.yml for both stable and beta.`);
|
|
1477
|
+
summary.warnings.push(`Next step: run "npm run beta:enter" once on "${args.betaBranch}", commit .changeset/pre.json, and push.`);
|
|
1478
|
+
printSummary(`beta setup completed for ${targetDir}`, summary);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function createChangesetFile(targetDir, packageName, bumpType, summaryText) {
|
|
1482
|
+
const changesetDir = path.join(targetDir, '.changeset');
|
|
1483
|
+
fs.mkdirSync(changesetDir, { recursive: true });
|
|
1484
|
+
const fileName = `promote-stable-${Date.now()}.md`;
|
|
1485
|
+
const filePath = path.join(changesetDir, fileName);
|
|
1486
|
+
const content = [
|
|
1487
|
+
'---',
|
|
1488
|
+
`"${packageName}": ${bumpType}`,
|
|
1489
|
+
'---',
|
|
1490
|
+
'',
|
|
1491
|
+
summaryText
|
|
1492
|
+
].join('\n');
|
|
1493
|
+
fs.writeFileSync(filePath, `${content}\n`);
|
|
1494
|
+
return path.posix.join('.changeset', fileName);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function promoteStable(args, dependencies = {}) {
|
|
1498
|
+
const deps = {
|
|
1499
|
+
exec: dependencies.exec || execCommand
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
const targetDir = path.resolve(args.dir);
|
|
1503
|
+
if (!fs.existsSync(targetDir)) {
|
|
1504
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
1508
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1509
|
+
throw new Error(`package.json not found in ${targetDir}`);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const prePath = path.join(targetDir, '.changeset', 'pre.json');
|
|
1513
|
+
if (!fs.existsSync(prePath)) {
|
|
1514
|
+
throw new Error(`No prerelease state found in ${targetDir}. Run "changeset pre enter beta" first.`);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
1518
|
+
if (!packageJson.name) {
|
|
1519
|
+
throw new Error(`package.json in ${targetDir} must define "name".`);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const summary = createSummary();
|
|
1523
|
+
summary.updatedScriptKeys.push('changeset.pre_exit', 'changeset.promote_stable');
|
|
1524
|
+
|
|
1525
|
+
if (args.dryRun) {
|
|
1526
|
+
summary.warnings.push(`dry-run: would run "npx @changesets/cli pre exit" in ${targetDir}`);
|
|
1527
|
+
summary.warnings.push(`dry-run: would create promotion changeset for ${packageJson.name} (${args.type})`);
|
|
1528
|
+
summary.warnings.push(`dry-run: promote flow targets stable branch ${DEFAULT_BASE_BRANCH}`);
|
|
1529
|
+
printSummary(`stable promotion dry-run for ${targetDir}`, summary);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const preExit = deps.exec('npx', ['@changesets/cli', 'pre', 'exit'], { cwd: targetDir });
|
|
1534
|
+
if (preExit.status !== 0) {
|
|
1535
|
+
throw new Error(`Failed to exit prerelease mode: ${(preExit.stderr || preExit.stdout || '').trim()}`);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const createdChangeset = createChangesetFile(targetDir, packageJson.name, args.type, args.summary);
|
|
1539
|
+
summary.createdFiles.push(createdChangeset);
|
|
1540
|
+
summary.warnings.push('Next step: open PR from beta branch to main and merge to publish stable.');
|
|
1541
|
+
printSummary(`stable promotion prepared for ${targetDir}`, summary);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
821
1544
|
function setupGithub(args, dependencies = {}) {
|
|
822
1545
|
const deps = {
|
|
823
1546
|
exec: dependencies.exec || execCommand
|
|
@@ -885,6 +1608,16 @@ async function run(argv, dependencies = {}) {
|
|
|
885
1608
|
return;
|
|
886
1609
|
}
|
|
887
1610
|
|
|
1611
|
+
if (parsed.mode === 'setup-beta') {
|
|
1612
|
+
setupBeta(parsed.args, dependencies);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (parsed.mode === 'promote-stable') {
|
|
1617
|
+
promoteStable(parsed.args, dependencies);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
888
1621
|
if (parsed.mode === 'setup-npm') {
|
|
889
1622
|
setupNpm(parsed.args, dependencies);
|
|
890
1623
|
return;
|
|
@@ -897,6 +1630,9 @@ module.exports = {
|
|
|
897
1630
|
run,
|
|
898
1631
|
parseRepoFromRemote,
|
|
899
1632
|
createBaseRulesetPayload,
|
|
1633
|
+
createBetaRulesetPayload,
|
|
900
1634
|
setupGithub,
|
|
901
|
-
setupNpm
|
|
1635
|
+
setupNpm,
|
|
1636
|
+
setupBeta,
|
|
1637
|
+
promoteStable
|
|
902
1638
|
};
|
package/package.json
CHANGED
package/template/CONTRIBUTING.md
CHANGED
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
3. `.github/workflows/release.yml` opens/updates `chore: release packages`.
|
|
13
13
|
4. Merge the release PR to publish.
|
|
14
14
|
|
|
15
|
+
## Beta process
|
|
16
|
+
|
|
17
|
+
1. Use branch `__BETA_BRANCH__` for prereleases.
|
|
18
|
+
2. Run `npm run beta:enter` once on `__BETA_BRANCH__`.
|
|
19
|
+
3. Publish beta versions via `.github/workflows/release.yml` on `__BETA_BRANCH__`.
|
|
20
|
+
4. Run `npm run beta:promote` to exit prerelease mode and create stable promotion changeset.
|
|
21
|
+
5. Open PR from `__BETA_BRANCH__` to `__DEFAULT_BRANCH__`.
|
|
22
|
+
|
|
15
23
|
## Trusted Publishing
|
|
16
24
|
|
|
17
25
|
If the package does not exist on npm yet, the first publish can be manual:
|
package/template/README.md
CHANGED
|
@@ -8,6 +8,10 @@ Package created by `@i-santos/create-package-starter`.
|
|
|
8
8
|
- `npm run changeset`
|
|
9
9
|
- `npm run version-packages`
|
|
10
10
|
- `npm run release`
|
|
11
|
+
- `npm run beta:enter`
|
|
12
|
+
- `npm run beta:exit`
|
|
13
|
+
- `npm run beta:publish`
|
|
14
|
+
- `npm run beta:promote`
|
|
11
15
|
|
|
12
16
|
## Release flow
|
|
13
17
|
|
|
@@ -16,6 +20,13 @@ Package created by `@i-santos/create-package-starter`.
|
|
|
16
20
|
3. `.github/workflows/release.yml` creates or updates `chore: release packages`.
|
|
17
21
|
4. Merge the release PR to publish.
|
|
18
22
|
|
|
23
|
+
## Beta release flow
|
|
24
|
+
|
|
25
|
+
1. Create `__BETA_BRANCH__` from `__DEFAULT_BRANCH__`.
|
|
26
|
+
2. Run `npm run beta:enter` once on `__BETA_BRANCH__`.
|
|
27
|
+
3. Push updates to `__BETA_BRANCH__` and let `.github/workflows/release.yml` publish beta versions.
|
|
28
|
+
4. When ready for stable, run `npm run beta:promote`, open PR from `__BETA_BRANCH__` to `__DEFAULT_BRANCH__`, and merge.
|
|
29
|
+
|
|
19
30
|
## Trusted Publishing
|
|
20
31
|
|
|
21
32
|
If this package does not exist on npm yet, first publish can be manual:
|
package/template/package.json
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
"check": "node scripts/check.js",
|
|
7
7
|
"changeset": "changeset",
|
|
8
8
|
"version-packages": "changeset version",
|
|
9
|
-
"release": "npm run check && changeset publish"
|
|
9
|
+
"release": "npm run check && changeset publish",
|
|
10
|
+
"beta:enter": "changeset pre enter beta",
|
|
11
|
+
"beta:exit": "changeset pre exit",
|
|
12
|
+
"beta:version": "changeset version",
|
|
13
|
+
"beta:publish": "changeset publish",
|
|
14
|
+
"beta:promote": "create-package-starter promote-stable --dir ."
|
|
10
15
|
},
|
|
11
16
|
"devDependencies": {
|
|
12
17
|
"@changesets/cli": "^2.29.7"
|