@i-santos/create-package-starter 1.5.0-beta.8 → 1.5.0-beta.9
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 +14 -4
- package/lib/run.js +584 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,15 +95,20 @@ Orchestrate release cycle:
|
|
|
95
95
|
- `--title <text>`
|
|
96
96
|
- `--body-file <path>`
|
|
97
97
|
- `--draft`
|
|
98
|
+
- `--phase <code|full>` (default: `full`)
|
|
98
99
|
- `--auto-merge` (default behavior: enabled)
|
|
99
100
|
- `--watch-checks` (default behavior: enabled)
|
|
100
101
|
- `--check-timeout <minutes>` (default: `30`)
|
|
102
|
+
- `--confirm-merges` (require confirmation before each merge step)
|
|
101
103
|
- `--merge-when-green` (default behavior: enabled)
|
|
102
104
|
- `--merge-method <squash|merge|rebase>` (default: `squash`)
|
|
103
105
|
- `--wait-release-pr` (default behavior: enabled)
|
|
104
106
|
- `--release-pr-timeout <minutes>` (default: `30`)
|
|
105
107
|
- `--merge-release-pr` (default behavior: enabled)
|
|
106
108
|
- `--verify-npm` (default behavior: enabled)
|
|
109
|
+
- `--confirm-cleanup` (require confirmation before cleanup; only after npm validation pass)
|
|
110
|
+
- `--sync-base <auto|rebase|merge|off>` (default: `auto`; keeps code branch updated with `release/beta`)
|
|
111
|
+
- `--no-resume` (disable automatic resume/reconciliation behavior)
|
|
107
112
|
- `--no-cleanup` (disable default local cleanup after successful cycle)
|
|
108
113
|
- `--yes`
|
|
109
114
|
- `--dry-run`
|
|
@@ -221,25 +226,30 @@ Body source priority:
|
|
|
221
226
|
|
|
222
227
|
Default mode is `auto`:
|
|
223
228
|
- if current branch starts with `changeset-release/` => `publish`
|
|
224
|
-
- else if exactly one open `changeset-release/*` PR exists => `publish`
|
|
229
|
+
- else if current branch is `release/beta` and exactly one open `changeset-release/*` PR exists => `publish`
|
|
225
230
|
- else => `open-pr`
|
|
226
231
|
|
|
227
232
|
For `open-pr` mode:
|
|
228
233
|
- runs open-pr flow
|
|
229
|
-
-
|
|
234
|
+
- auto-syncs feature/fix branch with `origin/release/beta` before PR (default `--sync-base auto`)
|
|
235
|
+
- enables auto-merge for code PR by default
|
|
236
|
+
- auto-resumes release phase if current code branch is already integrated into `release/beta`
|
|
237
|
+
- supports `--phase code` to stop after code PR merge
|
|
230
238
|
- can wait for release PR creation (`changeset-release/*`)
|
|
231
|
-
-
|
|
239
|
+
- enables auto-merge for release PR by default
|
|
232
240
|
- validates npm publish (package + version + expected dist-tag)
|
|
233
241
|
- cleans local branch by default when safety gates are satisfied (`--no-cleanup` to disable)
|
|
234
242
|
|
|
235
243
|
For `publish` mode:
|
|
236
244
|
- resolves release PR directly
|
|
237
245
|
- watches checks
|
|
238
|
-
-
|
|
246
|
+
- enables auto-merge and waits for merge completion (policy permitting)
|
|
239
247
|
|
|
240
248
|
The command is policy-aware:
|
|
241
249
|
- never bypasses required checks/reviews/rulesets
|
|
242
250
|
- fails fast with actionable diagnostics when blocked
|
|
251
|
+
- stops with actionable guidance when approval is still required before merge
|
|
252
|
+
- cleanup runs only when npm validation passes
|
|
243
253
|
|
|
244
254
|
### Protected `release/beta` stable promotion
|
|
245
255
|
|
package/lib/run.js
CHANGED
|
@@ -45,7 +45,7 @@ function usage() {
|
|
|
45
45
|
' create-package-starter setup-github [--repo <owner/repo>] [--default-branch <branch>] [--ruleset <path>] [--dry-run]',
|
|
46
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
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] [--track auto|beta|stable] [--promote-stable] [--promote-type patch|minor|major] [--promote-summary <text>] [--head <branch>] [--base <branch>] [--title <text>] [--body-file <path>] [--draft] [--auto-merge] [--watch-checks] [--check-timeout <minutes>] [--merge-when-green] [--merge-method squash|merge|rebase] [--wait-release-pr] [--release-pr-timeout <minutes>] [--merge-release-pr] [--verify-npm] [--no-cleanup] [--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
49
|
' create-package-starter promote-stable [--dir <directory>] [--type patch|minor|major] [--summary <text>] [--dry-run]',
|
|
50
50
|
' create-package-starter setup-npm [--dir <directory>] [--publish-first] [--dry-run]',
|
|
51
51
|
'',
|
|
@@ -446,6 +446,7 @@ function parseOpenPrArgs(argv) {
|
|
|
446
446
|
autoMerge: false,
|
|
447
447
|
watchChecks: false,
|
|
448
448
|
checkTimeout: 30,
|
|
449
|
+
updateExistingPr: true,
|
|
449
450
|
yes: false,
|
|
450
451
|
dryRun: false
|
|
451
452
|
};
|
|
@@ -496,11 +497,16 @@ function parseOpenPrArgs(argv) {
|
|
|
496
497
|
}
|
|
497
498
|
|
|
498
499
|
if (token === '--check-timeout') {
|
|
499
|
-
args.checkTimeout = Number.
|
|
500
|
+
args.checkTimeout = Number.parseFloat(parseValueFlag(argv, i, '--check-timeout'));
|
|
500
501
|
i += 1;
|
|
501
502
|
continue;
|
|
502
503
|
}
|
|
503
504
|
|
|
505
|
+
if (token === '--update-pr-description') {
|
|
506
|
+
args.updateExistingPr = true;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
504
510
|
if (token === '--draft') {
|
|
505
511
|
args.draft = true;
|
|
506
512
|
continue;
|
|
@@ -534,8 +540,8 @@ function parseOpenPrArgs(argv) {
|
|
|
534
540
|
throw new Error(`Invalid argument: ${token}\n\n${usage()}`);
|
|
535
541
|
}
|
|
536
542
|
|
|
537
|
-
if (!Number.
|
|
538
|
-
throw new Error('Invalid --check-timeout value. Expected a positive
|
|
543
|
+
if (!Number.isFinite(args.checkTimeout) || args.checkTimeout <= 0) {
|
|
544
|
+
throw new Error('Invalid --check-timeout value. Expected a positive number (minutes).');
|
|
539
545
|
}
|
|
540
546
|
|
|
541
547
|
return args;
|
|
@@ -545,6 +551,8 @@ function parseReleaseCycleArgs(argv) {
|
|
|
545
551
|
const args = {
|
|
546
552
|
repo: '',
|
|
547
553
|
mode: 'auto',
|
|
554
|
+
phase: 'full',
|
|
555
|
+
phaseProvided: false,
|
|
548
556
|
track: 'auto',
|
|
549
557
|
promoteStable: false,
|
|
550
558
|
promoteType: 'patch',
|
|
@@ -553,16 +561,21 @@ function parseReleaseCycleArgs(argv) {
|
|
|
553
561
|
base: '',
|
|
554
562
|
title: '',
|
|
555
563
|
bodyFile: '',
|
|
564
|
+
updatePrDescription: false,
|
|
556
565
|
draft: false,
|
|
557
566
|
autoMerge: true,
|
|
558
567
|
watchChecks: true,
|
|
559
568
|
checkTimeout: 30,
|
|
569
|
+
confirmMerges: false,
|
|
570
|
+
syncBase: 'auto',
|
|
571
|
+
resume: true,
|
|
560
572
|
mergeWhenGreen: true,
|
|
561
573
|
mergeMethod: 'squash',
|
|
562
574
|
waitReleasePr: true,
|
|
563
575
|
releasePrTimeout: 30,
|
|
564
576
|
mergeReleasePr: true,
|
|
565
577
|
verifyNpm: true,
|
|
578
|
+
confirmCleanup: false,
|
|
566
579
|
noCleanup: false,
|
|
567
580
|
yes: false,
|
|
568
581
|
dryRun: false
|
|
@@ -583,6 +596,13 @@ function parseReleaseCycleArgs(argv) {
|
|
|
583
596
|
continue;
|
|
584
597
|
}
|
|
585
598
|
|
|
599
|
+
if (token === '--phase') {
|
|
600
|
+
args.phase = parseValueFlag(argv, i, '--phase');
|
|
601
|
+
args.phaseProvided = true;
|
|
602
|
+
i += 1;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
586
606
|
if (token === '--track') {
|
|
587
607
|
args.track = parseValueFlag(argv, i, '--track');
|
|
588
608
|
i += 1;
|
|
@@ -625,14 +645,19 @@ function parseReleaseCycleArgs(argv) {
|
|
|
625
645
|
continue;
|
|
626
646
|
}
|
|
627
647
|
|
|
648
|
+
if (token === '--update-pr-description') {
|
|
649
|
+
args.updatePrDescription = true;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
|
|
628
653
|
if (token === '--check-timeout') {
|
|
629
|
-
args.checkTimeout = Number.
|
|
654
|
+
args.checkTimeout = Number.parseFloat(parseValueFlag(argv, i, '--check-timeout'));
|
|
630
655
|
i += 1;
|
|
631
656
|
continue;
|
|
632
657
|
}
|
|
633
658
|
|
|
634
659
|
if (token === '--release-pr-timeout') {
|
|
635
|
-
args.releasePrTimeout = Number.
|
|
660
|
+
args.releasePrTimeout = Number.parseFloat(parseValueFlag(argv, i, '--release-pr-timeout'));
|
|
636
661
|
i += 1;
|
|
637
662
|
continue;
|
|
638
663
|
}
|
|
@@ -658,6 +683,22 @@ function parseReleaseCycleArgs(argv) {
|
|
|
658
683
|
continue;
|
|
659
684
|
}
|
|
660
685
|
|
|
686
|
+
if (token === '--confirm-merges') {
|
|
687
|
+
args.confirmMerges = true;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (token === '--sync-base') {
|
|
692
|
+
args.syncBase = parseValueFlag(argv, i, '--sync-base');
|
|
693
|
+
i += 1;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (token === '--no-resume') {
|
|
698
|
+
args.resume = false;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
661
702
|
if (token === '--merge-when-green') {
|
|
662
703
|
args.mergeWhenGreen = true;
|
|
663
704
|
continue;
|
|
@@ -683,6 +724,11 @@ function parseReleaseCycleArgs(argv) {
|
|
|
683
724
|
continue;
|
|
684
725
|
}
|
|
685
726
|
|
|
727
|
+
if (token === '--confirm-cleanup') {
|
|
728
|
+
args.confirmCleanup = true;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
|
|
686
732
|
if (token === '--no-cleanup') {
|
|
687
733
|
args.noCleanup = true;
|
|
688
734
|
continue;
|
|
@@ -710,6 +756,10 @@ function parseReleaseCycleArgs(argv) {
|
|
|
710
756
|
throw new Error('Invalid --mode value. Expected auto, open-pr, or publish.');
|
|
711
757
|
}
|
|
712
758
|
|
|
759
|
+
if (!['code', 'full'].includes(args.phase)) {
|
|
760
|
+
throw new Error('Invalid --phase value. Expected code or full.');
|
|
761
|
+
}
|
|
762
|
+
|
|
713
763
|
if (!['auto', 'beta', 'stable'].includes(args.track)) {
|
|
714
764
|
throw new Error('Invalid --track value. Expected auto, beta, or stable.');
|
|
715
765
|
}
|
|
@@ -718,16 +768,20 @@ function parseReleaseCycleArgs(argv) {
|
|
|
718
768
|
throw new Error('Invalid --promote-type value. Expected patch, minor, or major.');
|
|
719
769
|
}
|
|
720
770
|
|
|
771
|
+
if (!['auto', 'rebase', 'merge', 'off'].includes(args.syncBase)) {
|
|
772
|
+
throw new Error('Invalid --sync-base value. Expected auto, rebase, merge, or off.');
|
|
773
|
+
}
|
|
774
|
+
|
|
721
775
|
if (!['squash', 'merge', 'rebase'].includes(args.mergeMethod)) {
|
|
722
776
|
throw new Error('Invalid --merge-method value. Expected squash, merge, or rebase.');
|
|
723
777
|
}
|
|
724
778
|
|
|
725
|
-
if (!Number.
|
|
726
|
-
throw new Error('Invalid --check-timeout value. Expected a positive
|
|
779
|
+
if (!Number.isFinite(args.checkTimeout) || args.checkTimeout <= 0) {
|
|
780
|
+
throw new Error('Invalid --check-timeout value. Expected a positive number (minutes).');
|
|
727
781
|
}
|
|
728
782
|
|
|
729
|
-
if (!Number.
|
|
730
|
-
throw new Error('Invalid --release-pr-timeout value. Expected a positive
|
|
783
|
+
if (!Number.isFinite(args.releasePrTimeout) || args.releasePrTimeout <= 0) {
|
|
784
|
+
throw new Error('Invalid --release-pr-timeout value. Expected a positive number (minutes).');
|
|
731
785
|
}
|
|
732
786
|
|
|
733
787
|
return args;
|
|
@@ -1281,6 +1335,29 @@ function sleepMs(milliseconds) {
|
|
|
1281
1335
|
Atomics.wait(view, 0, 0, milliseconds);
|
|
1282
1336
|
}
|
|
1283
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
|
+
|
|
1284
1361
|
function resolveGitContext(args, deps) {
|
|
1285
1362
|
const insideWorkTree = deps.exec('git', ['rev-parse', '--is-inside-work-tree']);
|
|
1286
1363
|
if (insideWorkTree.status !== 0 || insideWorkTree.stdout.trim() !== 'true') {
|
|
@@ -1454,6 +1531,14 @@ function ensureBranchPushed(repo, head, deps) {
|
|
|
1454
1531
|
|
|
1455
1532
|
function createOrUpdatePr(context, body, args, deps) {
|
|
1456
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
|
+
|
|
1457
1542
|
const bodyFilePath = path.join(process.cwd(), `.tmp-pr-body-${Date.now()}.md`);
|
|
1458
1543
|
fs.writeFileSync(bodyFilePath, body);
|
|
1459
1544
|
|
|
@@ -1540,8 +1625,8 @@ function getPrCheckState(repo, prNumber, deps) {
|
|
|
1540
1625
|
}
|
|
1541
1626
|
|
|
1542
1627
|
function watchPrChecks(repo, prNumber, timeoutMinutes, deps) {
|
|
1543
|
-
const timeoutAt =
|
|
1544
|
-
while (
|
|
1628
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1629
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1545
1630
|
const state = getPrCheckState(repo, prNumber, deps);
|
|
1546
1631
|
if (state.failed > 0) {
|
|
1547
1632
|
throw new Error(`PR #${prNumber} has failing required checks.`);
|
|
@@ -1551,7 +1636,7 @@ function watchPrChecks(repo, prNumber, timeoutMinutes, deps) {
|
|
|
1551
1636
|
return 'green';
|
|
1552
1637
|
}
|
|
1553
1638
|
|
|
1554
|
-
|
|
1639
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1555
1640
|
}
|
|
1556
1641
|
|
|
1557
1642
|
throw new Error(`Timed out waiting for checks on PR #${prNumber} after ${timeoutMinutes} minutes.`);
|
|
@@ -1569,6 +1654,237 @@ function mergePrWhenGreen(repo, prNumber, mergeMethod, deps) {
|
|
|
1569
1654
|
}
|
|
1570
1655
|
}
|
|
1571
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
|
+
|
|
1572
1888
|
function findReleasePrs(repo, deps) {
|
|
1573
1889
|
const prs = listOpenPullRequests(repo, deps);
|
|
1574
1890
|
return prs.filter(
|
|
@@ -1579,8 +1895,8 @@ function findReleasePrs(repo, deps) {
|
|
|
1579
1895
|
}
|
|
1580
1896
|
|
|
1581
1897
|
function waitForReleasePr(repo, timeoutMinutes, deps) {
|
|
1582
|
-
const timeoutAt =
|
|
1583
|
-
while (
|
|
1898
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1899
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1584
1900
|
const releasePrs = findReleasePrs(repo, deps);
|
|
1585
1901
|
if (releasePrs.length === 1) {
|
|
1586
1902
|
return releasePrs[0];
|
|
@@ -1589,7 +1905,7 @@ function waitForReleasePr(repo, timeoutMinutes, deps) {
|
|
|
1589
1905
|
throw new Error(`Multiple release PRs detected: ${releasePrs.map((item) => item.url).join(', ')}`);
|
|
1590
1906
|
}
|
|
1591
1907
|
|
|
1592
|
-
|
|
1908
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1593
1909
|
}
|
|
1594
1910
|
|
|
1595
1911
|
throw new Error(`Timed out waiting for release PR after ${timeoutMinutes} minutes.`);
|
|
@@ -1640,8 +1956,8 @@ function findPromotionPrs(repo, deps) {
|
|
|
1640
1956
|
}
|
|
1641
1957
|
|
|
1642
1958
|
function waitForPromotionPr(repo, timeoutMinutes, deps) {
|
|
1643
|
-
const timeoutAt =
|
|
1644
|
-
while (
|
|
1959
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1960
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1645
1961
|
const promotionPrs = findPromotionPrs(repo, deps);
|
|
1646
1962
|
if (promotionPrs.length === 1) {
|
|
1647
1963
|
return promotionPrs[0];
|
|
@@ -1652,7 +1968,7 @@ function waitForPromotionPr(repo, timeoutMinutes, deps) {
|
|
|
1652
1968
|
return promotionPrs[0];
|
|
1653
1969
|
}
|
|
1654
1970
|
|
|
1655
|
-
|
|
1971
|
+
waitForNextPoll(timeoutAt, 5000, deps);
|
|
1656
1972
|
}
|
|
1657
1973
|
|
|
1658
1974
|
throw new Error(`Timed out waiting for promotion PR after ${timeoutMinutes} minutes.`);
|
|
@@ -1678,11 +1994,11 @@ function getRemotePackageVersion(repo, ref, deps) {
|
|
|
1678
1994
|
}
|
|
1679
1995
|
|
|
1680
1996
|
function validateNpmPublishedVersionAndTag(packageName, expectedVersion, expectedTag, timeoutMinutes, deps) {
|
|
1681
|
-
const timeoutAt =
|
|
1997
|
+
const timeoutAt = nowMs(deps) + timeoutMinutes * 60 * 1000;
|
|
1682
1998
|
let lastObservedVersion = '';
|
|
1683
1999
|
let lastObservedTagVersion = '';
|
|
1684
2000
|
|
|
1685
|
-
while (
|
|
2001
|
+
while (nowMs(deps) <= timeoutAt) {
|
|
1686
2002
|
const versionResult = deps.exec('npm', ['view', packageName, 'version', '--json']);
|
|
1687
2003
|
const tagsResult = deps.exec('npm', ['view', packageName, 'dist-tags', '--json']);
|
|
1688
2004
|
if (versionResult.status === 0 && tagsResult.status === 0) {
|
|
@@ -1701,7 +2017,7 @@ function validateNpmPublishedVersionAndTag(packageName, expectedVersion, expecte
|
|
|
1701
2017
|
}
|
|
1702
2018
|
}
|
|
1703
2019
|
|
|
1704
|
-
|
|
2020
|
+
waitForNextPoll(timeoutAt, 10000, deps);
|
|
1705
2021
|
}
|
|
1706
2022
|
|
|
1707
2023
|
return {
|
|
@@ -1739,6 +2055,103 @@ function isCleanupCandidateBranch(branchName) {
|
|
|
1739
2055
|
return /^(feat|fix|chore|refactor|test)\//.test(branchName);
|
|
1740
2056
|
}
|
|
1741
2057
|
|
|
2058
|
+
function syncBranchWithBase({
|
|
2059
|
+
deps,
|
|
2060
|
+
headBranch,
|
|
2061
|
+
baseBranch,
|
|
2062
|
+
strategy,
|
|
2063
|
+
reporter,
|
|
2064
|
+
summary,
|
|
2065
|
+
dryRun
|
|
2066
|
+
}) {
|
|
2067
|
+
if (strategy === 'off') {
|
|
2068
|
+
summary.actionsSkipped.push('sync base branch');
|
|
2069
|
+
return {
|
|
2070
|
+
synchronized: false,
|
|
2071
|
+
wasBehind: false
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
reporter.start('release-cycle-sync-fetch', `Fetching origin/${baseBranch}...`);
|
|
2076
|
+
const fetch = deps.exec('git', ['fetch', 'origin', baseBranch]);
|
|
2077
|
+
if (fetch.status !== 0) {
|
|
2078
|
+
throw new Error(`Failed to fetch origin/${baseBranch}: ${(fetch.stderr || fetch.stdout || '').trim()}`);
|
|
2079
|
+
}
|
|
2080
|
+
reporter.ok('release-cycle-sync-fetch', `Fetched origin/${baseBranch}.`);
|
|
2081
|
+
|
|
2082
|
+
const behindCheck = deps.exec('git', ['rev-list', '--left-right', '--count', `${headBranch}...origin/${baseBranch}`]);
|
|
2083
|
+
if (behindCheck.status !== 0) {
|
|
2084
|
+
throw new Error(`Failed to compare ${headBranch} against origin/${baseBranch}.`);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const parts = (behindCheck.stdout || '').trim().split(/\s+/);
|
|
2088
|
+
const behindCount = Number.parseInt(parts[1] || '0', 10);
|
|
2089
|
+
const isBehind = Number.isInteger(behindCount) && behindCount > 0;
|
|
2090
|
+
if (!isBehind) {
|
|
2091
|
+
summary.actionsPerformed.push(`sync base: ${headBranch} already up to date with origin/${baseBranch}`);
|
|
2092
|
+
return {
|
|
2093
|
+
synchronized: true,
|
|
2094
|
+
wasBehind: false
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const effectiveStrategy = strategy === 'auto' ? 'rebase' : strategy;
|
|
2099
|
+
if (dryRun) {
|
|
2100
|
+
summary.actionsPerformed.push(`dry-run: would ${effectiveStrategy} ${headBranch} onto origin/${baseBranch}`);
|
|
2101
|
+
return {
|
|
2102
|
+
synchronized: false,
|
|
2103
|
+
wasBehind: true
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
if (effectiveStrategy === 'rebase') {
|
|
2108
|
+
reporter.start('release-cycle-sync-rebase', `Rebasing ${headBranch} onto origin/${baseBranch}...`);
|
|
2109
|
+
const rebase = deps.exec('git', ['rebase', `origin/${baseBranch}`]);
|
|
2110
|
+
if (rebase.status !== 0) {
|
|
2111
|
+
throw new Error(
|
|
2112
|
+
[
|
|
2113
|
+
`Rebase failed while syncing ${headBranch} with origin/${baseBranch}.`,
|
|
2114
|
+
'Resolve conflicts, then run `git rebase --continue` or `git rebase --abort`.',
|
|
2115
|
+
(rebase.stderr || rebase.stdout || '').trim()
|
|
2116
|
+
].filter(Boolean).join('\n')
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
reporter.ok('release-cycle-sync-rebase', `${headBranch} rebased onto origin/${baseBranch}.`);
|
|
2120
|
+
summary.actionsPerformed.push(`sync base: rebased ${headBranch} onto origin/${baseBranch}`);
|
|
2121
|
+
return {
|
|
2122
|
+
synchronized: true,
|
|
2123
|
+
wasBehind: true
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
reporter.start('release-cycle-sync-merge', `Merging origin/${baseBranch} into ${headBranch}...`);
|
|
2128
|
+
const merge = deps.exec('git', ['merge', '--no-edit', `origin/${baseBranch}`]);
|
|
2129
|
+
if (merge.status !== 0) {
|
|
2130
|
+
throw new Error(
|
|
2131
|
+
[
|
|
2132
|
+
`Merge failed while syncing ${headBranch} with origin/${baseBranch}.`,
|
|
2133
|
+
'Resolve conflicts and commit merge before rerunning.',
|
|
2134
|
+
(merge.stderr || merge.stdout || '').trim()
|
|
2135
|
+
].filter(Boolean).join('\n')
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
reporter.ok('release-cycle-sync-merge', `Merged origin/${baseBranch} into ${headBranch}.`);
|
|
2139
|
+
summary.actionsPerformed.push(`sync base: merged origin/${baseBranch} into ${headBranch}`);
|
|
2140
|
+
return {
|
|
2141
|
+
synchronized: true,
|
|
2142
|
+
wasBehind: true
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function isHeadIntegratedIntoBase(headRef, baseBranch, deps) {
|
|
2147
|
+
const fetch = deps.exec('git', ['fetch', 'origin', baseBranch]);
|
|
2148
|
+
if (fetch.status !== 0) {
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
const ancestor = deps.exec('git', ['merge-base', '--is-ancestor', headRef, `origin/${baseBranch}`]);
|
|
2152
|
+
return ancestor.status === 0;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1742
2155
|
function runLocalCleanup({
|
|
1743
2156
|
deps,
|
|
1744
2157
|
originalBranch,
|
|
@@ -2406,6 +2819,9 @@ async function runOpenPrFlow(args, dependencies = {}) {
|
|
|
2406
2819
|
summary.prAction = `${prResult.action} (#${prResult.number})`;
|
|
2407
2820
|
summary.prUrl = prResult.url || 'n/a';
|
|
2408
2821
|
summary.actionsPerformed.push(`pr ${prResult.action}: #${prResult.number}`);
|
|
2822
|
+
if (prResult.action === 'reused') {
|
|
2823
|
+
summary.warnings.push('Existing PR reused without body/title changes. Use --update-pr-description to refresh PR content.');
|
|
2824
|
+
}
|
|
2409
2825
|
|
|
2410
2826
|
if (args.autoMerge) {
|
|
2411
2827
|
reporter.start('open-pr-auto-merge', `Enabling auto-merge for PR #${prResult.number}...`);
|
|
@@ -2452,6 +2868,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2452
2868
|
const summary = createOrchestrationSummary();
|
|
2453
2869
|
const reporter = new StepReporter();
|
|
2454
2870
|
const originalBranch = deps.exec('git', ['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
|
2871
|
+
const useAutoMerge = args.autoMerge;
|
|
2455
2872
|
|
|
2456
2873
|
reporter.start('release-cycle-preflight-gh', 'Validating GitHub CLI and authentication...');
|
|
2457
2874
|
ensureGhAvailable(deps);
|
|
@@ -2459,6 +2876,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2459
2876
|
|
|
2460
2877
|
const gitContext = resolveGitContext(args, deps);
|
|
2461
2878
|
summary.repoResolved = gitContext.repo;
|
|
2879
|
+
const effectivePhase = args.phase;
|
|
2462
2880
|
const requestedTrack = args.track === 'auto' ? (args.promoteStable ? 'stable' : 'beta') : args.track;
|
|
2463
2881
|
if (args.promoteStable && gitContext.head !== DEFAULT_BETA_BRANCH) {
|
|
2464
2882
|
throw new Error(`--promote-stable is only allowed when running from "${DEFAULT_BETA_BRANCH}".`);
|
|
@@ -2471,7 +2889,6 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2471
2889
|
}
|
|
2472
2890
|
summary.actionsPerformed.push(`release track: ${requestedTrack}`);
|
|
2473
2891
|
summary.releaseTrack = requestedTrack;
|
|
2474
|
-
|
|
2475
2892
|
let detectedMode = args.mode;
|
|
2476
2893
|
if (args.promoteStable) {
|
|
2477
2894
|
detectedMode = 'open-pr';
|
|
@@ -2479,6 +2896,8 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2479
2896
|
const releasePrs = findReleasePrs(gitContext.repo, deps);
|
|
2480
2897
|
if (gitContext.head.startsWith('changeset-release/')) {
|
|
2481
2898
|
detectedMode = 'publish';
|
|
2899
|
+
} else if (gitContext.head !== DEFAULT_BETA_BRANCH) {
|
|
2900
|
+
detectedMode = 'open-pr';
|
|
2482
2901
|
} else if (releasePrs.length === 1) {
|
|
2483
2902
|
detectedMode = 'publish';
|
|
2484
2903
|
} else if (releasePrs.length > 1) {
|
|
@@ -2551,40 +2970,95 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2551
2970
|
summary.promotionPr = 'skipped';
|
|
2552
2971
|
}
|
|
2553
2972
|
|
|
2554
|
-
const
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
summary.prAction = openPrResult.summary.prAction;
|
|
2571
|
-
summary.prUrl = openPrResult.summary.prUrl;
|
|
2572
|
-
summary.branchPushed = openPrResult.summary.branchPushed;
|
|
2573
|
-
summary.autoMerge = openPrResult.summary.autoMerge;
|
|
2574
|
-
summary.checks = openPrResult.summary.checks;
|
|
2575
|
-
summary.actionsPerformed.push(...openPrResult.summary.actionsPerformed);
|
|
2576
|
-
summary.actionsSkipped.push(...openPrResult.summary.actionsSkipped);
|
|
2577
|
-
summary.warnings.push(...openPrResult.summary.warnings);
|
|
2578
|
-
|
|
2579
|
-
if (args.mergeWhenGreen && codePr && !args.dryRun) {
|
|
2580
|
-
reporter.start('release-cycle-merge-code-pr', `Merging code PR #${codePr.number}...`);
|
|
2581
|
-
mergePrWhenGreen(gitContext.repo, codePr.number, args.mergeMethod, deps);
|
|
2582
|
-
reporter.ok('release-cycle-merge-code-pr', `Code PR #${codePr.number} merged.`);
|
|
2583
|
-
summary.merge = `code pr merged (#${codePr.number})`;
|
|
2584
|
-
summary.actionsPerformed.push(`code pr merged: #${codePr.number}`);
|
|
2973
|
+
const canResumeFromMergedCode = args.resume
|
|
2974
|
+
&& !args.promoteStable
|
|
2975
|
+
&& gitContext.head !== DEFAULT_BETA_BRANCH
|
|
2976
|
+
&& !gitContext.head.startsWith('changeset-release/')
|
|
2977
|
+
&& isHeadIntegratedIntoBase('HEAD', DEFAULT_BETA_BRANCH, deps);
|
|
2978
|
+
|
|
2979
|
+
let codePr = null;
|
|
2980
|
+
if (canResumeFromMergedCode) {
|
|
2981
|
+
summary.prAction = 'skipped (resume: code already merged)';
|
|
2982
|
+
summary.prUrl = 'n/a';
|
|
2983
|
+
summary.branchPushed = 'skipped (resume)';
|
|
2984
|
+
summary.autoMerge = 'skipped (resume)';
|
|
2985
|
+
summary.checks = 'skipped (resume)';
|
|
2986
|
+
summary.merge = 'skipped (resume: already merged)';
|
|
2987
|
+
summary.actionsPerformed.push(`resume detected: ${gitContext.head} already integrated into ${DEFAULT_BETA_BRANCH}`);
|
|
2988
|
+
summary.actionsSkipped.push('open/update code pr (resume)');
|
|
2585
2989
|
} else {
|
|
2586
|
-
|
|
2587
|
-
|
|
2990
|
+
if (!args.promoteStable && gitContext.head !== DEFAULT_BETA_BRANCH && !gitContext.head.startsWith('changeset-release/')) {
|
|
2991
|
+
syncBranchWithBase({
|
|
2992
|
+
deps,
|
|
2993
|
+
headBranch: gitContext.head,
|
|
2994
|
+
baseBranch: DEFAULT_BETA_BRANCH,
|
|
2995
|
+
strategy: args.syncBase,
|
|
2996
|
+
reporter,
|
|
2997
|
+
summary,
|
|
2998
|
+
dryRun: args.dryRun
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
const openPrResult = await runOpenPrFlow(
|
|
3003
|
+
{
|
|
3004
|
+
...args,
|
|
3005
|
+
head: args.promoteStable ? DEFAULT_BETA_BRANCH : args.head,
|
|
3006
|
+
base: args.promoteStable ? DEFAULT_BASE_BRANCH : args.base,
|
|
3007
|
+
autoMerge: useAutoMerge,
|
|
3008
|
+
watchChecks: args.watchChecks,
|
|
3009
|
+
checkTimeout: args.checkTimeout,
|
|
3010
|
+
mergeMethod: args.mergeMethod,
|
|
3011
|
+
updateExistingPr: args.updatePrDescription,
|
|
3012
|
+
skipPush: args.promoteStable,
|
|
3013
|
+
printSummary: false
|
|
3014
|
+
},
|
|
3015
|
+
dependencies
|
|
3016
|
+
);
|
|
3017
|
+
|
|
3018
|
+
codePr = openPrResult.pr;
|
|
3019
|
+
summary.prAction = openPrResult.summary.prAction;
|
|
3020
|
+
summary.prUrl = openPrResult.summary.prUrl;
|
|
3021
|
+
summary.branchPushed = openPrResult.summary.branchPushed;
|
|
3022
|
+
summary.autoMerge = openPrResult.summary.autoMerge;
|
|
3023
|
+
summary.checks = openPrResult.summary.checks;
|
|
3024
|
+
summary.actionsPerformed.push(...openPrResult.summary.actionsPerformed);
|
|
3025
|
+
summary.actionsSkipped.push(...openPrResult.summary.actionsSkipped);
|
|
3026
|
+
summary.warnings.push(...openPrResult.summary.warnings);
|
|
3027
|
+
|
|
3028
|
+
if (args.mergeWhenGreen && codePr && !args.dryRun) {
|
|
3029
|
+
reporter.start('release-cycle-merge-code-ready', `Checking merge readiness for code PR #${codePr.number}...`);
|
|
3030
|
+
const codeReadiness = waitForPrMergeReadinessOrThrow(
|
|
3031
|
+
gitContext.repo,
|
|
3032
|
+
codePr.number,
|
|
3033
|
+
`Code PR #${codePr.number}`,
|
|
3034
|
+
args.checkTimeout,
|
|
3035
|
+
deps
|
|
3036
|
+
);
|
|
3037
|
+
await confirmMergeIfNeeded(args, codeReadiness, `Code PR #${codePr.number}`);
|
|
3038
|
+
reporter.ok('release-cycle-merge-code-ready', `Code PR #${codePr.number} is ready for merge.`);
|
|
3039
|
+
reporter.start('release-cycle-code-auto-merge', `Enabling auto-merge for code PR #${codePr.number}...`);
|
|
3040
|
+
enablePrAutoMerge(gitContext.repo, codePr.number, args.mergeMethod, deps);
|
|
3041
|
+
reporter.ok('release-cycle-code-auto-merge', `Auto-merge enabled for code PR #${codePr.number}.`);
|
|
3042
|
+
reporter.start('release-cycle-wait-code-merge', `Waiting for code PR #${codePr.number} merge...`);
|
|
3043
|
+
waitForPrMerged(gitContext.repo, codePr.number, args.releasePrTimeout, deps);
|
|
3044
|
+
reporter.ok('release-cycle-wait-code-merge', `Code PR #${codePr.number} merged.`);
|
|
3045
|
+
summary.actionsPerformed.push(`code pr merged: #${codePr.number}`);
|
|
3046
|
+
summary.merge = `code pr merged (#${codePr.number})`;
|
|
3047
|
+
} else {
|
|
3048
|
+
summary.merge = args.dryRun ? 'dry-run: would merge code PR' : 'skipped';
|
|
3049
|
+
summary.actionsSkipped.push('merge code pr');
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
if (effectivePhase === 'code') {
|
|
3054
|
+
summary.releasePr = 'skipped (phase=code)';
|
|
3055
|
+
summary.npmValidation = 'skipped (phase=code)';
|
|
3056
|
+
summary.cleanup = 'skipped (phase=code; requires npm validation)';
|
|
3057
|
+
summary.actionsSkipped.push('wait release pr (phase=code)');
|
|
3058
|
+
summary.actionsSkipped.push('verify npm (phase=code)');
|
|
3059
|
+
summary.actionsSkipped.push('cleanup (phase=code)');
|
|
3060
|
+
printOrchestrationSummary(`release-cycle completed in ${detectedMode} mode`, summary);
|
|
3061
|
+
return;
|
|
2588
3062
|
}
|
|
2589
3063
|
|
|
2590
3064
|
let mergedReleasePr = null;
|
|
@@ -2605,11 +3079,26 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2605
3079
|
}
|
|
2606
3080
|
|
|
2607
3081
|
if (args.mergeReleasePr) {
|
|
2608
|
-
reporter.start('release-cycle-merge-release-
|
|
2609
|
-
|
|
2610
|
-
|
|
3082
|
+
reporter.start('release-cycle-merge-release-ready', `Checking merge readiness for release PR #${releasePr.number}...`);
|
|
3083
|
+
const releaseReadiness = waitForPrMergeReadinessOrThrow(
|
|
3084
|
+
gitContext.repo,
|
|
3085
|
+
releasePr.number,
|
|
3086
|
+
`Release PR #${releasePr.number}`,
|
|
3087
|
+
args.checkTimeout,
|
|
3088
|
+
deps,
|
|
3089
|
+
{ allowBehindTransient: true }
|
|
3090
|
+
);
|
|
3091
|
+
await confirmMergeIfNeeded(args, releaseReadiness, `Release PR #${releasePr.number}`);
|
|
3092
|
+
reporter.ok('release-cycle-merge-release-ready', `Release PR #${releasePr.number} is ready for merge.`);
|
|
3093
|
+
reporter.start('release-cycle-release-auto-merge', `Enabling auto-merge for release PR #${releasePr.number}...`);
|
|
3094
|
+
enablePrAutoMerge(gitContext.repo, releasePr.number, args.mergeMethod, deps);
|
|
3095
|
+
reporter.ok('release-cycle-release-auto-merge', `Auto-merge enabled for release PR #${releasePr.number}.`);
|
|
3096
|
+
reporter.start('release-cycle-release-wait-merge', `Waiting for release PR #${releasePr.number} merge...`);
|
|
3097
|
+
waitForPrMerged(gitContext.repo, releasePr.number, args.releasePrTimeout, deps);
|
|
3098
|
+
reporter.ok('release-cycle-release-wait-merge', `Release PR #${releasePr.number} merged.`);
|
|
2611
3099
|
summary.releasePr = `merged (#${releasePr.number})`;
|
|
2612
3100
|
summary.actionsPerformed.push(`release pr merged: #${releasePr.number}`);
|
|
3101
|
+
summary.autoMerge = 'enabled (code + release)';
|
|
2613
3102
|
mergedReleasePr = releasePr;
|
|
2614
3103
|
} else {
|
|
2615
3104
|
summary.actionsSkipped.push('merge release pr');
|
|
@@ -2620,6 +3109,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2620
3109
|
summary.actionsSkipped.push('wait release pr');
|
|
2621
3110
|
}
|
|
2622
3111
|
|
|
3112
|
+
let npmValidationPassed = false;
|
|
2623
3113
|
if (args.verifyNpm && !args.dryRun && mergedReleasePr) {
|
|
2624
3114
|
reporter.start('release-cycle-verify-npm', 'Validating npm publish and dist-tag...');
|
|
2625
3115
|
const targetRef = args.promoteStable ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
|
|
@@ -2646,6 +3136,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2646
3136
|
reporter.ok('release-cycle-verify-npm', `${remotePackage.name}@${remotePackage.version} validated on tag ${expectedTag}.`);
|
|
2647
3137
|
summary.actionsPerformed.push(`npm validation: ${remotePackage.name}@${remotePackage.version} (${expectedTag})`);
|
|
2648
3138
|
summary.npmValidation = `pass (${expectedTag} -> ${remotePackage.version})`;
|
|
3139
|
+
npmValidationPassed = true;
|
|
2649
3140
|
} else if (!args.verifyNpm) {
|
|
2650
3141
|
summary.actionsSkipped.push('verify npm');
|
|
2651
3142
|
summary.npmValidation = 'skipped';
|
|
@@ -2655,7 +3146,10 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2655
3146
|
summary.npmValidation = 'skipped (release pr not merged)';
|
|
2656
3147
|
}
|
|
2657
3148
|
|
|
2658
|
-
if (!args.dryRun) {
|
|
3149
|
+
if (!args.dryRun && npmValidationPassed) {
|
|
3150
|
+
if (args.confirmCleanup && !args.yes) {
|
|
3151
|
+
await confirmOrThrow('Release completed and npm validation passed.\nProceed with local cleanup now?');
|
|
3152
|
+
}
|
|
2659
3153
|
runLocalCleanup({
|
|
2660
3154
|
deps,
|
|
2661
3155
|
originalBranch,
|
|
@@ -2664,6 +3158,9 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2664
3158
|
summary,
|
|
2665
3159
|
reporter
|
|
2666
3160
|
});
|
|
3161
|
+
} else if (!args.dryRun && !npmValidationPassed) {
|
|
3162
|
+
summary.actionsSkipped.push('cleanup (npm validation did not pass)');
|
|
3163
|
+
summary.cleanup = 'skipped (requires npm validation pass)';
|
|
2667
3164
|
} else {
|
|
2668
3165
|
summary.actionsSkipped.push('cleanup (dry-run)');
|
|
2669
3166
|
summary.cleanup = 'skipped (dry-run)';
|
|
@@ -2711,11 +3208,26 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2711
3208
|
summary.merge = `dry-run: would merge release PR #${releasePr.number}`;
|
|
2712
3209
|
summary.releasePr = `dry-run: would merge (#${releasePr.number})`;
|
|
2713
3210
|
} else {
|
|
2714
|
-
reporter.start('release-cycle-publish-merge', `
|
|
2715
|
-
|
|
2716
|
-
|
|
3211
|
+
reporter.start('release-cycle-publish-merge-ready', `Checking merge readiness for release PR #${releasePr.number}...`);
|
|
3212
|
+
const publishReadiness = waitForPrMergeReadinessOrThrow(
|
|
3213
|
+
gitContext.repo,
|
|
3214
|
+
releasePr.number,
|
|
3215
|
+
`Release PR #${releasePr.number}`,
|
|
3216
|
+
args.checkTimeout,
|
|
3217
|
+
deps,
|
|
3218
|
+
{ allowBehindTransient: true }
|
|
3219
|
+
);
|
|
3220
|
+
await confirmMergeIfNeeded(args, publishReadiness, `Release PR #${releasePr.number}`);
|
|
3221
|
+
reporter.ok('release-cycle-publish-merge-ready', `Release PR #${releasePr.number} is ready for merge.`);
|
|
3222
|
+
reporter.start('release-cycle-publish-auto-merge', `Enabling auto-merge for release PR #${releasePr.number}...`);
|
|
3223
|
+
enablePrAutoMerge(gitContext.repo, releasePr.number, args.mergeMethod, deps);
|
|
3224
|
+
reporter.ok('release-cycle-publish-auto-merge', `Auto-merge enabled for release PR #${releasePr.number}.`);
|
|
3225
|
+
reporter.start('release-cycle-publish-wait-merge', `Waiting for release PR #${releasePr.number} merge...`);
|
|
3226
|
+
waitForPrMerged(gitContext.repo, releasePr.number, args.releasePrTimeout, deps);
|
|
3227
|
+
reporter.ok('release-cycle-publish-wait-merge', `Release PR #${releasePr.number} merged.`);
|
|
2717
3228
|
summary.merge = `merged release pr (#${releasePr.number})`;
|
|
2718
3229
|
summary.releasePr = `merged (#${releasePr.number})`;
|
|
3230
|
+
summary.autoMerge = 'enabled (release)';
|
|
2719
3231
|
summary.actionsPerformed.push(`release pr merged: #${releasePr.number}`);
|
|
2720
3232
|
}
|
|
2721
3233
|
} else {
|
|
@@ -2724,6 +3236,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2724
3236
|
summary.actionsSkipped.push('merge release pr');
|
|
2725
3237
|
}
|
|
2726
3238
|
|
|
3239
|
+
let npmValidationPassed = false;
|
|
2727
3240
|
if (args.verifyNpm && !args.dryRun && (args.mergeReleasePr || args.mergeWhenGreen)) {
|
|
2728
3241
|
reporter.start('release-cycle-verify-npm', 'Validating npm publish and dist-tag...');
|
|
2729
3242
|
const targetRef = requestedTrack === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
|
|
@@ -2750,6 +3263,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2750
3263
|
reporter.ok('release-cycle-verify-npm', `${remotePackage.name}@${remotePackage.version} validated on tag ${expectedTag}.`);
|
|
2751
3264
|
summary.actionsPerformed.push(`npm validation: ${remotePackage.name}@${remotePackage.version} (${expectedTag})`);
|
|
2752
3265
|
summary.npmValidation = `pass (${expectedTag} -> ${remotePackage.version})`;
|
|
3266
|
+
npmValidationPassed = true;
|
|
2753
3267
|
} else if (!args.verifyNpm) {
|
|
2754
3268
|
summary.npmValidation = 'skipped';
|
|
2755
3269
|
} else if (args.dryRun) {
|
|
@@ -2758,7 +3272,10 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2758
3272
|
summary.npmValidation = 'skipped (release pr not merged)';
|
|
2759
3273
|
}
|
|
2760
3274
|
|
|
2761
|
-
if (!args.dryRun) {
|
|
3275
|
+
if (!args.dryRun && npmValidationPassed) {
|
|
3276
|
+
if (args.confirmCleanup && !args.yes) {
|
|
3277
|
+
await confirmOrThrow('Release completed and npm validation passed.\nProceed with local cleanup now?');
|
|
3278
|
+
}
|
|
2762
3279
|
runLocalCleanup({
|
|
2763
3280
|
deps,
|
|
2764
3281
|
originalBranch,
|
|
@@ -2767,6 +3284,9 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2767
3284
|
summary,
|
|
2768
3285
|
reporter
|
|
2769
3286
|
});
|
|
3287
|
+
} else if (!args.dryRun && !npmValidationPassed) {
|
|
3288
|
+
summary.actionsSkipped.push('cleanup (npm validation did not pass)');
|
|
3289
|
+
summary.cleanup = 'skipped (requires npm validation pass)';
|
|
2770
3290
|
} else {
|
|
2771
3291
|
summary.cleanup = 'skipped (dry-run)';
|
|
2772
3292
|
}
|
package/package.json
CHANGED