@i-santos/create-package-starter 1.5.0-beta.7 → 1.5.0-beta.8
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 +20 -0
- package/lib/run.js +445 -10
- package/package.json +1 -1
- package/template/.github/workflows/promote-stable.yml +103 -0
package/README.md
CHANGED
|
@@ -86,6 +86,10 @@ Orchestrate release cycle:
|
|
|
86
86
|
- `release-cycle`
|
|
87
87
|
- `--repo <owner/repo>` (optional; inferred from `remote.origin.url` when omitted)
|
|
88
88
|
- `--mode <auto|open-pr|publish>` (default: `auto`)
|
|
89
|
+
- `--track <auto|beta|stable>` (default: `auto`)
|
|
90
|
+
- `--promote-stable` (explicitly trigger stable promotion path; only valid from `release/beta`)
|
|
91
|
+
- `--promote-type <patch|minor|major>` (default: `patch`)
|
|
92
|
+
- `--promote-summary <text>`
|
|
89
93
|
- `--head <branch>`
|
|
90
94
|
- `--base <branch>`
|
|
91
95
|
- `--title <text>`
|
|
@@ -99,6 +103,8 @@ Orchestrate release cycle:
|
|
|
99
103
|
- `--wait-release-pr` (default behavior: enabled)
|
|
100
104
|
- `--release-pr-timeout <minutes>` (default: `30`)
|
|
101
105
|
- `--merge-release-pr` (default behavior: enabled)
|
|
106
|
+
- `--verify-npm` (default behavior: enabled)
|
|
107
|
+
- `--no-cleanup` (disable default local cleanup after successful cycle)
|
|
102
108
|
- `--yes`
|
|
103
109
|
- `--dry-run`
|
|
104
110
|
|
|
@@ -127,6 +133,7 @@ The generated and managed baseline includes:
|
|
|
127
133
|
- `.changeset/README.md`
|
|
128
134
|
- `.github/workflows/ci.yml`
|
|
129
135
|
- `.github/workflows/release.yml`
|
|
136
|
+
- `.github/workflows/promote-stable.yml`
|
|
130
137
|
- `.github/PULL_REQUEST_TEMPLATE.md`
|
|
131
138
|
- `.github/CODEOWNERS`
|
|
132
139
|
- `CONTRIBUTING.md`
|
|
@@ -222,6 +229,8 @@ For `open-pr` mode:
|
|
|
222
229
|
- can merge code PR when green
|
|
223
230
|
- can wait for release PR creation (`changeset-release/*`)
|
|
224
231
|
- can watch checks and merge release PR when green
|
|
232
|
+
- validates npm publish (package + version + expected dist-tag)
|
|
233
|
+
- cleans local branch by default when safety gates are satisfied (`--no-cleanup` to disable)
|
|
225
234
|
|
|
226
235
|
For `publish` mode:
|
|
227
236
|
- resolves release PR directly
|
|
@@ -232,6 +241,17 @@ The command is policy-aware:
|
|
|
232
241
|
- never bypasses required checks/reviews/rulesets
|
|
233
242
|
- fails fast with actionable diagnostics when blocked
|
|
234
243
|
|
|
244
|
+
### Protected `release/beta` stable promotion
|
|
245
|
+
|
|
246
|
+
When `release/beta` is protected (PR-only), stable promotion in `release-cycle --promote-stable` uses a hybrid flow:
|
|
247
|
+
|
|
248
|
+
1. dispatch `.github/workflows/promote-stable.yml`
|
|
249
|
+
2. workflow creates `promote/stable-*` branch
|
|
250
|
+
3. workflow opens PR `promote/stable-* -> release/beta` and enables auto-merge
|
|
251
|
+
4. after merge, cycle continues with `release/beta -> main` and release PR progression
|
|
252
|
+
|
|
253
|
+
No direct push to `release/beta` is used in this path.
|
|
254
|
+
|
|
235
255
|
`release-auth` modes:
|
|
236
256
|
- `pat` (recommended default): uses `CHANGESETS_GH_TOKEN` fallback to `GITHUB_TOKEN`
|
|
237
257
|
- `app`: generates token via GitHub App (`GH_APP_ID` or `GH_APP_CLIENT_ID`, plus `GH_APP_PRIVATE_KEY`)
|
package/lib/run.js
CHANGED
|
@@ -7,6 +7,7 @@ const CHANGESETS_DEP = '@changesets/cli';
|
|
|
7
7
|
const CHANGESETS_DEP_VERSION = '^2.29.7';
|
|
8
8
|
const DEFAULT_BASE_BRANCH = 'main';
|
|
9
9
|
const DEFAULT_BETA_BRANCH = 'release/beta';
|
|
10
|
+
const DEFAULT_PROMOTE_WORKFLOW = 'promote-stable.yml';
|
|
10
11
|
const DEFAULT_RULESET_NAME = 'Default main branch protection';
|
|
11
12
|
const REQUIRED_CHECK_CONTEXT = 'required-check';
|
|
12
13
|
const DEFAULT_RELEASE_AUTH = 'pat';
|
|
@@ -26,6 +27,7 @@ const MANAGED_FILE_SPECS = [
|
|
|
26
27
|
['.changeset/README.md', '.changeset/README.md'],
|
|
27
28
|
['.github/workflows/ci.yml', '.github/workflows/ci.yml'],
|
|
28
29
|
['.github/workflows/release.yml', '.github/workflows/release.yml'],
|
|
30
|
+
['.github/workflows/promote-stable.yml', '.github/workflows/promote-stable.yml'],
|
|
29
31
|
['.github/workflows/auto-retarget-pr.yml', '.github/workflows/auto-retarget-pr.yml'],
|
|
30
32
|
['.github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE.md'],
|
|
31
33
|
['.github/CODEOWNERS', '.github/CODEOWNERS'],
|
|
@@ -43,7 +45,7 @@ function usage() {
|
|
|
43
45
|
' create-package-starter setup-github [--repo <owner/repo>] [--default-branch <branch>] [--ruleset <path>] [--dry-run]',
|
|
44
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]',
|
|
45
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]',
|
|
46
|
-
' create-package-starter release-cycle [--repo <owner/repo>] [--mode auto|open-pr|publish] [--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] [--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]',
|
|
47
49
|
' create-package-starter promote-stable [--dir <directory>] [--type patch|minor|major] [--summary <text>] [--dry-run]',
|
|
48
50
|
' create-package-starter setup-npm [--dir <directory>] [--publish-first] [--dry-run]',
|
|
49
51
|
'',
|
|
@@ -57,6 +59,7 @@ function usage() {
|
|
|
57
59
|
' create-package-starter setup-beta --dir . --beta-branch release/beta --release-auth app',
|
|
58
60
|
' create-package-starter open-pr --auto-merge --watch-checks',
|
|
59
61
|
' create-package-starter release-cycle --yes',
|
|
62
|
+
' create-package-starter release-cycle --promote-stable --promote-type minor --yes',
|
|
60
63
|
' create-package-starter promote-stable --dir . --type patch --summary "Promote beta to stable"',
|
|
61
64
|
' create-package-starter setup-npm --dir . --publish-first'
|
|
62
65
|
].join('\n');
|
|
@@ -542,6 +545,10 @@ function parseReleaseCycleArgs(argv) {
|
|
|
542
545
|
const args = {
|
|
543
546
|
repo: '',
|
|
544
547
|
mode: 'auto',
|
|
548
|
+
track: 'auto',
|
|
549
|
+
promoteStable: false,
|
|
550
|
+
promoteType: 'patch',
|
|
551
|
+
promoteSummary: 'Promote beta track to stable release.',
|
|
545
552
|
head: '',
|
|
546
553
|
base: '',
|
|
547
554
|
title: '',
|
|
@@ -555,6 +562,8 @@ function parseReleaseCycleArgs(argv) {
|
|
|
555
562
|
waitReleasePr: true,
|
|
556
563
|
releasePrTimeout: 30,
|
|
557
564
|
mergeReleasePr: true,
|
|
565
|
+
verifyNpm: true,
|
|
566
|
+
noCleanup: false,
|
|
558
567
|
yes: false,
|
|
559
568
|
dryRun: false
|
|
560
569
|
};
|
|
@@ -574,6 +583,24 @@ function parseReleaseCycleArgs(argv) {
|
|
|
574
583
|
continue;
|
|
575
584
|
}
|
|
576
585
|
|
|
586
|
+
if (token === '--track') {
|
|
587
|
+
args.track = parseValueFlag(argv, i, '--track');
|
|
588
|
+
i += 1;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (token === '--promote-type') {
|
|
593
|
+
args.promoteType = parseValueFlag(argv, i, '--promote-type');
|
|
594
|
+
i += 1;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (token === '--promote-summary') {
|
|
599
|
+
args.promoteSummary = parseValueFlag(argv, i, '--promote-summary');
|
|
600
|
+
i += 1;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
577
604
|
if (token === '--head') {
|
|
578
605
|
args.head = parseValueFlag(argv, i, '--head');
|
|
579
606
|
i += 1;
|
|
@@ -646,6 +673,21 @@ function parseReleaseCycleArgs(argv) {
|
|
|
646
673
|
continue;
|
|
647
674
|
}
|
|
648
675
|
|
|
676
|
+
if (token === '--promote-stable') {
|
|
677
|
+
args.promoteStable = true;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (token === '--verify-npm') {
|
|
682
|
+
args.verifyNpm = true;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (token === '--no-cleanup') {
|
|
687
|
+
args.noCleanup = true;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
|
|
649
691
|
if (token === '--yes') {
|
|
650
692
|
args.yes = true;
|
|
651
693
|
continue;
|
|
@@ -668,6 +710,14 @@ function parseReleaseCycleArgs(argv) {
|
|
|
668
710
|
throw new Error('Invalid --mode value. Expected auto, open-pr, or publish.');
|
|
669
711
|
}
|
|
670
712
|
|
|
713
|
+
if (!['auto', 'beta', 'stable'].includes(args.track)) {
|
|
714
|
+
throw new Error('Invalid --track value. Expected auto, beta, or stable.');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (!['patch', 'minor', 'major'].includes(args.promoteType)) {
|
|
718
|
+
throw new Error('Invalid --promote-type value. Expected patch, minor, or major.');
|
|
719
|
+
}
|
|
720
|
+
|
|
671
721
|
if (!['squash', 'merge', 'rebase'].includes(args.mergeMethod)) {
|
|
672
722
|
throw new Error('Invalid --merge-method value. Expected squash, merge, or rebase.');
|
|
673
723
|
}
|
|
@@ -1161,6 +1211,11 @@ function createOrchestrationSummary() {
|
|
|
1161
1211
|
checks: '',
|
|
1162
1212
|
merge: '',
|
|
1163
1213
|
releasePr: '',
|
|
1214
|
+
releaseTrack: '',
|
|
1215
|
+
promotionWorkflow: '',
|
|
1216
|
+
promotionPr: '',
|
|
1217
|
+
npmValidation: '',
|
|
1218
|
+
cleanup: '',
|
|
1164
1219
|
actionsPerformed: [],
|
|
1165
1220
|
actionsSkipped: [],
|
|
1166
1221
|
warnings: [],
|
|
@@ -1195,6 +1250,11 @@ function printOrchestrationSummary(title, summary) {
|
|
|
1195
1250
|
console.log(` - checks watched result: ${summary.checks || 'n/a'}`);
|
|
1196
1251
|
console.log(` - merge performed/skipped: ${summary.merge || 'n/a'}`);
|
|
1197
1252
|
console.log(` - release pr discovered/merged: ${summary.releasePr || 'n/a'}`);
|
|
1253
|
+
console.log(` - release track: ${summary.releaseTrack || 'n/a'}`);
|
|
1254
|
+
console.log(` - promotion workflow run: ${summary.promotionWorkflow || 'n/a'}`);
|
|
1255
|
+
console.log(` - promotion PR: ${summary.promotionPr || 'n/a'}`);
|
|
1256
|
+
console.log(` - npm validation: ${summary.npmValidation || 'n/a'}`);
|
|
1257
|
+
console.log(` - cleanup: ${summary.cleanup || 'n/a'}`);
|
|
1198
1258
|
console.log('actions performed:');
|
|
1199
1259
|
formatList(summary.actionsPerformed).forEach((line) => console.log(line));
|
|
1200
1260
|
console.log('actions skipped:');
|
|
@@ -1548,6 +1608,206 @@ async function confirmDetectedModeIfNeeded(args, mode, planText) {
|
|
|
1548
1608
|
);
|
|
1549
1609
|
}
|
|
1550
1610
|
|
|
1611
|
+
function ghApiJson(deps, method, endpoint, payload) {
|
|
1612
|
+
const result = ghApi(deps, method, endpoint, payload);
|
|
1613
|
+
if (result.status !== 0) {
|
|
1614
|
+
throw new Error(`GitHub API ${method} ${endpoint} failed: ${result.stderr || result.stdout}`.trim());
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return parseJsonSafely(result.stdout || '{}', {});
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function dispatchPromoteStableWorkflow(repo, args, deps) {
|
|
1621
|
+
const endpoint = `/repos/${repo}/actions/workflows/${encodeURIComponent(DEFAULT_PROMOTE_WORKFLOW)}/dispatches`;
|
|
1622
|
+
const payload = {
|
|
1623
|
+
ref: args.head || DEFAULT_BETA_BRANCH,
|
|
1624
|
+
inputs: {
|
|
1625
|
+
promote_type: args.promoteType,
|
|
1626
|
+
summary: args.promoteSummary,
|
|
1627
|
+
target_beta_branch: DEFAULT_BETA_BRANCH
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
ghApiJson(deps, 'POST', endpoint, payload);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function findPromotionPrs(repo, deps) {
|
|
1634
|
+
const prs = listOpenPullRequests(repo, deps);
|
|
1635
|
+
return prs.filter(
|
|
1636
|
+
(item) => item.baseRefName === DEFAULT_BETA_BRANCH
|
|
1637
|
+
&& typeof item.headRefName === 'string'
|
|
1638
|
+
&& item.headRefName.startsWith('promote/stable-')
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function waitForPromotionPr(repo, timeoutMinutes, deps) {
|
|
1643
|
+
const timeoutAt = Date.now() + timeoutMinutes * 60 * 1000;
|
|
1644
|
+
while (Date.now() <= timeoutAt) {
|
|
1645
|
+
const promotionPrs = findPromotionPrs(repo, deps);
|
|
1646
|
+
if (promotionPrs.length === 1) {
|
|
1647
|
+
return promotionPrs[0];
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
if (promotionPrs.length > 1) {
|
|
1651
|
+
promotionPrs.sort((a, b) => b.number - a.number);
|
|
1652
|
+
return promotionPrs[0];
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
sleepMs(5000);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
throw new Error(`Timed out waiting for promotion PR after ${timeoutMinutes} minutes.`);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function getRemotePackageVersion(repo, ref, deps) {
|
|
1662
|
+
const endpoint = `/repos/${repo}/contents/package.json?ref=${encodeURIComponent(ref)}`;
|
|
1663
|
+
const contentResponse = ghApiJson(deps, 'GET', endpoint);
|
|
1664
|
+
if (!contentResponse.content) {
|
|
1665
|
+
throw new Error(`Could not read package.json content from ${repo}@${ref}.`);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const decoded = Buffer.from(String(contentResponse.content).replace(/\n/g, ''), 'base64').toString('utf8');
|
|
1669
|
+
const parsed = parseJsonSafely(decoded, {});
|
|
1670
|
+
if (!parsed.name || !parsed.version) {
|
|
1671
|
+
throw new Error(`package.json from ${repo}@${ref} must include name and version.`);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
return {
|
|
1675
|
+
name: parsed.name,
|
|
1676
|
+
version: parsed.version
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function validateNpmPublishedVersionAndTag(packageName, expectedVersion, expectedTag, timeoutMinutes, deps) {
|
|
1681
|
+
const timeoutAt = Date.now() + timeoutMinutes * 60 * 1000;
|
|
1682
|
+
let lastObservedVersion = '';
|
|
1683
|
+
let lastObservedTagVersion = '';
|
|
1684
|
+
|
|
1685
|
+
while (Date.now() <= timeoutAt) {
|
|
1686
|
+
const versionResult = deps.exec('npm', ['view', packageName, 'version', '--json']);
|
|
1687
|
+
const tagsResult = deps.exec('npm', ['view', packageName, 'dist-tags', '--json']);
|
|
1688
|
+
if (versionResult.status === 0 && tagsResult.status === 0) {
|
|
1689
|
+
const observedVersion = String(parseJsonSafely(versionResult.stdout || '""', '') || '');
|
|
1690
|
+
const tags = parseJsonSafely(tagsResult.stdout || '{}', {});
|
|
1691
|
+
const observedTagVersion = tags && tags[expectedTag] ? String(tags[expectedTag]) : '';
|
|
1692
|
+
lastObservedVersion = observedVersion;
|
|
1693
|
+
lastObservedTagVersion = observedTagVersion;
|
|
1694
|
+
|
|
1695
|
+
if (observedVersion === expectedVersion && observedTagVersion === expectedVersion) {
|
|
1696
|
+
return {
|
|
1697
|
+
status: 'pass',
|
|
1698
|
+
observedVersion,
|
|
1699
|
+
observedTagVersion
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
sleepMs(10000);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return {
|
|
1708
|
+
status: 'timeout',
|
|
1709
|
+
observedVersion: lastObservedVersion,
|
|
1710
|
+
observedTagVersion: lastObservedTagVersion
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function ensureWorkingTreeClean(deps) {
|
|
1715
|
+
const status = deps.exec('git', ['status', '--porcelain']);
|
|
1716
|
+
if (status.status !== 0) {
|
|
1717
|
+
throw new Error('Failed to inspect working tree status.');
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
return status.stdout.trim() === '';
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function isProtectedOrGeneratedBranch(branchName) {
|
|
1724
|
+
if (!branchName) {
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
return branchName === DEFAULT_BASE_BRANCH
|
|
1729
|
+
|| branchName === DEFAULT_BETA_BRANCH
|
|
1730
|
+
|| branchName.startsWith('changeset-release/')
|
|
1731
|
+
|| branchName.startsWith('promote/');
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function isCleanupCandidateBranch(branchName) {
|
|
1735
|
+
if (!branchName) {
|
|
1736
|
+
return false;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
return /^(feat|fix|chore|refactor|test)\//.test(branchName);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function runLocalCleanup({
|
|
1743
|
+
deps,
|
|
1744
|
+
originalBranch,
|
|
1745
|
+
targetBaseBranch,
|
|
1746
|
+
shouldRun,
|
|
1747
|
+
summary,
|
|
1748
|
+
reporter
|
|
1749
|
+
}) {
|
|
1750
|
+
if (!shouldRun) {
|
|
1751
|
+
summary.actionsSkipped.push('cleanup');
|
|
1752
|
+
summary.cleanup = 'skipped';
|
|
1753
|
+
summary.warnings.push('Local cleanup skipped by configuration (--no-cleanup).');
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (!isCleanupCandidateBranch(originalBranch)) {
|
|
1758
|
+
summary.actionsSkipped.push('cleanup');
|
|
1759
|
+
summary.cleanup = 'skipped';
|
|
1760
|
+
summary.warnings.push(`Cleanup skipped: branch "${originalBranch}" is not an allowed code branch pattern.`);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (isProtectedOrGeneratedBranch(originalBranch)) {
|
|
1765
|
+
summary.actionsSkipped.push('cleanup');
|
|
1766
|
+
summary.cleanup = 'skipped';
|
|
1767
|
+
summary.warnings.push(`Cleanup skipped: branch "${originalBranch}" is protected or generated.`);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
if (!ensureWorkingTreeClean(deps)) {
|
|
1772
|
+
summary.actionsSkipped.push('cleanup');
|
|
1773
|
+
summary.cleanup = 'skipped';
|
|
1774
|
+
summary.warnings.push('Cleanup skipped: working tree is not clean.');
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
reporter.start('release-cycle-cleanup-checkout', `Checking out ${targetBaseBranch}...`);
|
|
1779
|
+
const checkout = deps.exec('git', ['checkout', targetBaseBranch]);
|
|
1780
|
+
if (checkout.status !== 0) {
|
|
1781
|
+
summary.cleanup = 'failed';
|
|
1782
|
+
summary.warnings.push(`Cleanup failed: could not checkout ${targetBaseBranch}: ${(checkout.stderr || checkout.stdout || '').trim()}`);
|
|
1783
|
+
reporter.warn('release-cycle-cleanup-checkout', `Could not checkout ${targetBaseBranch}.`);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
reporter.ok('release-cycle-cleanup-checkout', `Checked out ${targetBaseBranch}.`);
|
|
1787
|
+
|
|
1788
|
+
reporter.start('release-cycle-cleanup-pull', `Pulling latest ${targetBaseBranch}...`);
|
|
1789
|
+
const pull = deps.exec('git', ['pull']);
|
|
1790
|
+
if (pull.status !== 0) {
|
|
1791
|
+
summary.cleanup = 'failed';
|
|
1792
|
+
summary.warnings.push(`Cleanup warning: could not pull ${targetBaseBranch}: ${(pull.stderr || pull.stdout || '').trim()}`);
|
|
1793
|
+
reporter.warn('release-cycle-cleanup-pull', `Could not pull ${targetBaseBranch}.`);
|
|
1794
|
+
} else {
|
|
1795
|
+
reporter.ok('release-cycle-cleanup-pull', `Pulled ${targetBaseBranch}.`);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
reporter.start('release-cycle-cleanup-delete', `Deleting local branch ${originalBranch}...`);
|
|
1799
|
+
const deleteResult = deps.exec('git', ['branch', '-d', originalBranch]);
|
|
1800
|
+
if (deleteResult.status !== 0) {
|
|
1801
|
+
summary.cleanup = 'failed';
|
|
1802
|
+
summary.warnings.push(`Cleanup warning: could not delete ${originalBranch}: ${(deleteResult.stderr || deleteResult.stdout || '').trim()}`);
|
|
1803
|
+
reporter.warn('release-cycle-cleanup-delete', `Could not delete ${originalBranch}.`);
|
|
1804
|
+
} else {
|
|
1805
|
+
summary.actionsPerformed.push(`cleanup deleted branch: ${originalBranch}`);
|
|
1806
|
+
summary.cleanup = 'completed';
|
|
1807
|
+
reporter.ok('release-cycle-cleanup-delete', `Deleted ${originalBranch}.`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1551
1811
|
function ensureFileFromTemplate(targetPath, templatePath, options) {
|
|
1552
1812
|
const exists = fs.existsSync(targetPath);
|
|
1553
1813
|
|
|
@@ -2126,13 +2386,18 @@ async function runOpenPrFlow(args, dependencies = {}) {
|
|
|
2126
2386
|
};
|
|
2127
2387
|
}
|
|
2128
2388
|
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2389
|
+
if (args.skipPush) {
|
|
2390
|
+
summary.branchPushed = `skipped (${context.head})`;
|
|
2391
|
+
summary.actionsSkipped.push(`push skipped: ${context.head}`);
|
|
2392
|
+
} else {
|
|
2393
|
+
reporter.start('open-pr-push', `Pushing branch "${context.head}"...`);
|
|
2394
|
+
const pushResult = ensureBranchPushed(context.repo, context.head, deps);
|
|
2395
|
+
reporter.ok('open-pr-push', `Branch "${context.head}" pushed (${pushResult.status}).`);
|
|
2396
|
+
summary.branchPushed = `${context.head} (${pushResult.status})`;
|
|
2397
|
+
summary.actionsPerformed.push(`branch pushed: ${context.head}`);
|
|
2398
|
+
if (pushResult.status === 'up-to-date') {
|
|
2399
|
+
summary.warnings.push(`Branch "${context.head}" had no new commits to push.`);
|
|
2400
|
+
}
|
|
2136
2401
|
}
|
|
2137
2402
|
|
|
2138
2403
|
reporter.start('open-pr-upsert', 'Creating or updating pull request...');
|
|
@@ -2144,7 +2409,7 @@ async function runOpenPrFlow(args, dependencies = {}) {
|
|
|
2144
2409
|
|
|
2145
2410
|
if (args.autoMerge) {
|
|
2146
2411
|
reporter.start('open-pr-auto-merge', `Enabling auto-merge for PR #${prResult.number}...`);
|
|
2147
|
-
enablePrAutoMerge(context.repo, prResult.number, 'squash', deps);
|
|
2412
|
+
enablePrAutoMerge(context.repo, prResult.number, args.mergeMethod || 'squash', deps);
|
|
2148
2413
|
reporter.ok('open-pr-auto-merge', `Auto-merge enabled for PR #${prResult.number}.`);
|
|
2149
2414
|
summary.autoMerge = 'enabled';
|
|
2150
2415
|
summary.actionsPerformed.push(`auto-merge enabled for #${prResult.number}`);
|
|
@@ -2186,6 +2451,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2186
2451
|
};
|
|
2187
2452
|
const summary = createOrchestrationSummary();
|
|
2188
2453
|
const reporter = new StepReporter();
|
|
2454
|
+
const originalBranch = deps.exec('git', ['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
|
2189
2455
|
|
|
2190
2456
|
reporter.start('release-cycle-preflight-gh', 'Validating GitHub CLI and authentication...');
|
|
2191
2457
|
ensureGhAvailable(deps);
|
|
@@ -2193,9 +2459,23 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2193
2459
|
|
|
2194
2460
|
const gitContext = resolveGitContext(args, deps);
|
|
2195
2461
|
summary.repoResolved = gitContext.repo;
|
|
2462
|
+
const requestedTrack = args.track === 'auto' ? (args.promoteStable ? 'stable' : 'beta') : args.track;
|
|
2463
|
+
if (args.promoteStable && gitContext.head !== DEFAULT_BETA_BRANCH) {
|
|
2464
|
+
throw new Error(`--promote-stable is only allowed when running from "${DEFAULT_BETA_BRANCH}".`);
|
|
2465
|
+
}
|
|
2466
|
+
if (requestedTrack === 'stable' && !args.promoteStable) {
|
|
2467
|
+
throw new Error('Stable track requires --promote-stable for explicit promotion.');
|
|
2468
|
+
}
|
|
2469
|
+
if (gitContext.head !== DEFAULT_BETA_BRANCH && requestedTrack === 'stable') {
|
|
2470
|
+
throw new Error(`Stable track is only supported from "${DEFAULT_BETA_BRANCH}".`);
|
|
2471
|
+
}
|
|
2472
|
+
summary.actionsPerformed.push(`release track: ${requestedTrack}`);
|
|
2473
|
+
summary.releaseTrack = requestedTrack;
|
|
2196
2474
|
|
|
2197
2475
|
let detectedMode = args.mode;
|
|
2198
|
-
if (
|
|
2476
|
+
if (args.promoteStable) {
|
|
2477
|
+
detectedMode = 'open-pr';
|
|
2478
|
+
} else if (detectedMode === 'auto') {
|
|
2199
2479
|
const releasePrs = findReleasePrs(gitContext.repo, deps);
|
|
2200
2480
|
if (gitContext.head.startsWith('changeset-release/')) {
|
|
2201
2481
|
detectedMode = 'publish';
|
|
@@ -2218,12 +2498,69 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2218
2498
|
);
|
|
2219
2499
|
|
|
2220
2500
|
if (detectedMode === 'open-pr') {
|
|
2501
|
+
if (args.promoteStable) {
|
|
2502
|
+
reporter.start('release-cycle-promote-dispatch', `Dispatching ${DEFAULT_PROMOTE_WORKFLOW}...`);
|
|
2503
|
+
if (args.dryRun) {
|
|
2504
|
+
reporter.warn('release-cycle-promote-dispatch', `Dry-run: would dispatch ${DEFAULT_PROMOTE_WORKFLOW}.`);
|
|
2505
|
+
summary.actionsPerformed.push(`dry-run: dispatch ${DEFAULT_PROMOTE_WORKFLOW}`);
|
|
2506
|
+
summary.promotionWorkflow = `dry-run: ${DEFAULT_PROMOTE_WORKFLOW}`;
|
|
2507
|
+
} else {
|
|
2508
|
+
dispatchPromoteStableWorkflow(gitContext.repo, {
|
|
2509
|
+
...args,
|
|
2510
|
+
head: DEFAULT_BETA_BRANCH
|
|
2511
|
+
}, deps);
|
|
2512
|
+
reporter.ok('release-cycle-promote-dispatch', `Dispatched ${DEFAULT_PROMOTE_WORKFLOW}.`);
|
|
2513
|
+
summary.actionsPerformed.push(`promotion workflow dispatched: ${DEFAULT_PROMOTE_WORKFLOW}`);
|
|
2514
|
+
summary.promotionWorkflow = `dispatched: ${DEFAULT_PROMOTE_WORKFLOW}`;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
if (!args.dryRun) {
|
|
2518
|
+
reporter.start('release-cycle-promote-pr', 'Waiting for promotion PR...');
|
|
2519
|
+
const promotionPr = waitForPromotionPr(gitContext.repo, args.releasePrTimeout, deps);
|
|
2520
|
+
reporter.ok('release-cycle-promote-pr', `Promotion PR found: #${promotionPr.number}`);
|
|
2521
|
+
summary.actionsPerformed.push(`promotion pr discovered: #${promotionPr.number}`);
|
|
2522
|
+
summary.promotionPr = `found (#${promotionPr.number})`;
|
|
2523
|
+
|
|
2524
|
+
if (args.watchChecks) {
|
|
2525
|
+
reporter.start('release-cycle-promote-checks', `Watching promotion PR checks #${promotionPr.number}...`);
|
|
2526
|
+
watchPrChecks(gitContext.repo, promotionPr.number, args.checkTimeout, deps);
|
|
2527
|
+
reporter.ok('release-cycle-promote-checks', `Promotion PR checks green (#${promotionPr.number}).`);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
if (args.mergeWhenGreen) {
|
|
2531
|
+
reporter.start('release-cycle-promote-merge', `Merging promotion PR #${promotionPr.number}...`);
|
|
2532
|
+
mergePrWhenGreen(gitContext.repo, promotionPr.number, args.mergeMethod, deps);
|
|
2533
|
+
reporter.ok('release-cycle-promote-merge', `Promotion PR #${promotionPr.number} merged.`);
|
|
2534
|
+
summary.actionsPerformed.push(`promotion pr merged: #${promotionPr.number}`);
|
|
2535
|
+
summary.promotionPr = `merged (#${promotionPr.number})`;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
reporter.start('release-cycle-sync-beta', `Syncing local ${DEFAULT_BETA_BRANCH} branch...`);
|
|
2539
|
+
const checkoutBeta = deps.exec('git', ['checkout', DEFAULT_BETA_BRANCH]);
|
|
2540
|
+
if (checkoutBeta.status !== 0) {
|
|
2541
|
+
throw new Error(`Could not checkout ${DEFAULT_BETA_BRANCH}: ${(checkoutBeta.stderr || checkoutBeta.stdout || '').trim()}`);
|
|
2542
|
+
}
|
|
2543
|
+
const pullBeta = deps.exec('git', ['pull']);
|
|
2544
|
+
if (pullBeta.status !== 0) {
|
|
2545
|
+
throw new Error(`Could not pull ${DEFAULT_BETA_BRANCH}: ${(pullBeta.stderr || pullBeta.stdout || '').trim()}`);
|
|
2546
|
+
}
|
|
2547
|
+
reporter.ok('release-cycle-sync-beta', `${DEFAULT_BETA_BRANCH} synced.`);
|
|
2548
|
+
}
|
|
2549
|
+
} else {
|
|
2550
|
+
summary.promotionWorkflow = 'skipped';
|
|
2551
|
+
summary.promotionPr = 'skipped';
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2221
2554
|
const openPrResult = await runOpenPrFlow(
|
|
2222
2555
|
{
|
|
2223
2556
|
...args,
|
|
2557
|
+
head: args.promoteStable ? DEFAULT_BETA_BRANCH : args.head,
|
|
2558
|
+
base: args.promoteStable ? DEFAULT_BASE_BRANCH : args.base,
|
|
2224
2559
|
autoMerge: args.autoMerge,
|
|
2225
2560
|
watchChecks: args.watchChecks,
|
|
2226
2561
|
checkTimeout: args.checkTimeout,
|
|
2562
|
+
mergeMethod: args.mergeMethod,
|
|
2563
|
+
skipPush: args.promoteStable,
|
|
2227
2564
|
printSummary: false
|
|
2228
2565
|
},
|
|
2229
2566
|
dependencies
|
|
@@ -2250,6 +2587,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2250
2587
|
summary.actionsSkipped.push('merge code pr');
|
|
2251
2588
|
}
|
|
2252
2589
|
|
|
2590
|
+
let mergedReleasePr = null;
|
|
2253
2591
|
if (args.waitReleasePr) {
|
|
2254
2592
|
if (args.dryRun) {
|
|
2255
2593
|
summary.releasePr = `dry-run: would wait release PR (${args.releasePrTimeout}m)`;
|
|
@@ -2272,6 +2610,7 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2272
2610
|
reporter.ok('release-cycle-merge-release-pr', `Release PR #${releasePr.number} merged.`);
|
|
2273
2611
|
summary.releasePr = `merged (#${releasePr.number})`;
|
|
2274
2612
|
summary.actionsPerformed.push(`release pr merged: #${releasePr.number}`);
|
|
2613
|
+
mergedReleasePr = releasePr;
|
|
2275
2614
|
} else {
|
|
2276
2615
|
summary.actionsSkipped.push('merge release pr');
|
|
2277
2616
|
}
|
|
@@ -2281,6 +2620,55 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2281
2620
|
summary.actionsSkipped.push('wait release pr');
|
|
2282
2621
|
}
|
|
2283
2622
|
|
|
2623
|
+
if (args.verifyNpm && !args.dryRun && mergedReleasePr) {
|
|
2624
|
+
reporter.start('release-cycle-verify-npm', 'Validating npm publish and dist-tag...');
|
|
2625
|
+
const targetRef = args.promoteStable ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
|
|
2626
|
+
const expectedTag = requestedTrack === 'stable' ? 'latest' : 'beta';
|
|
2627
|
+
const remotePackage = getRemotePackageVersion(gitContext.repo, targetRef, deps);
|
|
2628
|
+
const npmValidation = validateNpmPublishedVersionAndTag(
|
|
2629
|
+
remotePackage.name,
|
|
2630
|
+
remotePackage.version,
|
|
2631
|
+
expectedTag,
|
|
2632
|
+
args.releasePrTimeout,
|
|
2633
|
+
deps
|
|
2634
|
+
);
|
|
2635
|
+
if (npmValidation.status !== 'pass') {
|
|
2636
|
+
summary.npmValidation = `failed (${expectedTag})`;
|
|
2637
|
+
throw new Error(
|
|
2638
|
+
[
|
|
2639
|
+
'npm validation failed after release merge.',
|
|
2640
|
+
`Expected: ${remotePackage.name}@${remotePackage.version} with dist-tag ${expectedTag}`,
|
|
2641
|
+
`Observed version: ${npmValidation.observedVersion || 'n/a'}`,
|
|
2642
|
+
`Observed tag (${expectedTag}): ${npmValidation.observedTagVersion || 'n/a'}`
|
|
2643
|
+
].join('\n')
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
reporter.ok('release-cycle-verify-npm', `${remotePackage.name}@${remotePackage.version} validated on tag ${expectedTag}.`);
|
|
2647
|
+
summary.actionsPerformed.push(`npm validation: ${remotePackage.name}@${remotePackage.version} (${expectedTag})`);
|
|
2648
|
+
summary.npmValidation = `pass (${expectedTag} -> ${remotePackage.version})`;
|
|
2649
|
+
} else if (!args.verifyNpm) {
|
|
2650
|
+
summary.actionsSkipped.push('verify npm');
|
|
2651
|
+
summary.npmValidation = 'skipped';
|
|
2652
|
+
} else if (args.dryRun) {
|
|
2653
|
+
summary.npmValidation = 'skipped (dry-run)';
|
|
2654
|
+
} else if (!mergedReleasePr) {
|
|
2655
|
+
summary.npmValidation = 'skipped (release pr not merged)';
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
if (!args.dryRun) {
|
|
2659
|
+
runLocalCleanup({
|
|
2660
|
+
deps,
|
|
2661
|
+
originalBranch,
|
|
2662
|
+
targetBaseBranch: requestedTrack === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH,
|
|
2663
|
+
shouldRun: !args.noCleanup,
|
|
2664
|
+
summary,
|
|
2665
|
+
reporter
|
|
2666
|
+
});
|
|
2667
|
+
} else {
|
|
2668
|
+
summary.actionsSkipped.push('cleanup (dry-run)');
|
|
2669
|
+
summary.cleanup = 'skipped (dry-run)';
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2284
2672
|
printOrchestrationSummary(`release-cycle completed in ${detectedMode} mode`, summary);
|
|
2285
2673
|
return;
|
|
2286
2674
|
}
|
|
@@ -2336,6 +2724,53 @@ async function runReleaseCycle(args, dependencies = {}) {
|
|
|
2336
2724
|
summary.actionsSkipped.push('merge release pr');
|
|
2337
2725
|
}
|
|
2338
2726
|
|
|
2727
|
+
if (args.verifyNpm && !args.dryRun && (args.mergeReleasePr || args.mergeWhenGreen)) {
|
|
2728
|
+
reporter.start('release-cycle-verify-npm', 'Validating npm publish and dist-tag...');
|
|
2729
|
+
const targetRef = requestedTrack === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH;
|
|
2730
|
+
const expectedTag = requestedTrack === 'stable' ? 'latest' : 'beta';
|
|
2731
|
+
const remotePackage = getRemotePackageVersion(gitContext.repo, targetRef, deps);
|
|
2732
|
+
const npmValidation = validateNpmPublishedVersionAndTag(
|
|
2733
|
+
remotePackage.name,
|
|
2734
|
+
remotePackage.version,
|
|
2735
|
+
expectedTag,
|
|
2736
|
+
args.releasePrTimeout,
|
|
2737
|
+
deps
|
|
2738
|
+
);
|
|
2739
|
+
if (npmValidation.status !== 'pass') {
|
|
2740
|
+
summary.npmValidation = `failed (${expectedTag})`;
|
|
2741
|
+
throw new Error(
|
|
2742
|
+
[
|
|
2743
|
+
'npm validation failed after release merge.',
|
|
2744
|
+
`Expected: ${remotePackage.name}@${remotePackage.version} with dist-tag ${expectedTag}`,
|
|
2745
|
+
`Observed version: ${npmValidation.observedVersion || 'n/a'}`,
|
|
2746
|
+
`Observed tag (${expectedTag}): ${npmValidation.observedTagVersion || 'n/a'}`
|
|
2747
|
+
].join('\n')
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
reporter.ok('release-cycle-verify-npm', `${remotePackage.name}@${remotePackage.version} validated on tag ${expectedTag}.`);
|
|
2751
|
+
summary.actionsPerformed.push(`npm validation: ${remotePackage.name}@${remotePackage.version} (${expectedTag})`);
|
|
2752
|
+
summary.npmValidation = `pass (${expectedTag} -> ${remotePackage.version})`;
|
|
2753
|
+
} else if (!args.verifyNpm) {
|
|
2754
|
+
summary.npmValidation = 'skipped';
|
|
2755
|
+
} else if (args.dryRun) {
|
|
2756
|
+
summary.npmValidation = 'skipped (dry-run)';
|
|
2757
|
+
} else {
|
|
2758
|
+
summary.npmValidation = 'skipped (release pr not merged)';
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
if (!args.dryRun) {
|
|
2762
|
+
runLocalCleanup({
|
|
2763
|
+
deps,
|
|
2764
|
+
originalBranch,
|
|
2765
|
+
targetBaseBranch: requestedTrack === 'stable' ? DEFAULT_BASE_BRANCH : DEFAULT_BETA_BRANCH,
|
|
2766
|
+
shouldRun: !args.noCleanup,
|
|
2767
|
+
summary,
|
|
2768
|
+
reporter
|
|
2769
|
+
});
|
|
2770
|
+
} else {
|
|
2771
|
+
summary.cleanup = 'skipped (dry-run)';
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2339
2774
|
printOrchestrationSummary(`release-cycle completed in ${detectedMode} mode`, summary);
|
|
2340
2775
|
}
|
|
2341
2776
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
name: Promote Stable
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
promote_type:
|
|
7
|
+
description: Promotion bump type
|
|
8
|
+
required: true
|
|
9
|
+
default: patch
|
|
10
|
+
type: choice
|
|
11
|
+
options:
|
|
12
|
+
- patch
|
|
13
|
+
- minor
|
|
14
|
+
- major
|
|
15
|
+
summary:
|
|
16
|
+
description: Promotion changeset summary
|
|
17
|
+
required: true
|
|
18
|
+
default: Promote beta track to stable release.
|
|
19
|
+
target_beta_branch:
|
|
20
|
+
description: Target beta branch
|
|
21
|
+
required: true
|
|
22
|
+
default: __BETA_BRANCH__
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
contents: write
|
|
26
|
+
pull-requests: write
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
promote:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
outputs:
|
|
32
|
+
promotion_branch: ${{ steps.prepare.outputs.promotion_branch }}
|
|
33
|
+
promotion_pr_number: ${{ steps.pr.outputs.pull-request-number }}
|
|
34
|
+
promotion_pr_url: ${{ steps.pr.outputs.pull-request-url }}
|
|
35
|
+
steps:
|
|
36
|
+
__RELEASE_AUTH_APP_STEP__
|
|
37
|
+
|
|
38
|
+
- name: Checkout
|
|
39
|
+
uses: actions/checkout@v4
|
|
40
|
+
with:
|
|
41
|
+
fetch-depth: 0
|
|
42
|
+
token: __RELEASE_AUTH_CHECKOUT_TOKEN__
|
|
43
|
+
ref: ${{ github.event.inputs.target_beta_branch }}
|
|
44
|
+
|
|
45
|
+
- name: Setup Node.js
|
|
46
|
+
uses: actions/setup-node@v4
|
|
47
|
+
with:
|
|
48
|
+
node-version: 22
|
|
49
|
+
cache: npm
|
|
50
|
+
|
|
51
|
+
- name: Install
|
|
52
|
+
run: npm ci
|
|
53
|
+
|
|
54
|
+
- name: Prepare promotion branch
|
|
55
|
+
id: prepare
|
|
56
|
+
run: |
|
|
57
|
+
promotion_branch="promote/stable-$(date +%s)"
|
|
58
|
+
echo "promotion_branch=$promotion_branch" >> "$GITHUB_OUTPUT"
|
|
59
|
+
git checkout -b "$promotion_branch"
|
|
60
|
+
|
|
61
|
+
- name: Exit prerelease mode
|
|
62
|
+
run: npm run beta:exit
|
|
63
|
+
|
|
64
|
+
- name: Create promotion changeset
|
|
65
|
+
run: |
|
|
66
|
+
mkdir -p .changeset
|
|
67
|
+
file=".changeset/promote-stable-${{ github.run_id }}.md"
|
|
68
|
+
cat > "$file" <<EOF
|
|
69
|
+
---
|
|
70
|
+
"__PACKAGE_NAME__": ${{ github.event.inputs.promote_type }}
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
${{ github.event.inputs.summary }}
|
|
74
|
+
EOF
|
|
75
|
+
|
|
76
|
+
- name: Commit and push
|
|
77
|
+
run: |
|
|
78
|
+
git config user.name "github-actions[bot]"
|
|
79
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
80
|
+
git add .changeset
|
|
81
|
+
git commit -m "chore: promote beta to stable"
|
|
82
|
+
git push --set-upstream origin "${{ steps.prepare.outputs.promotion_branch }}"
|
|
83
|
+
|
|
84
|
+
- name: Open promotion PR
|
|
85
|
+
id: pr
|
|
86
|
+
uses: peter-evans/create-pull-request@v6
|
|
87
|
+
with:
|
|
88
|
+
token: __RELEASE_AUTH_GITHUB_TOKEN__
|
|
89
|
+
base: ${{ github.event.inputs.target_beta_branch }}
|
|
90
|
+
branch: ${{ steps.prepare.outputs.promotion_branch }}
|
|
91
|
+
title: "chore: promote beta to stable"
|
|
92
|
+
body: |
|
|
93
|
+
## Summary
|
|
94
|
+
- Exit prerelease mode (`changeset pre exit`)
|
|
95
|
+
- Add stable promotion changeset
|
|
96
|
+
- Prepare stable release flow from `${{ github.event.inputs.target_beta_branch }}`
|
|
97
|
+
draft: false
|
|
98
|
+
|
|
99
|
+
- name: Enable auto-merge
|
|
100
|
+
if: steps.pr.outputs.pull-request-number != ''
|
|
101
|
+
run: gh pr merge "${{ steps.pr.outputs.pull-request-number }}" --repo "${{ github.repository }}" --squash --auto
|
|
102
|
+
env:
|
|
103
|
+
GH_TOKEN: __RELEASE_AUTH_GITHUB_TOKEN__
|