@wipcomputer/wip-release 1.9.72 → 1.9.73

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.
Files changed (2) hide show
  1. package/core.mjs +143 -18
  2. package/package.json +1 -1
package/core.mjs CHANGED
@@ -1507,6 +1507,125 @@ function checkTagCollision(repoPath, newVersion) {
1507
1507
  return { ok: true };
1508
1508
  }
1509
1509
 
1510
+ /**
1511
+ * Push the release commit + tag, handling protected-main rejection via
1512
+ * automatic PR flow.
1513
+ *
1514
+ * If `git push origin main` succeeds directly, we also push tags and return.
1515
+ * If it fails with a "protected branch" error (GH006), create a temporary
1516
+ * release branch from the current HEAD, push the branch, open a PR targeting
1517
+ * main, auto-merge it via `gh pr merge --merge --delete-branch`, then push
1518
+ * the tag separately (tags bypass branch protection).
1519
+ *
1520
+ * Phase 4 of the release-pipeline master plan. Earlier today this entire
1521
+ * flow was done manually every time, adding 2-3 minutes per release.
1522
+ *
1523
+ * Returns `{ ok: true, via }` on success where `via` is `"direct"` or `"pr"`.
1524
+ * Returns `{ ok: false, reason, detail }` on failure; caller logs and
1525
+ * continues (matches prior non-fatal push behavior).
1526
+ */
1527
+ function pushReleaseWithAutoPr(repoPath, newVersion, level) {
1528
+ const tag = `v${newVersion}`;
1529
+ // 1. Try direct push first. Most repos allow it.
1530
+ try {
1531
+ execFileSync('git', ['push'], { cwd: repoPath, stdio: 'pipe' });
1532
+ execFileSync('git', ['push', 'origin', tag], { cwd: repoPath, stdio: 'pipe' });
1533
+ return { ok: true, via: 'direct' };
1534
+ } catch (err) {
1535
+ const msg = String(err?.stderr ?? err?.message ?? err);
1536
+ const isProtected = /protected branch|GH006|Changes must be made through a pull request/i.test(msg);
1537
+ if (!isProtected) {
1538
+ return { ok: false, reason: 'push-failed', detail: msg };
1539
+ }
1540
+ }
1541
+
1542
+ // 2. Protected main. Open auto-PR flow.
1543
+ const releaseBranch = `cc-mini/release-${tag}`;
1544
+ console.log(` - Direct push to main refused (protected). Opening release PR...`);
1545
+ try {
1546
+ // Create branch at current HEAD
1547
+ execFileSync('git', ['branch', releaseBranch], { cwd: repoPath, stdio: 'pipe' });
1548
+ execFileSync('git', ['push', '-u', 'origin', releaseBranch], {
1549
+ cwd: repoPath, stdio: 'pipe'
1550
+ });
1551
+ } catch (err) {
1552
+ return {
1553
+ ok: false,
1554
+ reason: 'branch-push-failed',
1555
+ detail: String(err?.stderr ?? err?.message ?? err),
1556
+ };
1557
+ }
1558
+
1559
+ // 3. Create and merge PR via gh CLI
1560
+ try {
1561
+ const prTitle = `release: ${tag}`;
1562
+ const prBody = `Release commit for ${tag} (${level}). Auto-generated by wip-release.`;
1563
+ execFileSync('gh', [
1564
+ 'pr', 'create',
1565
+ '--base', 'main',
1566
+ '--head', releaseBranch,
1567
+ '--title', prTitle,
1568
+ '--body', prBody,
1569
+ ], { cwd: repoPath, stdio: 'pipe' });
1570
+ execFileSync('gh', [
1571
+ 'pr', 'merge', releaseBranch,
1572
+ '--merge', '--delete-branch',
1573
+ ], { cwd: repoPath, stdio: 'pipe' });
1574
+ console.log(` ✓ Release PR merged`);
1575
+ } catch (err) {
1576
+ return {
1577
+ ok: false,
1578
+ reason: 'gh-pr-failed',
1579
+ detail: String(err?.stderr ?? err?.message ?? err),
1580
+ };
1581
+ }
1582
+
1583
+ // 4. Push the tag separately. Tags bypass branch protection on most
1584
+ // GitHub setups, but if this fails the user can push the tag manually.
1585
+ try {
1586
+ execFileSync('git', ['push', 'origin', tag], { cwd: repoPath, stdio: 'pipe' });
1587
+ console.log(` ✓ Pushed tag ${tag}`);
1588
+ } catch (err) {
1589
+ return {
1590
+ ok: false,
1591
+ reason: 'tag-push-failed',
1592
+ detail: String(err?.stderr ?? err?.message ?? err),
1593
+ partialSuccess: 'pr-merged',
1594
+ };
1595
+ }
1596
+
1597
+ // 5. Pull latest main so local HEAD reflects the merge commit. This
1598
+ // keeps subsequent git operations (like deploy-public) happy.
1599
+ try {
1600
+ execFileSync('git', ['fetch', 'origin', 'main'], { cwd: repoPath, stdio: 'pipe' });
1601
+ execFileSync('git', ['merge', '--ff-only', 'origin/main'], {
1602
+ cwd: repoPath, stdio: 'pipe'
1603
+ });
1604
+ } catch {
1605
+ // Non-fatal: main may have diverged (unlikely here). Deploy-public
1606
+ // will handle its own state.
1607
+ }
1608
+
1609
+ return { ok: true, via: 'pr' };
1610
+ }
1611
+
1612
+ function logPushFailure(result, tag) {
1613
+ console.log(` ! Push failed: ${result.reason}`);
1614
+ if (result.detail) {
1615
+ console.log(` ${result.detail.split('\n')[0]}`);
1616
+ }
1617
+ console.log(` Manual recovery:`);
1618
+ if (result.reason === 'push-failed' || result.reason === 'branch-push-failed') {
1619
+ console.log(` git push && git push origin ${tag}`);
1620
+ } else if (result.reason === 'gh-pr-failed') {
1621
+ console.log(` Open a PR for cc-mini/release-${tag} targeting main, merge, then:`);
1622
+ console.log(` git push origin ${tag}`);
1623
+ } else if (result.reason === 'tag-push-failed') {
1624
+ console.log(` PR already merged. Just push the tag:`);
1625
+ console.log(` git push origin ${tag}`);
1626
+ }
1627
+ }
1628
+
1510
1629
  function logMainBranchGuardFailure(result) {
1511
1630
  if (result.reason === 'linked-worktree') {
1512
1631
  console.log(` \u2717 wip-release must run from the main working tree, not a worktree.`);
@@ -1898,12 +2017,14 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1898
2017
  gitCommitAndTag(repoPath, newVersion, notes);
1899
2018
  console.log(` ✓ Committed and tagged v${newVersion}`);
1900
2019
 
1901
- // 5. Push commit + tag
1902
- try {
1903
- execSync('git push && git push --tags', { cwd: repoPath, stdio: 'pipe' });
1904
- console.log(` ✓ Pushed to remote`);
1905
- } catch {
1906
- console.log(` ! Push failed (maybe branch protection). Push manually.`);
2020
+ // 5. Push commit + tag (with auto-PR fallback on protected main, Phase 4)
2021
+ {
2022
+ const pushResult = pushReleaseWithAutoPr(repoPath, newVersion, level);
2023
+ if (pushResult.ok) {
2024
+ console.log(` Pushed to remote (${pushResult.via})`);
2025
+ } else {
2026
+ logPushFailure(pushResult, `v${newVersion}`);
2027
+ }
1907
2028
  }
1908
2029
 
1909
2030
  // Distribution results collector (#104)
@@ -2260,12 +2381,14 @@ export async function releasePrerelease({ repoPath, track, notes, dryRun, noPubl
2260
2381
  execFileSync('git', ['tag', `v${newVersion}`], { cwd: repoPath, stdio: 'pipe' });
2261
2382
  console.log(` \u2713 Committed and tagged v${newVersion}`);
2262
2383
 
2263
- // 4. Push commit + tag
2264
- try {
2265
- execSync('git push && git push --tags', { cwd: repoPath, stdio: 'pipe' });
2266
- console.log(` \u2713 Pushed to remote`);
2267
- } catch {
2268
- console.log(` ! Push failed. Push manually.`);
2384
+ // 4. Push commit + tag (with auto-PR fallback on protected main, Phase 4)
2385
+ {
2386
+ const pushResult = pushReleaseWithAutoPr(repoPath, newVersion, track);
2387
+ if (pushResult.ok) {
2388
+ console.log(` \u2713 Pushed to remote (${pushResult.via})`);
2389
+ } else {
2390
+ logPushFailure(pushResult, `v${newVersion}`);
2391
+ }
2269
2392
  }
2270
2393
 
2271
2394
  const distResults = [];
@@ -2470,12 +2593,14 @@ export async function releaseHotfix({ repoPath, notes, notesSource, dryRun, noPu
2470
2593
  gitCommitAndTag(repoPath, newVersion, notes);
2471
2594
  console.log(` \u2713 Committed and tagged v${newVersion}`);
2472
2595
 
2473
- // 5. Push commit + tag
2474
- try {
2475
- execSync('git push && git push --tags', { cwd: repoPath, stdio: 'pipe' });
2476
- console.log(` \u2713 Pushed to remote`);
2477
- } catch {
2478
- console.log(` ! Push failed. Push manually.`);
2596
+ // 5. Push commit + tag (with auto-PR fallback on protected main, Phase 4)
2597
+ {
2598
+ const pushResult = pushReleaseWithAutoPr(repoPath, newVersion, 'hotfix');
2599
+ if (pushResult.ok) {
2600
+ console.log(` \u2713 Pushed to remote (${pushResult.via})`);
2601
+ } else {
2602
+ logPushFailure(pushResult, `v${newVersion}`);
2603
+ }
2479
2604
  }
2480
2605
 
2481
2606
  const distResults = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.72",
3
+ "version": "1.9.73",
4
4
  "type": "module",
5
5
  "description": "One-command release pipeline. Bumps version, updates changelog + SKILL.md, publishes to npm + GitHub.",
6
6
  "main": "core.mjs",