@i-santos/create-package-starter 1.3.0 → 1.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/lib/run.js +803 -3
- package/package.json +1 -1
- 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,9 @@ 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"
|
|
14
|
+
npx @i-santos/create-package-starter setup-npm --dir ./existing-package --publish-first
|
|
12
15
|
```
|
|
13
16
|
|
|
14
17
|
## Commands
|
|
@@ -36,6 +39,32 @@ Configure GitHub repository settings:
|
|
|
36
39
|
- `--ruleset <path>` (optional JSON override)
|
|
37
40
|
- `--dry-run` (prints intended operations only)
|
|
38
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
|
+
|
|
61
|
+
Bootstrap npm publishing:
|
|
62
|
+
|
|
63
|
+
- `setup-npm`
|
|
64
|
+
- `--dir <directory>` (default: current directory)
|
|
65
|
+
- `--publish-first` (run `npm publish --access public` only when package is not found on npm)
|
|
66
|
+
- `--dry-run` (prints intended operations only)
|
|
67
|
+
|
|
39
68
|
## Managed Standards
|
|
40
69
|
|
|
41
70
|
The generated and managed baseline includes:
|
|
@@ -84,6 +113,39 @@ All commands print a deterministic summary with:
|
|
|
84
113
|
|
|
85
114
|
If `gh` is missing or unauthenticated, command exits non-zero with actionable guidance.
|
|
86
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
|
+
- ensures `release/beta` branch exists remotely (created from default branch if missing)
|
|
123
|
+
- applies beta branch protection ruleset on GitHub
|
|
124
|
+
- asks for confirmation before mutating repository settings and again before overwriting existing beta ruleset
|
|
125
|
+
- supports safe-merge by default and `--force` overwrite
|
|
126
|
+
- supports configurable beta branch (`release/beta` by default)
|
|
127
|
+
|
|
128
|
+
## promote-stable Behavior
|
|
129
|
+
|
|
130
|
+
`promote-stable` prepares stable promotion from prerelease mode:
|
|
131
|
+
|
|
132
|
+
- validates `.changeset/pre.json` exists
|
|
133
|
+
- runs `changeset pre exit`
|
|
134
|
+
- creates a promotion changeset (`patch|minor|major`)
|
|
135
|
+
- prints next step guidance for opening beta->main PR
|
|
136
|
+
|
|
137
|
+
## setup-npm Behavior
|
|
138
|
+
|
|
139
|
+
`setup-npm` validates npm publish readiness:
|
|
140
|
+
|
|
141
|
+
- checks npm CLI availability
|
|
142
|
+
- checks npm authentication (`npm whoami`)
|
|
143
|
+
- checks whether package already exists on npm
|
|
144
|
+
- optionally performs first publish (`--publish-first`)
|
|
145
|
+
- prints next steps for Trusted Publisher configuration
|
|
146
|
+
|
|
147
|
+
Important: Trusted Publisher still needs manual setup in npm package settings.
|
|
148
|
+
|
|
87
149
|
## Trusted Publishing Note
|
|
88
150
|
|
|
89
151
|
If package does not exist on npm yet, first publish may be manual:
|
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,13 +26,19 @@ 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]',
|
|
31
|
+
' create-package-starter setup-npm [--dir <directory>] [--publish-first] [--dry-run]',
|
|
28
32
|
'',
|
|
29
33
|
'Examples:',
|
|
30
34
|
' create-package-starter --name hello-package',
|
|
31
35
|
' create-package-starter --name @i-santos/swarm --out ./packages',
|
|
32
36
|
' create-package-starter init --dir ./my-package',
|
|
33
37
|
' create-package-starter init --cleanup-legacy-release',
|
|
34
|
-
' create-package-starter setup-github --repo i-santos/firestack --dry-run'
|
|
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"',
|
|
41
|
+
' create-package-starter setup-npm --dir . --publish-first'
|
|
35
42
|
].join('\n');
|
|
36
43
|
}
|
|
37
44
|
|
|
@@ -100,6 +107,12 @@ function parseInitArgs(argv) {
|
|
|
100
107
|
continue;
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
if (token === '--repo') {
|
|
111
|
+
args.repo = parseValueFlag(argv, i, '--repo');
|
|
112
|
+
i += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
if (token === '--scope') {
|
|
104
117
|
args.scope = parseValueFlag(argv, i, '--scope');
|
|
105
118
|
i += 1;
|
|
@@ -176,6 +189,155 @@ function parseSetupGithubArgs(argv) {
|
|
|
176
189
|
return args;
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
function parseSetupNpmArgs(argv) {
|
|
193
|
+
const args = {
|
|
194
|
+
dir: process.cwd(),
|
|
195
|
+
publishFirst: false,
|
|
196
|
+
dryRun: false
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
200
|
+
const token = argv[i];
|
|
201
|
+
|
|
202
|
+
if (token === '--dir') {
|
|
203
|
+
args.dir = parseValueFlag(argv, i, '--dir');
|
|
204
|
+
i += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (token === '--publish-first') {
|
|
209
|
+
args.publishFirst = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (token === '--dry-run') {
|
|
214
|
+
args.dryRun = true;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (token === '--help' || token === '-h') {
|
|
219
|
+
args.help = true;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return args;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseSetupBetaArgs(argv) {
|
|
230
|
+
const args = {
|
|
231
|
+
dir: process.cwd(),
|
|
232
|
+
betaBranch: 'release/beta',
|
|
233
|
+
defaultBranch: DEFAULT_BASE_BRANCH,
|
|
234
|
+
force: false,
|
|
235
|
+
yes: false,
|
|
236
|
+
dryRun: false
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
240
|
+
const token = argv[i];
|
|
241
|
+
|
|
242
|
+
if (token === '--dir') {
|
|
243
|
+
args.dir = parseValueFlag(argv, i, '--dir');
|
|
244
|
+
i += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (token === '--repo') {
|
|
249
|
+
args.repo = parseValueFlag(argv, i, '--repo');
|
|
250
|
+
i += 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (token === '--beta-branch') {
|
|
255
|
+
args.betaBranch = parseValueFlag(argv, i, '--beta-branch');
|
|
256
|
+
i += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (token === '--default-branch') {
|
|
261
|
+
args.defaultBranch = parseValueFlag(argv, i, '--default-branch');
|
|
262
|
+
i += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (token === '--force') {
|
|
267
|
+
args.force = true;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (token === '--yes') {
|
|
272
|
+
args.yes = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (token === '--dry-run') {
|
|
277
|
+
args.dryRun = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (token === '--help' || token === '-h') {
|
|
282
|
+
args.help = true;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return args;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parsePromoteStableArgs(argv) {
|
|
293
|
+
const args = {
|
|
294
|
+
dir: process.cwd(),
|
|
295
|
+
type: 'patch',
|
|
296
|
+
summary: 'Promote beta track to stable release.',
|
|
297
|
+
dryRun: false
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
301
|
+
const token = argv[i];
|
|
302
|
+
|
|
303
|
+
if (token === '--dir') {
|
|
304
|
+
args.dir = parseValueFlag(argv, i, '--dir');
|
|
305
|
+
i += 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (token === '--type') {
|
|
310
|
+
args.type = parseValueFlag(argv, i, '--type');
|
|
311
|
+
i += 1;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (token === '--summary') {
|
|
316
|
+
args.summary = parseValueFlag(argv, i, '--summary');
|
|
317
|
+
i += 1;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (token === '--dry-run') {
|
|
322
|
+
args.dryRun = true;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (token === '--help' || token === '-h') {
|
|
327
|
+
args.help = true;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!['patch', 'minor', 'major'].includes(args.type)) {
|
|
335
|
+
throw new Error(`Invalid --type value: ${args.type}. Expected patch, minor, or major.`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return args;
|
|
339
|
+
}
|
|
340
|
+
|
|
179
341
|
function parseArgs(argv) {
|
|
180
342
|
if (argv[0] === 'init') {
|
|
181
343
|
return {
|
|
@@ -191,6 +353,27 @@ function parseArgs(argv) {
|
|
|
191
353
|
};
|
|
192
354
|
}
|
|
193
355
|
|
|
356
|
+
if (argv[0] === 'setup-npm') {
|
|
357
|
+
return {
|
|
358
|
+
mode: 'setup-npm',
|
|
359
|
+
args: parseSetupNpmArgs(argv.slice(1))
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (argv[0] === 'setup-beta') {
|
|
364
|
+
return {
|
|
365
|
+
mode: 'setup-beta',
|
|
366
|
+
args: parseSetupBetaArgs(argv.slice(1))
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (argv[0] === 'promote-stable') {
|
|
371
|
+
return {
|
|
372
|
+
mode: 'promote-stable',
|
|
373
|
+
args: parsePromoteStableArgs(argv.slice(1))
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
194
377
|
return {
|
|
195
378
|
mode: 'create',
|
|
196
379
|
args: parseCreateArgs(argv)
|
|
@@ -308,6 +491,38 @@ function printSummary(title, summary) {
|
|
|
308
491
|
console.log(`warnings: ${list(summary.warnings)}`);
|
|
309
492
|
}
|
|
310
493
|
|
|
494
|
+
function logStep(status, message) {
|
|
495
|
+
const labels = {
|
|
496
|
+
run: '[RUN ]',
|
|
497
|
+
ok: '[OK ]',
|
|
498
|
+
warn: '[WARN]',
|
|
499
|
+
err: '[ERR ]'
|
|
500
|
+
};
|
|
501
|
+
const prefix = labels[status] || '[INFO]';
|
|
502
|
+
const writer = status === 'err' ? console.error : console.log;
|
|
503
|
+
writer(`${prefix} ${message}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function confirmOrThrow(questionText) {
|
|
507
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
508
|
+
throw new Error(`Confirmation required but no interactive terminal was detected. Re-run with --yes if you want to proceed non-interactively.`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const rl = readline.createInterface({
|
|
512
|
+
input: process.stdin,
|
|
513
|
+
output: process.stdout
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
const answer = await rl.question(`${questionText}\nType "yes" to continue: `);
|
|
518
|
+
if (answer.trim().toLowerCase() !== 'yes') {
|
|
519
|
+
throw new Error('Operation cancelled by user.');
|
|
520
|
+
}
|
|
521
|
+
} finally {
|
|
522
|
+
rl.close();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
311
526
|
function ensureFileFromTemplate(targetPath, templatePath, options) {
|
|
312
527
|
const exists = fs.existsSync(targetPath);
|
|
313
528
|
|
|
@@ -328,6 +543,118 @@ function ensureFileFromTemplate(targetPath, templatePath, options) {
|
|
|
328
543
|
return 'created';
|
|
329
544
|
}
|
|
330
545
|
|
|
546
|
+
function ensureReleaseWorkflowBranches(content, defaultBranch, betaBranch) {
|
|
547
|
+
const lines = content.split('\n');
|
|
548
|
+
const onIndex = lines.findIndex((line) => line.trim() === 'on:');
|
|
549
|
+
const permissionsIndex = lines.findIndex((line) => line.trim() === 'permissions:');
|
|
550
|
+
|
|
551
|
+
if (onIndex < 0 || permissionsIndex < 0 || permissionsIndex <= onIndex) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const onBlock = lines.slice(onIndex, permissionsIndex);
|
|
556
|
+
const pushRelativeIndex = onBlock.findIndex((line) => line.trim() === 'push:');
|
|
557
|
+
if (pushRelativeIndex < 0) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const branchesRelativeIndex = onBlock.findIndex((line) => line.trim() === 'branches:');
|
|
562
|
+
if (branchesRelativeIndex < 0 || branchesRelativeIndex <= pushRelativeIndex) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const listStart = branchesRelativeIndex + 1;
|
|
567
|
+
let listEnd = listStart;
|
|
568
|
+
while (listEnd < onBlock.length && onBlock[listEnd].trim().startsWith('- ')) {
|
|
569
|
+
listEnd += 1;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (listEnd === listStart) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const existingBranches = onBlock.slice(listStart, listEnd)
|
|
577
|
+
.map((line) => line.trim().replace(/^- /, '').trim())
|
|
578
|
+
.filter(Boolean);
|
|
579
|
+
|
|
580
|
+
const desiredBranches = [...new Set([defaultBranch, betaBranch])];
|
|
581
|
+
const mergedBranches = [...existingBranches];
|
|
582
|
+
for (const branch of desiredBranches) {
|
|
583
|
+
if (!mergedBranches.includes(branch)) {
|
|
584
|
+
mergedBranches.push(branch);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const changed = mergedBranches.length !== existingBranches.length
|
|
589
|
+
|| mergedBranches.some((branch, index) => branch !== existingBranches[index]);
|
|
590
|
+
|
|
591
|
+
if (!changed) {
|
|
592
|
+
return {
|
|
593
|
+
changed: false,
|
|
594
|
+
content
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const updatedOnBlock = [
|
|
599
|
+
...onBlock.slice(0, listStart),
|
|
600
|
+
...mergedBranches.map((branch) => ` - ${branch}`),
|
|
601
|
+
...onBlock.slice(listEnd)
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
const updatedLines = [
|
|
605
|
+
...lines.slice(0, onIndex),
|
|
606
|
+
...updatedOnBlock,
|
|
607
|
+
...lines.slice(permissionsIndex)
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
changed: true,
|
|
612
|
+
content: updatedLines.join('\n')
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function upsertReleaseWorkflow(targetPath, templatePath, options) {
|
|
617
|
+
const exists = fs.existsSync(targetPath);
|
|
618
|
+
if (!exists || options.force) {
|
|
619
|
+
if (options.dryRun) {
|
|
620
|
+
return {
|
|
621
|
+
result: exists ? 'overwritten' : 'created'
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const result = ensureFileFromTemplate(targetPath, templatePath, {
|
|
626
|
+
force: options.force,
|
|
627
|
+
variables: options.variables
|
|
628
|
+
});
|
|
629
|
+
return { result };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const current = fs.readFileSync(targetPath, 'utf8');
|
|
633
|
+
const ensured = ensureReleaseWorkflowBranches(
|
|
634
|
+
current,
|
|
635
|
+
options.variables.DEFAULT_BRANCH,
|
|
636
|
+
options.variables.BETA_BRANCH
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
if (!ensured) {
|
|
640
|
+
return {
|
|
641
|
+
result: 'skipped',
|
|
642
|
+
warning: `Could not safely update trigger branches in ${path.basename(targetPath)}. Use --force to overwrite from template.`
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!ensured.changed) {
|
|
647
|
+
return { result: 'skipped' };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (options.dryRun) {
|
|
651
|
+
return { result: 'updated' };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fs.writeFileSync(targetPath, ensured.content);
|
|
655
|
+
return { result: 'updated' };
|
|
656
|
+
}
|
|
657
|
+
|
|
331
658
|
function detectEquivalentManagedFile(packageDir, targetRelativePath) {
|
|
332
659
|
if (targetRelativePath !== '.github/PULL_REQUEST_TEMPLATE.md') {
|
|
333
660
|
return targetRelativePath;
|
|
@@ -410,7 +737,12 @@ function configureExistingPackage(packageDir, templateDir, options) {
|
|
|
410
737
|
check: 'npm run test',
|
|
411
738
|
changeset: 'changeset',
|
|
412
739
|
'version-packages': 'changeset version',
|
|
413
|
-
release: 'npm run check && changeset publish'
|
|
740
|
+
release: 'npm run check && changeset publish',
|
|
741
|
+
'beta:enter': 'changeset pre enter beta',
|
|
742
|
+
'beta:exit': 'changeset pre exit',
|
|
743
|
+
'beta:version': 'changeset version',
|
|
744
|
+
'beta:publish': 'changeset publish',
|
|
745
|
+
'beta:promote': 'create-package-starter promote-stable --dir .'
|
|
414
746
|
};
|
|
415
747
|
|
|
416
748
|
let packageJsonChanged = false;
|
|
@@ -472,6 +804,7 @@ function configureExistingPackage(packageDir, templateDir, options) {
|
|
|
472
804
|
variables: {
|
|
473
805
|
PACKAGE_NAME: packageName,
|
|
474
806
|
DEFAULT_BRANCH: options.defaultBranch,
|
|
807
|
+
BETA_BRANCH: options.betaBranch || 'release/beta',
|
|
475
808
|
SCOPE: deriveScope(options.scope, packageName)
|
|
476
809
|
}
|
|
477
810
|
}, summary);
|
|
@@ -507,12 +840,14 @@ function createNewPackage(args) {
|
|
|
507
840
|
const createdFiles = copyDirRecursive(templateDir, targetDir, {
|
|
508
841
|
PACKAGE_NAME: args.name,
|
|
509
842
|
DEFAULT_BRANCH: args.defaultBranch,
|
|
843
|
+
BETA_BRANCH: 'release/beta',
|
|
510
844
|
SCOPE: deriveScope('', args.name)
|
|
511
845
|
});
|
|
512
846
|
|
|
513
847
|
summary.createdFiles.push(...createdFiles);
|
|
514
848
|
|
|
515
849
|
summary.updatedScriptKeys.push('check', 'changeset', 'version-packages', 'release');
|
|
850
|
+
summary.updatedScriptKeys.push('beta:enter', 'beta:exit', 'beta:version', 'beta:publish', 'beta:promote');
|
|
516
851
|
summary.updatedDependencyKeys.push(CHANGESETS_DEP);
|
|
517
852
|
|
|
518
853
|
printSummary(`Package created in ${targetDir}`, summary);
|
|
@@ -592,6 +927,35 @@ function createBaseRulesetPayload(defaultBranch) {
|
|
|
592
927
|
};
|
|
593
928
|
}
|
|
594
929
|
|
|
930
|
+
function createBetaRulesetPayload(betaBranch) {
|
|
931
|
+
return {
|
|
932
|
+
name: `Beta branch protection (${betaBranch})`,
|
|
933
|
+
target: 'branch',
|
|
934
|
+
enforcement: 'active',
|
|
935
|
+
conditions: {
|
|
936
|
+
ref_name: {
|
|
937
|
+
include: [`refs/heads/${betaBranch}`],
|
|
938
|
+
exclude: []
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
bypass_actors: [],
|
|
942
|
+
rules: [
|
|
943
|
+
{ type: 'deletion' },
|
|
944
|
+
{ type: 'non_fast_forward' },
|
|
945
|
+
{
|
|
946
|
+
type: 'pull_request',
|
|
947
|
+
parameters: {
|
|
948
|
+
required_approving_review_count: 0,
|
|
949
|
+
dismiss_stale_reviews_on_push: true,
|
|
950
|
+
require_code_owner_review: false,
|
|
951
|
+
require_last_push_approval: false,
|
|
952
|
+
required_review_thread_resolution: true
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
]
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
595
959
|
function createRulesetPayload(args) {
|
|
596
960
|
if (!args.ruleset) {
|
|
597
961
|
return createBaseRulesetPayload(args.defaultBranch);
|
|
@@ -683,6 +1047,423 @@ function updateWorkflowPermissions(deps, repo) {
|
|
|
683
1047
|
}
|
|
684
1048
|
}
|
|
685
1049
|
|
|
1050
|
+
function isNotFoundResponse(result) {
|
|
1051
|
+
const output = `${result.stderr || ''}\n${result.stdout || ''}`.toLowerCase();
|
|
1052
|
+
return output.includes('404') || output.includes('not found');
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function ensureBranchExists(deps, repo, defaultBranch, targetBranch) {
|
|
1056
|
+
const encodedTarget = encodeURIComponent(targetBranch);
|
|
1057
|
+
const getTarget = ghApi(deps, 'GET', `/repos/${repo}/branches/${encodedTarget}`);
|
|
1058
|
+
if (getTarget.status === 0) {
|
|
1059
|
+
return 'exists';
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (!isNotFoundResponse(getTarget)) {
|
|
1063
|
+
throw new Error(`Failed to check branch "${targetBranch}": ${getTarget.stderr || getTarget.stdout}`.trim());
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const encodedDefault = encodeURIComponent(defaultBranch);
|
|
1067
|
+
const getDefaultRef = ghApi(deps, 'GET', `/repos/${repo}/git/ref/heads/${encodedDefault}`);
|
|
1068
|
+
if (getDefaultRef.status !== 0) {
|
|
1069
|
+
throw new Error(`Failed to resolve default branch "${defaultBranch}": ${getDefaultRef.stderr || getDefaultRef.stdout}`.trim());
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const parsed = parseJsonOutput(getDefaultRef.stdout || '{}', 'Failed to parse default branch ref from GitHub API.');
|
|
1073
|
+
const sha = parsed && parsed.object && parsed.object.sha;
|
|
1074
|
+
if (!sha) {
|
|
1075
|
+
throw new Error(`Could not determine SHA for default branch "${defaultBranch}".`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const createRef = ghApi(deps, 'POST', `/repos/${repo}/git/refs`, {
|
|
1079
|
+
ref: `refs/heads/${targetBranch}`,
|
|
1080
|
+
sha
|
|
1081
|
+
});
|
|
1082
|
+
if (createRef.status !== 0) {
|
|
1083
|
+
throw new Error(`Failed to create branch "${targetBranch}": ${createRef.stderr || createRef.stdout}`.trim());
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return 'created';
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function branchExists(deps, repo, targetBranch) {
|
|
1090
|
+
const encodedTarget = encodeURIComponent(targetBranch);
|
|
1091
|
+
const getTarget = ghApi(deps, 'GET', `/repos/${repo}/branches/${encodedTarget}`);
|
|
1092
|
+
if (getTarget.status === 0) {
|
|
1093
|
+
return true;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (isNotFoundResponse(getTarget)) {
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
throw new Error(`Failed to check branch "${targetBranch}": ${getTarget.stderr || getTarget.stdout}`.trim());
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function findRulesetByName(deps, repo, name) {
|
|
1104
|
+
const listResult = ghApi(deps, 'GET', `/repos/${repo}/rulesets`);
|
|
1105
|
+
if (listResult.status !== 0) {
|
|
1106
|
+
throw new Error(`Failed to list rulesets: ${listResult.stderr || listResult.stdout}`.trim());
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const rulesets = parseJsonOutput(listResult.stdout || '[]', 'Failed to parse rulesets response from GitHub API.');
|
|
1110
|
+
return rulesets.find((ruleset) => ruleset.name === name) || null;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function ensureNpmAvailable(deps) {
|
|
1114
|
+
const version = deps.exec('npm', ['--version']);
|
|
1115
|
+
if (version.status !== 0) {
|
|
1116
|
+
throw new Error('npm CLI is required. Install npm and rerun.');
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function ensureNpmAuthenticated(deps) {
|
|
1121
|
+
const whoami = deps.exec('npm', ['whoami']);
|
|
1122
|
+
if (whoami.status !== 0) {
|
|
1123
|
+
throw new Error('npm CLI is not authenticated. Run "npm login" and rerun.');
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function packageExistsOnNpm(deps, packageName) {
|
|
1128
|
+
const view = deps.exec('npm', ['view', packageName, 'version', '--json']);
|
|
1129
|
+
if (view.status === 0) {
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const output = `${view.stderr || ''}\n${view.stdout || ''}`.toLowerCase();
|
|
1134
|
+
if (output.includes('e404') || output.includes('not found') || output.includes('404')) {
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
throw new Error(`Failed to check package on npm: ${view.stderr || view.stdout}`.trim());
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function setupNpm(args, dependencies = {}) {
|
|
1142
|
+
const deps = {
|
|
1143
|
+
exec: dependencies.exec || execCommand
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const targetDir = path.resolve(args.dir);
|
|
1147
|
+
if (!fs.existsSync(targetDir)) {
|
|
1148
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
1152
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1153
|
+
throw new Error(`package.json not found in ${targetDir}`);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
1157
|
+
if (!packageJson.name) {
|
|
1158
|
+
throw new Error(`package.json in ${targetDir} must define "name".`);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
ensureNpmAvailable(deps);
|
|
1162
|
+
ensureNpmAuthenticated(deps);
|
|
1163
|
+
|
|
1164
|
+
const summary = createSummary();
|
|
1165
|
+
summary.updatedScriptKeys.push('npm.auth', 'npm.package.lookup');
|
|
1166
|
+
|
|
1167
|
+
if (!packageJson.publishConfig || packageJson.publishConfig.access !== 'public') {
|
|
1168
|
+
summary.warnings.push('package.json publishConfig.access is not "public". First publish may fail for public packages.');
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const existsOnNpm = packageExistsOnNpm(deps, packageJson.name);
|
|
1172
|
+
if (existsOnNpm) {
|
|
1173
|
+
summary.skippedScriptKeys.push('npm.first_publish');
|
|
1174
|
+
} else {
|
|
1175
|
+
summary.updatedScriptKeys.push('npm.first_publish_required');
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (!existsOnNpm && !args.publishFirst) {
|
|
1179
|
+
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.`);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (args.publishFirst) {
|
|
1183
|
+
if (existsOnNpm) {
|
|
1184
|
+
summary.warnings.push(`package "${packageJson.name}" already exists on npm. Skipping first publish.`);
|
|
1185
|
+
} else if (args.dryRun) {
|
|
1186
|
+
summary.warnings.push(`dry-run: would run "npm publish --access public" in ${targetDir}`);
|
|
1187
|
+
} else {
|
|
1188
|
+
const publish = deps.exec('npm', ['publish', '--access', 'public'], { cwd: targetDir, stdio: 'inherit' });
|
|
1189
|
+
if (publish.status !== 0) {
|
|
1190
|
+
const publishOutput = `${publish.stderr || ''}\n${publish.stdout || ''}`.toLowerCase();
|
|
1191
|
+
const isOtpError = publishOutput.includes('eotp') || publishOutput.includes('one-time password');
|
|
1192
|
+
|
|
1193
|
+
if (isOtpError) {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
[
|
|
1196
|
+
'First publish failed due to npm 2FA/OTP requirements.',
|
|
1197
|
+
'This command already delegates to the standard npm publish flow.',
|
|
1198
|
+
'If npm still requires manual OTP entry, complete publish manually:',
|
|
1199
|
+
` (cd ${targetDir} && npm publish --access public)`
|
|
1200
|
+
].join('\n')
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
throw new Error('First publish failed. Check npm output above and try again.');
|
|
1205
|
+
}
|
|
1206
|
+
summary.updatedScriptKeys.push('npm.first_publish_done');
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
summary.warnings.push('Configure npm Trusted Publisher manually in npm package settings after first publish.');
|
|
1211
|
+
summary.warnings.push('Trusted Publisher requires owner, repository, workflow file (.github/workflows/release.yml), and branch (main by default).');
|
|
1212
|
+
|
|
1213
|
+
printSummary(`npm setup completed for ${packageJson.name}`, summary);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
async function setupBeta(args, dependencies = {}) {
|
|
1217
|
+
const deps = {
|
|
1218
|
+
exec: dependencies.exec || execCommand
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const targetDir = path.resolve(args.dir);
|
|
1222
|
+
if (!fs.existsSync(targetDir)) {
|
|
1223
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
1227
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1228
|
+
throw new Error(`package.json not found in ${targetDir}`);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
1232
|
+
const templateDir = path.join(packageRoot, 'template');
|
|
1233
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
1234
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
1235
|
+
|
|
1236
|
+
logStep('run', 'Checking GitHub CLI availability and authentication...');
|
|
1237
|
+
try {
|
|
1238
|
+
ensureGhAvailable(deps);
|
|
1239
|
+
logStep('ok', 'GitHub CLI is available and authenticated.');
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
logStep('err', error.message);
|
|
1242
|
+
if (error.message.includes('not authenticated')) {
|
|
1243
|
+
logStep('warn', 'Run "gh auth login" and retry.');
|
|
1244
|
+
}
|
|
1245
|
+
throw error;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
logStep('run', 'Resolving repository target...');
|
|
1249
|
+
const repo = resolveRepo(args, deps);
|
|
1250
|
+
logStep('ok', `Using repository ${repo}.`);
|
|
1251
|
+
|
|
1252
|
+
const summary = createSummary();
|
|
1253
|
+
summary.updatedScriptKeys.push('github.beta_branch', 'github.beta_ruleset', 'actions.default_workflow_permissions');
|
|
1254
|
+
const desiredScripts = {
|
|
1255
|
+
'beta:enter': 'changeset pre enter beta',
|
|
1256
|
+
'beta:exit': 'changeset pre exit',
|
|
1257
|
+
'beta:version': 'changeset version',
|
|
1258
|
+
'beta:publish': 'changeset publish',
|
|
1259
|
+
'beta:promote': 'create-package-starter promote-stable --dir .'
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
let packageJsonChanged = false;
|
|
1263
|
+
for (const [key, value] of Object.entries(desiredScripts)) {
|
|
1264
|
+
const exists = Object.prototype.hasOwnProperty.call(packageJson.scripts, key);
|
|
1265
|
+
if (!exists || args.force) {
|
|
1266
|
+
if (!exists || packageJson.scripts[key] !== value) {
|
|
1267
|
+
packageJson.scripts[key] = value;
|
|
1268
|
+
packageJsonChanged = true;
|
|
1269
|
+
}
|
|
1270
|
+
summary.updatedScriptKeys.push(key);
|
|
1271
|
+
} else {
|
|
1272
|
+
summary.skippedScriptKeys.push(key);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const workflowRelativePath = '.github/workflows/release.yml';
|
|
1277
|
+
const workflowTemplatePath = path.join(templateDir, workflowRelativePath);
|
|
1278
|
+
const workflowTargetPath = path.join(targetDir, workflowRelativePath);
|
|
1279
|
+
if (!fs.existsSync(workflowTemplatePath)) {
|
|
1280
|
+
throw new Error(`Template not found: ${workflowTemplatePath}`);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (args.dryRun) {
|
|
1284
|
+
logStep('warn', 'Dry-run mode enabled. No remote or file changes will be applied.');
|
|
1285
|
+
const workflowPreview = upsertReleaseWorkflow(workflowTargetPath, workflowTemplatePath, {
|
|
1286
|
+
force: args.force,
|
|
1287
|
+
dryRun: true,
|
|
1288
|
+
variables: {
|
|
1289
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
1290
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
1291
|
+
BETA_BRANCH: args.betaBranch,
|
|
1292
|
+
SCOPE: deriveScope('', packageJson.name || '')
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
if (workflowPreview.result === 'created') {
|
|
1296
|
+
summary.warnings.push(`dry-run: would create ${workflowRelativePath}`);
|
|
1297
|
+
} else if (workflowPreview.result === 'overwritten') {
|
|
1298
|
+
summary.warnings.push(`dry-run: would overwrite ${workflowRelativePath}`);
|
|
1299
|
+
} else if (workflowPreview.result === 'updated') {
|
|
1300
|
+
summary.warnings.push(`dry-run: would update ${workflowRelativePath} trigger branches`);
|
|
1301
|
+
} else {
|
|
1302
|
+
summary.warnings.push(`dry-run: would keep existing ${workflowRelativePath}`);
|
|
1303
|
+
if (workflowPreview.warning) {
|
|
1304
|
+
summary.warnings.push(`dry-run: ${workflowPreview.warning}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (packageJsonChanged) {
|
|
1308
|
+
summary.warnings.push('dry-run: would update package.json beta scripts');
|
|
1309
|
+
}
|
|
1310
|
+
summary.warnings.push(`dry-run: would ensure branch "${args.betaBranch}" exists in ${repo}`);
|
|
1311
|
+
summary.warnings.push(`dry-run: would upsert ruleset for refs/heads/${args.betaBranch}`);
|
|
1312
|
+
summary.warnings.push(`dry-run: would set Actions workflow permissions to write for ${repo}`);
|
|
1313
|
+
summary.warnings.push(`dry-run: beta branch configured as ${args.betaBranch}`);
|
|
1314
|
+
} else {
|
|
1315
|
+
const betaRulesetPayload = createBetaRulesetPayload(args.betaBranch);
|
|
1316
|
+
const doesBranchExist = branchExists(deps, repo, args.betaBranch);
|
|
1317
|
+
const existingRuleset = findRulesetByName(deps, repo, betaRulesetPayload.name);
|
|
1318
|
+
|
|
1319
|
+
if (args.yes) {
|
|
1320
|
+
logStep('warn', 'Confirmation prompts skipped due to --yes.');
|
|
1321
|
+
} else {
|
|
1322
|
+
await confirmOrThrow(
|
|
1323
|
+
[
|
|
1324
|
+
`This will modify GitHub repository settings for ${repo}:`,
|
|
1325
|
+
`- set Actions workflow permissions to write`,
|
|
1326
|
+
`- ensure branch "${args.betaBranch}" exists${doesBranchExist ? ' (already exists)' : ' (will be created)'}`,
|
|
1327
|
+
`- apply branch protection ruleset "${betaRulesetPayload.name}"`,
|
|
1328
|
+
`- update local ${workflowRelativePath} and package.json beta scripts`
|
|
1329
|
+
].join('\n')
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
if (existingRuleset) {
|
|
1333
|
+
await confirmOrThrow(
|
|
1334
|
+
`Ruleset "${betaRulesetPayload.name}" already exists and will be overwritten.`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
logStep('run', `Ensuring ${workflowRelativePath} includes stable+beta triggers...`);
|
|
1340
|
+
const workflowUpsert = upsertReleaseWorkflow(workflowTargetPath, workflowTemplatePath, {
|
|
1341
|
+
force: args.force,
|
|
1342
|
+
dryRun: false,
|
|
1343
|
+
variables: {
|
|
1344
|
+
PACKAGE_NAME: packageJson.name || packageDirFromName(path.basename(targetDir)),
|
|
1345
|
+
DEFAULT_BRANCH: args.defaultBranch,
|
|
1346
|
+
BETA_BRANCH: args.betaBranch,
|
|
1347
|
+
SCOPE: deriveScope('', packageJson.name || '')
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
const workflowResult = workflowUpsert.result;
|
|
1351
|
+
|
|
1352
|
+
if (workflowResult === 'created') {
|
|
1353
|
+
summary.createdFiles.push(workflowRelativePath);
|
|
1354
|
+
logStep('ok', `${workflowRelativePath} created.`);
|
|
1355
|
+
} else if (workflowResult === 'overwritten') {
|
|
1356
|
+
summary.overwrittenFiles.push(workflowRelativePath);
|
|
1357
|
+
logStep('ok', `${workflowRelativePath} overwritten.`);
|
|
1358
|
+
} else if (workflowResult === 'updated') {
|
|
1359
|
+
summary.overwrittenFiles.push(workflowRelativePath);
|
|
1360
|
+
logStep('ok', `${workflowRelativePath} updated with missing branch triggers.`);
|
|
1361
|
+
} else {
|
|
1362
|
+
summary.skippedFiles.push(workflowRelativePath);
|
|
1363
|
+
if (workflowUpsert.warning) {
|
|
1364
|
+
summary.warnings.push(workflowUpsert.warning);
|
|
1365
|
+
logStep('warn', workflowUpsert.warning);
|
|
1366
|
+
} else {
|
|
1367
|
+
logStep('warn', `${workflowRelativePath} already configured; kept as-is.`);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (packageJsonChanged) {
|
|
1372
|
+
logStep('run', 'Updating package.json beta scripts...');
|
|
1373
|
+
writeJsonFile(packageJsonPath, packageJson);
|
|
1374
|
+
logStep('ok', 'package.json beta scripts updated.');
|
|
1375
|
+
} else {
|
|
1376
|
+
logStep('warn', 'package.json beta scripts already present; no changes needed.');
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
logStep('run', 'Applying GitHub Actions workflow permissions...');
|
|
1380
|
+
updateWorkflowPermissions(deps, repo);
|
|
1381
|
+
logStep('ok', 'Workflow permissions configured.');
|
|
1382
|
+
|
|
1383
|
+
logStep('run', `Ensuring branch "${args.betaBranch}" exists...`);
|
|
1384
|
+
const branchResult = ensureBranchExists(deps, repo, args.defaultBranch, args.betaBranch);
|
|
1385
|
+
if (branchResult === 'created') {
|
|
1386
|
+
summary.createdFiles.push(`github-branch:${args.betaBranch}`);
|
|
1387
|
+
logStep('ok', `Branch "${args.betaBranch}" created from "${args.defaultBranch}".`);
|
|
1388
|
+
} else {
|
|
1389
|
+
summary.skippedFiles.push(`github-branch:${args.betaBranch}`);
|
|
1390
|
+
logStep('warn', `Branch "${args.betaBranch}" already exists.`);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
logStep('run', `Applying protection ruleset to "${args.betaBranch}"...`);
|
|
1394
|
+
const upsertResult = upsertRuleset(deps, repo, betaRulesetPayload);
|
|
1395
|
+
summary.overwrittenFiles.push(`github-beta-ruleset:${upsertResult}`);
|
|
1396
|
+
logStep('ok', `Beta branch ruleset ${upsertResult}.`);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
summary.warnings.push(`Trusted Publisher supports a single workflow file per package. Keep publishing on .github/workflows/release.yml for both stable and beta.`);
|
|
1400
|
+
summary.warnings.push(`Next step: run "npm run beta:enter" once on "${args.betaBranch}", commit .changeset/pre.json, and push.`);
|
|
1401
|
+
printSummary(`beta setup completed for ${targetDir}`, summary);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function createChangesetFile(targetDir, packageName, bumpType, summaryText) {
|
|
1405
|
+
const changesetDir = path.join(targetDir, '.changeset');
|
|
1406
|
+
fs.mkdirSync(changesetDir, { recursive: true });
|
|
1407
|
+
const fileName = `promote-stable-${Date.now()}.md`;
|
|
1408
|
+
const filePath = path.join(changesetDir, fileName);
|
|
1409
|
+
const content = [
|
|
1410
|
+
'---',
|
|
1411
|
+
`"${packageName}": ${bumpType}`,
|
|
1412
|
+
'---',
|
|
1413
|
+
'',
|
|
1414
|
+
summaryText
|
|
1415
|
+
].join('\n');
|
|
1416
|
+
fs.writeFileSync(filePath, `${content}\n`);
|
|
1417
|
+
return path.posix.join('.changeset', fileName);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function promoteStable(args, dependencies = {}) {
|
|
1421
|
+
const deps = {
|
|
1422
|
+
exec: dependencies.exec || execCommand
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
const targetDir = path.resolve(args.dir);
|
|
1426
|
+
if (!fs.existsSync(targetDir)) {
|
|
1427
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
1431
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1432
|
+
throw new Error(`package.json not found in ${targetDir}`);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const prePath = path.join(targetDir, '.changeset', 'pre.json');
|
|
1436
|
+
if (!fs.existsSync(prePath)) {
|
|
1437
|
+
throw new Error(`No prerelease state found in ${targetDir}. Run "changeset pre enter beta" first.`);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
1441
|
+
if (!packageJson.name) {
|
|
1442
|
+
throw new Error(`package.json in ${targetDir} must define "name".`);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const summary = createSummary();
|
|
1446
|
+
summary.updatedScriptKeys.push('changeset.pre_exit', 'changeset.promote_stable');
|
|
1447
|
+
|
|
1448
|
+
if (args.dryRun) {
|
|
1449
|
+
summary.warnings.push(`dry-run: would run "npx @changesets/cli pre exit" in ${targetDir}`);
|
|
1450
|
+
summary.warnings.push(`dry-run: would create promotion changeset for ${packageJson.name} (${args.type})`);
|
|
1451
|
+
summary.warnings.push(`dry-run: promote flow targets stable branch ${DEFAULT_BASE_BRANCH}`);
|
|
1452
|
+
printSummary(`stable promotion dry-run for ${targetDir}`, summary);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const preExit = deps.exec('npx', ['@changesets/cli', 'pre', 'exit'], { cwd: targetDir });
|
|
1457
|
+
if (preExit.status !== 0) {
|
|
1458
|
+
throw new Error(`Failed to exit prerelease mode: ${(preExit.stderr || preExit.stdout || '').trim()}`);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const createdChangeset = createChangesetFile(targetDir, packageJson.name, args.type, args.summary);
|
|
1462
|
+
summary.createdFiles.push(createdChangeset);
|
|
1463
|
+
summary.warnings.push('Next step: open PR from beta branch to main and merge to publish stable.');
|
|
1464
|
+
printSummary(`stable promotion prepared for ${targetDir}`, summary);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
686
1467
|
function setupGithub(args, dependencies = {}) {
|
|
687
1468
|
const deps = {
|
|
688
1469
|
exec: dependencies.exec || execCommand
|
|
@@ -750,6 +1531,21 @@ async function run(argv, dependencies = {}) {
|
|
|
750
1531
|
return;
|
|
751
1532
|
}
|
|
752
1533
|
|
|
1534
|
+
if (parsed.mode === 'setup-beta') {
|
|
1535
|
+
setupBeta(parsed.args, dependencies);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (parsed.mode === 'promote-stable') {
|
|
1540
|
+
promoteStable(parsed.args, dependencies);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (parsed.mode === 'setup-npm') {
|
|
1545
|
+
setupNpm(parsed.args, dependencies);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
753
1549
|
createNewPackage(parsed.args);
|
|
754
1550
|
}
|
|
755
1551
|
|
|
@@ -757,5 +1553,9 @@ module.exports = {
|
|
|
757
1553
|
run,
|
|
758
1554
|
parseRepoFromRemote,
|
|
759
1555
|
createBaseRulesetPayload,
|
|
760
|
-
|
|
1556
|
+
createBetaRulesetPayload,
|
|
1557
|
+
setupGithub,
|
|
1558
|
+
setupNpm,
|
|
1559
|
+
setupBeta,
|
|
1560
|
+
promoteStable
|
|
761
1561
|
};
|
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"
|