@qlover/fe-release 5.0.4 → 5.0.6

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/dist/cli.cjs CHANGED
@@ -28,7 +28,7 @@ var import_commander = require("commander");
28
28
 
29
29
  // package.json
30
30
  var description = "A tool for releasing front-end projects, supporting multiple release modes and configurations, simplifying the release process and improving efficiency.";
31
- var version = "5.0.4";
31
+ var version = "5.0.6";
32
32
 
33
33
  // src/cli.ts
34
34
  var import_semver = __toESM(require("semver"), 1);
@@ -69,16 +69,23 @@ var releaseJson = {
69
69
  autoMergeReleasePR: false,
70
70
  pushChangeLabels: false,
71
71
  skipCreateReleasePR: false,
72
- PRTitle: "Release ${env} ${pkgName} ${tagName}",
72
+ /** Used when releasing 1–2 packages */
73
+ PRTitle: "Release ${spaces}",
74
+ /** Used when releasing more than 2 packages */
75
+ PRTitleMany: "Release ${count} packages: ${spaces}",
73
76
  PRBody: "## Changelog\n\n${changelog}",
74
- batchPRBody: "\n## ${name} ${newVersion}\n${changelog}\n",
77
+ /**
78
+ * Per-workspace section in the PR body (single and multi package).
79
+ * `${changeTypeSection}` is Patch/Minor/Major Changes from increment.
80
+ */
81
+ batchPRBody: "\n### ${name}@${newVersion}\n\n#### ${changeTypeSection}\n\n${changelog}\n",
75
82
  branchName: "release/${repoName}-${releaseId}",
76
83
  releaseTagName: "release-tag-${count}-patch-${releaseId}",
77
84
  commitMessage: "chore(release): ${spaces}",
78
85
  label: {
79
86
  name: "CI-Release",
80
87
  color: "1A7F37",
81
- description: "Release PR"
88
+ description: "Automated release PR (merge to publish)"
82
89
  }
83
90
  },
84
91
  changesetVersion: {
@@ -585,6 +592,9 @@ function workspaceVersionSummary(workspace) {
585
592
  function shouldProcessWorkspace(workspace, ignoreNonUpdatedPackages) {
586
593
  return !(ignoreNonUpdatedPackages && workspace.dependencyRelease);
587
594
  }
595
+ function isPrivateWorkspace(workspace) {
596
+ return workspace.packageJson?.private === true;
597
+ }
588
598
  function readJson(path) {
589
599
  const packageJsonContent = (0, import_fs.readFileSync)(path, "utf-8");
590
600
  return JSON.parse(packageJsonContent);
@@ -1726,9 +1736,18 @@ ${workspaces?.map((workspace) => workspace.toString()).join("\n")}`
1726
1736
  this.logDryRun("Would run: changeset publish, publish packages and tags");
1727
1737
  const preview = this.mergeWorkspaces(workspaces);
1728
1738
  this.context.setWorkspaces(preview);
1729
- const previewTags = preview.filter(
1730
- (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && workspace.tagName
1739
+ const previewTags = this.collectPushableReleaseTags(preview);
1740
+ const privateTags = preview.filter(
1741
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1731
1742
  ).map((workspace) => workspace.tagName);
1743
+ if (privateTags.length > 0) {
1744
+ this.logDryRun(
1745
+ `Would create local tag(s) for private package(s):
1746
+ ${[
1747
+ ...new Set(privateTags)
1748
+ ].map((tag) => ` ${tag}`).join("\n")}`
1749
+ );
1750
+ }
1732
1751
  this.logDryRun(
1733
1752
  previewTags.length === 0 ? "Would push release tag(s) from workspace.tagName: none" : `Would push release tag(s) from workspace.tagName:
1734
1753
  ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
@@ -1742,37 +1761,106 @@ ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
1742
1761
  if (workspaces.length > 0) {
1743
1762
  this.syncWorkspaces(workspaces);
1744
1763
  }
1764
+ const syncedWorkspaces = this.context.workspaces ?? workspaces;
1765
+ await this.step({
1766
+ label: "Create Private Package Tags",
1767
+ task: () => this.ensurePrivateReleaseTags(syncedWorkspaces)
1768
+ });
1745
1769
  await this.step({
1746
1770
  label: "Push Release Tags",
1747
- task: () => this.pushWorkspaceReleaseTags(this.context.workspaces ?? workspaces)
1771
+ task: () => this.pushWorkspaceReleaseTags(syncedWorkspaces)
1748
1772
  });
1749
1773
  }
1750
1774
  /**
1751
- * Push release tags from synced workspaces' `tagName` fields.
1775
+ * Collect tag names that should be pushed after `changeset publish`.
1752
1776
  *
1753
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
1754
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1777
+ * Includes public packages (tagged by changesets) and private packages
1778
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1779
+ * workspaces.
1755
1780
  */
1756
- async pushWorkspaceReleaseTags(workspaces) {
1781
+ collectPushableReleaseTags(workspaces) {
1757
1782
  const tags = workspaces.filter(
1758
1783
  (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && !!workspace.tagName
1759
1784
  ).map((workspace) => workspace.tagName);
1760
- const uniqueTags = [...new Set(tags)];
1785
+ return [...new Set(tags)];
1786
+ }
1787
+ /**
1788
+ * List local git tag names (short refs under `refs/tags/`).
1789
+ */
1790
+ async listLocalTags() {
1791
+ const output = await this.shell.exec(
1792
+ 'git for-each-ref --format="%(refname:short)" refs/tags/',
1793
+ { dryRun: false, silent: true }
1794
+ );
1795
+ const tags = output.split("\n").map((line) => line.trim()).filter(Boolean);
1796
+ return new Set(tags);
1797
+ }
1798
+ /**
1799
+ * Create local git tags for private packages after `changeset publish`.
1800
+ *
1801
+ * Changesets skips npm publish and tagging for `private: true` packages.
1802
+ * Release still needs those tags as changelog baselines, so create them here.
1803
+ */
1804
+ async ensurePrivateReleaseTags(workspaces) {
1805
+ const privateWorkspaces = workspaces.filter(
1806
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1807
+ );
1808
+ if (privateWorkspaces.length === 0) {
1809
+ return;
1810
+ }
1811
+ const localTags = await this.listLocalTags();
1812
+ for (const workspace of privateWorkspaces) {
1813
+ const tagName = workspace.tagName;
1814
+ if (localTags.has(tagName)) {
1815
+ this.logger.debug(`Private package tag already exists: ${tagName}`);
1816
+ continue;
1817
+ }
1818
+ this.logger.info(
1819
+ `Create local release tag for private package: ${tagName}`
1820
+ );
1821
+ await this.shell.exec(["git", "tag", tagName]);
1822
+ localTags.add(tagName);
1823
+ }
1824
+ }
1825
+ /**
1826
+ * Push release tags from synced workspaces' `tagName` fields.
1827
+ *
1828
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1829
+ * tags that exist as local refs. Missing public-package tags are skipped
1830
+ * (changeset may have skipped publish); they are not invented here.
1831
+ */
1832
+ async pushWorkspaceReleaseTags(workspaces) {
1833
+ const uniqueTags = this.collectPushableReleaseTags(workspaces);
1761
1834
  if (uniqueTags.length === 0) {
1762
1835
  this.logger.warn(
1763
1836
  "No workspace.tagName found after publish sync; skip tag push"
1764
1837
  );
1765
1838
  return;
1766
1839
  }
1840
+ const localTags = await this.listLocalTags();
1841
+ const existingTags = uniqueTags.filter((tag) => localTags.has(tag));
1842
+ const missingTags = uniqueTags.filter((tag) => !localTags.has(tag));
1843
+ if (missingTags.length > 0) {
1844
+ this.logger.warn(
1845
+ `Skip push for missing local tag(s):
1846
+ ${missingTags.map((tag) => ` ${tag}`).join("\n")}`
1847
+ );
1848
+ }
1849
+ if (existingTags.length === 0) {
1850
+ this.logger.warn(
1851
+ "No local release tags found to push after publish; skip tag push"
1852
+ );
1853
+ return;
1854
+ }
1767
1855
  this.logger.info(
1768
- `Pushing ${uniqueTags.length} release tag(s) to origin:
1769
- ${uniqueTags.map((tag) => ` ${tag}`).join("\n")}`
1856
+ `Pushing ${existingTags.length} release tag(s) to origin:
1857
+ ${existingTags.map((tag) => ` ${tag}`).join("\n")}`
1770
1858
  );
1771
1859
  await this.shell.exec([
1772
1860
  "git",
1773
1861
  "push",
1774
1862
  "origin",
1775
- ...uniqueTags.map((tag) => `"refs/tags/${tag}"`)
1863
+ ...existingTags.map((tag) => `"refs/tags/${tag}"`)
1776
1864
  ]);
1777
1865
  }
1778
1866
  syncWorkspaces(workspaces) {
@@ -3375,23 +3463,33 @@ var ReleaseFormatter = class {
3375
3463
  if (workspaces.length === 0) {
3376
3464
  return "";
3377
3465
  }
3378
- if (workspaces.length === 1) {
3379
- return workspaces[0].changelog || "";
3380
- }
3466
+ const changeTypeSection = this.getChangeTypeSection();
3467
+ const batchTpl = this.config.batchPRBody || releaseJson.github.batchPRBody;
3381
3468
  return workspaces.map(
3382
- (workspace) => this.templateEngine.render(
3383
- this.config.batchPRBody || releaseJson.github.batchPRBody,
3384
- {
3385
- ...workspace,
3386
- // Prefer bumped version in PR changelog section headers
3387
- version: workspace.newVersion || workspace.version,
3388
- newVersion: workspace.newVersion || workspace.version
3389
- }
3390
- )
3469
+ (workspace) => this.templateEngine.render(batchTpl, {
3470
+ ...workspace,
3471
+ // Prefer bumped version in PR changelog section headers
3472
+ version: workspace.newVersion || workspace.version,
3473
+ newVersion: workspace.newVersion || workspace.version,
3474
+ changeTypeSection
3475
+ })
3391
3476
  ).join("\n");
3392
3477
  }
3478
+ /**
3479
+ * Map semver increment to Changesets-style section title.
3480
+ */
3481
+ getChangeTypeSection() {
3482
+ const increment = (this.config.increment || "patch").toLowerCase();
3483
+ if (increment === "major") {
3484
+ return "Major Changes";
3485
+ }
3486
+ if (increment === "minor") {
3487
+ return "Minor Changes";
3488
+ }
3489
+ return "Patch Changes";
3490
+ }
3393
3491
  formatPRTagName(workspaces, releaseBranchResult) {
3394
- if (workspaces.length <= 1) {
3492
+ if (workspaces.length === 0) {
3395
3493
  return releaseBranchResult.releaseTagName;
3396
3494
  }
3397
3495
  return workspaces.map(
@@ -3399,7 +3497,8 @@ var ReleaseFormatter = class {
3399
3497
  ).join(" ");
3400
3498
  }
3401
3499
  getPRTitle(releaseBranchResult, context, workspaces = []) {
3402
- const prTitleTpl = this.config.PRTitle || releaseJson.github.PRTitle;
3500
+ const count = workspaces.length;
3501
+ const prTitleTpl = count > 2 ? this.config.PRTitleMany || this.config.PRTitle || releaseJson.github.PRTitleMany : this.config.PRTitle || releaseJson.github.PRTitle;
3403
3502
  return this.templateEngine.render(
3404
3503
  prTitleTpl,
3405
3504
  this.buildPRTemplateVars(workspaces, releaseBranchResult, context)
@@ -3437,7 +3536,8 @@ var Github = class extends GitBase {
3437
3536
  repoName: this.context.options.repoName,
3438
3537
  authorName: this.context.options.authorName,
3439
3538
  env: this.context.options.releaseEnv,
3440
- releaseId: this.context.releaseId
3539
+ releaseId: this.context.releaseId,
3540
+ increment: this.context.parameters.changesetVersion?.increment || releaseJson.changesetVersion.increment
3441
3541
  });
3442
3542
  this.logger.info(`Github mode: ${this.mode}`);
3443
3543
  }
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { Command } from "commander";
5
5
 
6
6
  // package.json
7
7
  var description = "A tool for releasing front-end projects, supporting multiple release modes and configurations, simplifying the release process and improving efficiency.";
8
- var version = "5.0.4";
8
+ var version = "5.0.6";
9
9
 
10
10
  // src/cli.ts
11
11
  import semver from "semver";
@@ -46,16 +46,23 @@ var releaseJson = {
46
46
  autoMergeReleasePR: false,
47
47
  pushChangeLabels: false,
48
48
  skipCreateReleasePR: false,
49
- PRTitle: "Release ${env} ${pkgName} ${tagName}",
49
+ /** Used when releasing 1–2 packages */
50
+ PRTitle: "Release ${spaces}",
51
+ /** Used when releasing more than 2 packages */
52
+ PRTitleMany: "Release ${count} packages: ${spaces}",
50
53
  PRBody: "## Changelog\n\n${changelog}",
51
- batchPRBody: "\n## ${name} ${newVersion}\n${changelog}\n",
54
+ /**
55
+ * Per-workspace section in the PR body (single and multi package).
56
+ * `${changeTypeSection}` is Patch/Minor/Major Changes from increment.
57
+ */
58
+ batchPRBody: "\n### ${name}@${newVersion}\n\n#### ${changeTypeSection}\n\n${changelog}\n",
52
59
  branchName: "release/${repoName}-${releaseId}",
53
60
  releaseTagName: "release-tag-${count}-patch-${releaseId}",
54
61
  commitMessage: "chore(release): ${spaces}",
55
62
  label: {
56
63
  name: "CI-Release",
57
64
  color: "1A7F37",
58
- description: "Release PR"
65
+ description: "Automated release PR (merge to publish)"
59
66
  }
60
67
  },
61
68
  changesetVersion: {
@@ -565,6 +572,9 @@ function workspaceVersionSummary(workspace) {
565
572
  function shouldProcessWorkspace(workspace, ignoreNonUpdatedPackages) {
566
573
  return !(ignoreNonUpdatedPackages && workspace.dependencyRelease);
567
574
  }
575
+ function isPrivateWorkspace(workspace) {
576
+ return workspace.packageJson?.private === true;
577
+ }
568
578
  function readJson(path) {
569
579
  const packageJsonContent = readFileSync(path, "utf-8");
570
580
  return JSON.parse(packageJsonContent);
@@ -1706,9 +1716,18 @@ ${workspaces?.map((workspace) => workspace.toString()).join("\n")}`
1706
1716
  this.logDryRun("Would run: changeset publish, publish packages and tags");
1707
1717
  const preview = this.mergeWorkspaces(workspaces);
1708
1718
  this.context.setWorkspaces(preview);
1709
- const previewTags = preview.filter(
1710
- (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && workspace.tagName
1719
+ const previewTags = this.collectPushableReleaseTags(preview);
1720
+ const privateTags = preview.filter(
1721
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1711
1722
  ).map((workspace) => workspace.tagName);
1723
+ if (privateTags.length > 0) {
1724
+ this.logDryRun(
1725
+ `Would create local tag(s) for private package(s):
1726
+ ${[
1727
+ ...new Set(privateTags)
1728
+ ].map((tag) => ` ${tag}`).join("\n")}`
1729
+ );
1730
+ }
1712
1731
  this.logDryRun(
1713
1732
  previewTags.length === 0 ? "Would push release tag(s) from workspace.tagName: none" : `Would push release tag(s) from workspace.tagName:
1714
1733
  ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
@@ -1722,37 +1741,106 @@ ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
1722
1741
  if (workspaces.length > 0) {
1723
1742
  this.syncWorkspaces(workspaces);
1724
1743
  }
1744
+ const syncedWorkspaces = this.context.workspaces ?? workspaces;
1745
+ await this.step({
1746
+ label: "Create Private Package Tags",
1747
+ task: () => this.ensurePrivateReleaseTags(syncedWorkspaces)
1748
+ });
1725
1749
  await this.step({
1726
1750
  label: "Push Release Tags",
1727
- task: () => this.pushWorkspaceReleaseTags(this.context.workspaces ?? workspaces)
1751
+ task: () => this.pushWorkspaceReleaseTags(syncedWorkspaces)
1728
1752
  });
1729
1753
  }
1730
1754
  /**
1731
- * Push release tags from synced workspaces' `tagName` fields.
1755
+ * Collect tag names that should be pushed after `changeset publish`.
1732
1756
  *
1733
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
1734
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1757
+ * Includes public packages (tagged by changesets) and private packages
1758
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1759
+ * workspaces.
1735
1760
  */
1736
- async pushWorkspaceReleaseTags(workspaces) {
1761
+ collectPushableReleaseTags(workspaces) {
1737
1762
  const tags = workspaces.filter(
1738
1763
  (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && !!workspace.tagName
1739
1764
  ).map((workspace) => workspace.tagName);
1740
- const uniqueTags = [...new Set(tags)];
1765
+ return [...new Set(tags)];
1766
+ }
1767
+ /**
1768
+ * List local git tag names (short refs under `refs/tags/`).
1769
+ */
1770
+ async listLocalTags() {
1771
+ const output = await this.shell.exec(
1772
+ 'git for-each-ref --format="%(refname:short)" refs/tags/',
1773
+ { dryRun: false, silent: true }
1774
+ );
1775
+ const tags = output.split("\n").map((line) => line.trim()).filter(Boolean);
1776
+ return new Set(tags);
1777
+ }
1778
+ /**
1779
+ * Create local git tags for private packages after `changeset publish`.
1780
+ *
1781
+ * Changesets skips npm publish and tagging for `private: true` packages.
1782
+ * Release still needs those tags as changelog baselines, so create them here.
1783
+ */
1784
+ async ensurePrivateReleaseTags(workspaces) {
1785
+ const privateWorkspaces = workspaces.filter(
1786
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1787
+ );
1788
+ if (privateWorkspaces.length === 0) {
1789
+ return;
1790
+ }
1791
+ const localTags = await this.listLocalTags();
1792
+ for (const workspace of privateWorkspaces) {
1793
+ const tagName = workspace.tagName;
1794
+ if (localTags.has(tagName)) {
1795
+ this.logger.debug(`Private package tag already exists: ${tagName}`);
1796
+ continue;
1797
+ }
1798
+ this.logger.info(
1799
+ `Create local release tag for private package: ${tagName}`
1800
+ );
1801
+ await this.shell.exec(["git", "tag", tagName]);
1802
+ localTags.add(tagName);
1803
+ }
1804
+ }
1805
+ /**
1806
+ * Push release tags from synced workspaces' `tagName` fields.
1807
+ *
1808
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1809
+ * tags that exist as local refs. Missing public-package tags are skipped
1810
+ * (changeset may have skipped publish); they are not invented here.
1811
+ */
1812
+ async pushWorkspaceReleaseTags(workspaces) {
1813
+ const uniqueTags = this.collectPushableReleaseTags(workspaces);
1741
1814
  if (uniqueTags.length === 0) {
1742
1815
  this.logger.warn(
1743
1816
  "No workspace.tagName found after publish sync; skip tag push"
1744
1817
  );
1745
1818
  return;
1746
1819
  }
1820
+ const localTags = await this.listLocalTags();
1821
+ const existingTags = uniqueTags.filter((tag) => localTags.has(tag));
1822
+ const missingTags = uniqueTags.filter((tag) => !localTags.has(tag));
1823
+ if (missingTags.length > 0) {
1824
+ this.logger.warn(
1825
+ `Skip push for missing local tag(s):
1826
+ ${missingTags.map((tag) => ` ${tag}`).join("\n")}`
1827
+ );
1828
+ }
1829
+ if (existingTags.length === 0) {
1830
+ this.logger.warn(
1831
+ "No local release tags found to push after publish; skip tag push"
1832
+ );
1833
+ return;
1834
+ }
1747
1835
  this.logger.info(
1748
- `Pushing ${uniqueTags.length} release tag(s) to origin:
1749
- ${uniqueTags.map((tag) => ` ${tag}`).join("\n")}`
1836
+ `Pushing ${existingTags.length} release tag(s) to origin:
1837
+ ${existingTags.map((tag) => ` ${tag}`).join("\n")}`
1750
1838
  );
1751
1839
  await this.shell.exec([
1752
1840
  "git",
1753
1841
  "push",
1754
1842
  "origin",
1755
- ...uniqueTags.map((tag) => `"refs/tags/${tag}"`)
1843
+ ...existingTags.map((tag) => `"refs/tags/${tag}"`)
1756
1844
  ]);
1757
1845
  }
1758
1846
  syncWorkspaces(workspaces) {
@@ -3355,23 +3443,33 @@ var ReleaseFormatter = class {
3355
3443
  if (workspaces.length === 0) {
3356
3444
  return "";
3357
3445
  }
3358
- if (workspaces.length === 1) {
3359
- return workspaces[0].changelog || "";
3360
- }
3446
+ const changeTypeSection = this.getChangeTypeSection();
3447
+ const batchTpl = this.config.batchPRBody || releaseJson.github.batchPRBody;
3361
3448
  return workspaces.map(
3362
- (workspace) => this.templateEngine.render(
3363
- this.config.batchPRBody || releaseJson.github.batchPRBody,
3364
- {
3365
- ...workspace,
3366
- // Prefer bumped version in PR changelog section headers
3367
- version: workspace.newVersion || workspace.version,
3368
- newVersion: workspace.newVersion || workspace.version
3369
- }
3370
- )
3449
+ (workspace) => this.templateEngine.render(batchTpl, {
3450
+ ...workspace,
3451
+ // Prefer bumped version in PR changelog section headers
3452
+ version: workspace.newVersion || workspace.version,
3453
+ newVersion: workspace.newVersion || workspace.version,
3454
+ changeTypeSection
3455
+ })
3371
3456
  ).join("\n");
3372
3457
  }
3458
+ /**
3459
+ * Map semver increment to Changesets-style section title.
3460
+ */
3461
+ getChangeTypeSection() {
3462
+ const increment = (this.config.increment || "patch").toLowerCase();
3463
+ if (increment === "major") {
3464
+ return "Major Changes";
3465
+ }
3466
+ if (increment === "minor") {
3467
+ return "Minor Changes";
3468
+ }
3469
+ return "Patch Changes";
3470
+ }
3373
3471
  formatPRTagName(workspaces, releaseBranchResult) {
3374
- if (workspaces.length <= 1) {
3472
+ if (workspaces.length === 0) {
3375
3473
  return releaseBranchResult.releaseTagName;
3376
3474
  }
3377
3475
  return workspaces.map(
@@ -3379,7 +3477,8 @@ var ReleaseFormatter = class {
3379
3477
  ).join(" ");
3380
3478
  }
3381
3479
  getPRTitle(releaseBranchResult, context, workspaces = []) {
3382
- const prTitleTpl = this.config.PRTitle || releaseJson.github.PRTitle;
3480
+ const count = workspaces.length;
3481
+ const prTitleTpl = count > 2 ? this.config.PRTitleMany || this.config.PRTitle || releaseJson.github.PRTitleMany : this.config.PRTitle || releaseJson.github.PRTitle;
3383
3482
  return this.templateEngine.render(
3384
3483
  prTitleTpl,
3385
3484
  this.buildPRTemplateVars(workspaces, releaseBranchResult, context)
@@ -3417,7 +3516,8 @@ var Github = class extends GitBase {
3417
3516
  repoName: this.context.options.repoName,
3418
3517
  authorName: this.context.options.authorName,
3419
3518
  env: this.context.options.releaseEnv,
3420
- releaseId: this.context.releaseId
3519
+ releaseId: this.context.releaseId,
3520
+ increment: this.context.parameters.changesetVersion?.increment || releaseJson.changesetVersion.increment
3421
3521
  });
3422
3522
  this.logger.info(`Github mode: ${this.mode}`);
3423
3523
  }
package/dist/index.cjs CHANGED
@@ -66,16 +66,23 @@ var releaseJson = {
66
66
  autoMergeReleasePR: false,
67
67
  pushChangeLabels: false,
68
68
  skipCreateReleasePR: false,
69
- PRTitle: "Release ${env} ${pkgName} ${tagName}",
69
+ /** Used when releasing 1–2 packages */
70
+ PRTitle: "Release ${spaces}",
71
+ /** Used when releasing more than 2 packages */
72
+ PRTitleMany: "Release ${count} packages: ${spaces}",
70
73
  PRBody: "## Changelog\n\n${changelog}",
71
- batchPRBody: "\n## ${name} ${newVersion}\n${changelog}\n",
74
+ /**
75
+ * Per-workspace section in the PR body (single and multi package).
76
+ * `${changeTypeSection}` is Patch/Minor/Major Changes from increment.
77
+ */
78
+ batchPRBody: "\n### ${name}@${newVersion}\n\n#### ${changeTypeSection}\n\n${changelog}\n",
72
79
  branchName: "release/${repoName}-${releaseId}",
73
80
  releaseTagName: "release-tag-${count}-patch-${releaseId}",
74
81
  commitMessage: "chore(release): ${spaces}",
75
82
  label: {
76
83
  name: "CI-Release",
77
84
  color: "1A7F37",
78
- description: "Release PR"
85
+ description: "Automated release PR (merge to publish)"
79
86
  }
80
87
  },
81
88
  changesetVersion: {
@@ -590,6 +597,9 @@ function workspaceVersionSummary(workspace) {
590
597
  function shouldProcessWorkspace(workspace, ignoreNonUpdatedPackages) {
591
598
  return !(ignoreNonUpdatedPackages && workspace.dependencyRelease);
592
599
  }
600
+ function isPrivateWorkspace(workspace) {
601
+ return workspace.packageJson?.private === true;
602
+ }
593
603
  function readJson(path) {
594
604
  const packageJsonContent = (0, import_fs.readFileSync)(path, "utf-8");
595
605
  return JSON.parse(packageJsonContent);
@@ -1731,9 +1741,18 @@ ${workspaces?.map((workspace) => workspace.toString()).join("\n")}`
1731
1741
  this.logDryRun("Would run: changeset publish, publish packages and tags");
1732
1742
  const preview = this.mergeWorkspaces(workspaces);
1733
1743
  this.context.setWorkspaces(preview);
1734
- const previewTags = preview.filter(
1735
- (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && workspace.tagName
1744
+ const previewTags = this.collectPushableReleaseTags(preview);
1745
+ const privateTags = preview.filter(
1746
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1736
1747
  ).map((workspace) => workspace.tagName);
1748
+ if (privateTags.length > 0) {
1749
+ this.logDryRun(
1750
+ `Would create local tag(s) for private package(s):
1751
+ ${[
1752
+ ...new Set(privateTags)
1753
+ ].map((tag) => ` ${tag}`).join("\n")}`
1754
+ );
1755
+ }
1737
1756
  this.logDryRun(
1738
1757
  previewTags.length === 0 ? "Would push release tag(s) from workspace.tagName: none" : `Would push release tag(s) from workspace.tagName:
1739
1758
  ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
@@ -1747,37 +1766,106 @@ ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
1747
1766
  if (workspaces.length > 0) {
1748
1767
  this.syncWorkspaces(workspaces);
1749
1768
  }
1769
+ const syncedWorkspaces = this.context.workspaces ?? workspaces;
1770
+ await this.step({
1771
+ label: "Create Private Package Tags",
1772
+ task: () => this.ensurePrivateReleaseTags(syncedWorkspaces)
1773
+ });
1750
1774
  await this.step({
1751
1775
  label: "Push Release Tags",
1752
- task: () => this.pushWorkspaceReleaseTags(this.context.workspaces ?? workspaces)
1776
+ task: () => this.pushWorkspaceReleaseTags(syncedWorkspaces)
1753
1777
  });
1754
1778
  }
1755
1779
  /**
1756
- * Push release tags from synced workspaces' `tagName` fields.
1780
+ * Collect tag names that should be pushed after `changeset publish`.
1757
1781
  *
1758
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
1759
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1782
+ * Includes public packages (tagged by changesets) and private packages
1783
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1784
+ * workspaces.
1760
1785
  */
1761
- async pushWorkspaceReleaseTags(workspaces) {
1786
+ collectPushableReleaseTags(workspaces) {
1762
1787
  const tags = workspaces.filter(
1763
1788
  (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && !!workspace.tagName
1764
1789
  ).map((workspace) => workspace.tagName);
1765
- const uniqueTags = [...new Set(tags)];
1790
+ return [...new Set(tags)];
1791
+ }
1792
+ /**
1793
+ * List local git tag names (short refs under `refs/tags/`).
1794
+ */
1795
+ async listLocalTags() {
1796
+ const output = await this.shell.exec(
1797
+ 'git for-each-ref --format="%(refname:short)" refs/tags/',
1798
+ { dryRun: false, silent: true }
1799
+ );
1800
+ const tags = output.split("\n").map((line) => line.trim()).filter(Boolean);
1801
+ return new Set(tags);
1802
+ }
1803
+ /**
1804
+ * Create local git tags for private packages after `changeset publish`.
1805
+ *
1806
+ * Changesets skips npm publish and tagging for `private: true` packages.
1807
+ * Release still needs those tags as changelog baselines, so create them here.
1808
+ */
1809
+ async ensurePrivateReleaseTags(workspaces) {
1810
+ const privateWorkspaces = workspaces.filter(
1811
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1812
+ );
1813
+ if (privateWorkspaces.length === 0) {
1814
+ return;
1815
+ }
1816
+ const localTags = await this.listLocalTags();
1817
+ for (const workspace of privateWorkspaces) {
1818
+ const tagName = workspace.tagName;
1819
+ if (localTags.has(tagName)) {
1820
+ this.logger.debug(`Private package tag already exists: ${tagName}`);
1821
+ continue;
1822
+ }
1823
+ this.logger.info(
1824
+ `Create local release tag for private package: ${tagName}`
1825
+ );
1826
+ await this.shell.exec(["git", "tag", tagName]);
1827
+ localTags.add(tagName);
1828
+ }
1829
+ }
1830
+ /**
1831
+ * Push release tags from synced workspaces' `tagName` fields.
1832
+ *
1833
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1834
+ * tags that exist as local refs. Missing public-package tags are skipped
1835
+ * (changeset may have skipped publish); they are not invented here.
1836
+ */
1837
+ async pushWorkspaceReleaseTags(workspaces) {
1838
+ const uniqueTags = this.collectPushableReleaseTags(workspaces);
1766
1839
  if (uniqueTags.length === 0) {
1767
1840
  this.logger.warn(
1768
1841
  "No workspace.tagName found after publish sync; skip tag push"
1769
1842
  );
1770
1843
  return;
1771
1844
  }
1845
+ const localTags = await this.listLocalTags();
1846
+ const existingTags = uniqueTags.filter((tag) => localTags.has(tag));
1847
+ const missingTags = uniqueTags.filter((tag) => !localTags.has(tag));
1848
+ if (missingTags.length > 0) {
1849
+ this.logger.warn(
1850
+ `Skip push for missing local tag(s):
1851
+ ${missingTags.map((tag) => ` ${tag}`).join("\n")}`
1852
+ );
1853
+ }
1854
+ if (existingTags.length === 0) {
1855
+ this.logger.warn(
1856
+ "No local release tags found to push after publish; skip tag push"
1857
+ );
1858
+ return;
1859
+ }
1772
1860
  this.logger.info(
1773
- `Pushing ${uniqueTags.length} release tag(s) to origin:
1774
- ${uniqueTags.map((tag) => ` ${tag}`).join("\n")}`
1861
+ `Pushing ${existingTags.length} release tag(s) to origin:
1862
+ ${existingTags.map((tag) => ` ${tag}`).join("\n")}`
1775
1863
  );
1776
1864
  await this.shell.exec([
1777
1865
  "git",
1778
1866
  "push",
1779
1867
  "origin",
1780
- ...uniqueTags.map((tag) => `"refs/tags/${tag}"`)
1868
+ ...existingTags.map((tag) => `"refs/tags/${tag}"`)
1781
1869
  ]);
1782
1870
  }
1783
1871
  syncWorkspaces(workspaces) {
@@ -3380,23 +3468,33 @@ var ReleaseFormatter = class {
3380
3468
  if (workspaces.length === 0) {
3381
3469
  return "";
3382
3470
  }
3383
- if (workspaces.length === 1) {
3384
- return workspaces[0].changelog || "";
3385
- }
3471
+ const changeTypeSection = this.getChangeTypeSection();
3472
+ const batchTpl = this.config.batchPRBody || releaseJson.github.batchPRBody;
3386
3473
  return workspaces.map(
3387
- (workspace) => this.templateEngine.render(
3388
- this.config.batchPRBody || releaseJson.github.batchPRBody,
3389
- {
3390
- ...workspace,
3391
- // Prefer bumped version in PR changelog section headers
3392
- version: workspace.newVersion || workspace.version,
3393
- newVersion: workspace.newVersion || workspace.version
3394
- }
3395
- )
3474
+ (workspace) => this.templateEngine.render(batchTpl, {
3475
+ ...workspace,
3476
+ // Prefer bumped version in PR changelog section headers
3477
+ version: workspace.newVersion || workspace.version,
3478
+ newVersion: workspace.newVersion || workspace.version,
3479
+ changeTypeSection
3480
+ })
3396
3481
  ).join("\n");
3397
3482
  }
3483
+ /**
3484
+ * Map semver increment to Changesets-style section title.
3485
+ */
3486
+ getChangeTypeSection() {
3487
+ const increment = (this.config.increment || "patch").toLowerCase();
3488
+ if (increment === "major") {
3489
+ return "Major Changes";
3490
+ }
3491
+ if (increment === "minor") {
3492
+ return "Minor Changes";
3493
+ }
3494
+ return "Patch Changes";
3495
+ }
3398
3496
  formatPRTagName(workspaces, releaseBranchResult) {
3399
- if (workspaces.length <= 1) {
3497
+ if (workspaces.length === 0) {
3400
3498
  return releaseBranchResult.releaseTagName;
3401
3499
  }
3402
3500
  return workspaces.map(
@@ -3404,7 +3502,8 @@ var ReleaseFormatter = class {
3404
3502
  ).join(" ");
3405
3503
  }
3406
3504
  getPRTitle(releaseBranchResult, context, workspaces = []) {
3407
- const prTitleTpl = this.config.PRTitle || releaseJson.github.PRTitle;
3505
+ const count = workspaces.length;
3506
+ const prTitleTpl = count > 2 ? this.config.PRTitleMany || this.config.PRTitle || releaseJson.github.PRTitleMany : this.config.PRTitle || releaseJson.github.PRTitle;
3408
3507
  return this.templateEngine.render(
3409
3508
  prTitleTpl,
3410
3509
  this.buildPRTemplateVars(workspaces, releaseBranchResult, context)
@@ -3442,7 +3541,8 @@ var Github = class extends GitBase {
3442
3541
  repoName: this.context.options.repoName,
3443
3542
  authorName: this.context.options.authorName,
3444
3543
  env: this.context.options.releaseEnv,
3445
- releaseId: this.context.releaseId
3544
+ releaseId: this.context.releaseId,
3545
+ increment: this.context.parameters.changesetVersion?.increment || releaseJson.changesetVersion.increment
3446
3546
  });
3447
3547
  this.logger.info(`Github mode: ${this.mode}`);
3448
3548
  }
package/dist/index.d.ts CHANGED
@@ -216,11 +216,19 @@ interface ReleaseFormatterConfig {
216
216
  more: string;
217
217
  };
218
218
  /**
219
- * Pull request title template
219
+ * Pull request title template (1–2 packages)
220
220
  *
221
- * @default {@link DEFAULT_PR_TITLE}
221
+ * @default {@link releaseJson.github.PRTitle}
222
222
  */
223
223
  PRTitle?: string;
224
+ /**
225
+ * Pull request title template when releasing more than 2 packages
226
+ *
227
+ * Falls back to {@link PRTitle} when unset.
228
+ *
229
+ * @default {@link releaseJson.github.PRTitleMany}
230
+ */
231
+ PRTitleMany?: string;
224
232
  /**
225
233
  * Pull request body template
226
234
  *
@@ -228,11 +236,19 @@ interface ReleaseFormatterConfig {
228
236
  */
229
237
  PRBody?: string;
230
238
  /**
231
- * Template for each workspace section in a multi-workspace PR body
239
+ * Template for each workspace section in the PR body
240
+ *
241
+ * Used for both single- and multi-workspace releases.
242
+ * Supports `${changeTypeSection}` (Patch/Minor/Major Changes).
232
243
  *
233
244
  * @default from release.json
234
245
  */
235
246
  batchPRBody?: string;
247
+ /**
248
+ * Semver increment used to title changelog sections (patch | minor | major)
249
+ * @default 'patch'
250
+ */
251
+ increment?: string;
236
252
  }
237
253
 
238
254
  /**
@@ -840,9 +856,10 @@ interface ChangeLogInterface {
840
856
  * - **onSuccess**: run version and/or publish flow based on `mode`
841
857
  *
842
858
  * Publish flow (`mode: 'publish'` or second half of `'both'`):
843
- * 1. Run `changeset publish` (npm publish + local `git tag` only)
859
+ * 1. Run `changeset publish` (npm publish + local `git tag` for public packages)
844
860
  * 2. Sync workspaces from disk so `tagName` is set from the release version
845
- * 3. Push `workspace.tagName` refs to `origin`
861
+ * 3. Create local tags for `private` packages (changesets skips tagging them)
862
+ * 4. Push `workspace.tagName` refs that exist locally to `origin`
846
863
  *
847
864
  * Version flow (`mode: 'version'` or first half of `'both'`):
848
865
  * 1. Write `.changeset/*.md` files for directly changed packages only
@@ -976,11 +993,31 @@ declare class ChangesetVersion extends ScriptPlugin<ReleaseContext, ChangesetVer
976
993
  protected logDryRun(message: string): void;
977
994
  protected runVersionFlow(): Promise<void>;
978
995
  protected runChangesetPublish(): Promise<void>;
996
+ /**
997
+ * Collect tag names that should be pushed after `changeset publish`.
998
+ *
999
+ * Includes public packages (tagged by changesets) and private packages
1000
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1001
+ * workspaces.
1002
+ */
1003
+ protected collectPushableReleaseTags(workspaces: WorkspaceInterface[]): string[];
1004
+ /**
1005
+ * List local git tag names (short refs under `refs/tags/`).
1006
+ */
1007
+ protected listLocalTags(): Promise<Set<string>>;
1008
+ /**
1009
+ * Create local git tags for private packages after `changeset publish`.
1010
+ *
1011
+ * Changesets skips npm publish and tagging for `private: true` packages.
1012
+ * Release still needs those tags as changelog baselines, so create them here.
1013
+ */
1014
+ protected ensurePrivateReleaseTags(workspaces: WorkspaceInterface[]): Promise<void>;
979
1015
  /**
980
1016
  * Push release tags from synced workspaces' `tagName` fields.
981
1017
  *
982
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
983
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1018
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1019
+ * tags that exist as local refs. Missing public-package tags are skipped
1020
+ * (changeset may have skipped publish); they are not invented here.
984
1021
  */
985
1022
  protected pushWorkspaceReleaseTags(workspaces: WorkspaceInterface[]): Promise<void>;
986
1023
  protected syncWorkspaces(workspaces: WorkspaceInterface[]): void;
@@ -1415,16 +1452,23 @@ declare const releaseJson: {
1415
1452
  readonly autoMergeReleasePR: false;
1416
1453
  readonly pushChangeLabels: false;
1417
1454
  readonly skipCreateReleasePR: false;
1418
- readonly PRTitle: "Release ${env} ${pkgName} ${tagName}";
1455
+ /** Used when releasing 1–2 packages */
1456
+ readonly PRTitle: "Release ${spaces}";
1457
+ /** Used when releasing more than 2 packages */
1458
+ readonly PRTitleMany: "Release ${count} packages: ${spaces}";
1419
1459
  readonly PRBody: "## Changelog\n\n${changelog}";
1420
- readonly batchPRBody: "\n## ${name} ${newVersion}\n${changelog}\n";
1460
+ /**
1461
+ * Per-workspace section in the PR body (single and multi package).
1462
+ * `${changeTypeSection}` is Patch/Minor/Major Changes from increment.
1463
+ */
1464
+ readonly batchPRBody: "\n### ${name}@${newVersion}\n\n#### ${changeTypeSection}\n\n${changelog}\n";
1421
1465
  readonly branchName: "release/${repoName}-${releaseId}";
1422
1466
  readonly releaseTagName: "release-tag-${count}-patch-${releaseId}";
1423
1467
  readonly commitMessage: "chore(release): ${spaces}";
1424
1468
  readonly label: {
1425
1469
  readonly name: "CI-Release";
1426
1470
  readonly color: "1A7F37";
1427
- readonly description: "Release PR";
1471
+ readonly description: "Automated release PR (merge to publish)";
1428
1472
  };
1429
1473
  };
1430
1474
  readonly changesetVersion: {
package/dist/index.js CHANGED
@@ -14,16 +14,23 @@ var releaseJson = {
14
14
  autoMergeReleasePR: false,
15
15
  pushChangeLabels: false,
16
16
  skipCreateReleasePR: false,
17
- PRTitle: "Release ${env} ${pkgName} ${tagName}",
17
+ /** Used when releasing 1–2 packages */
18
+ PRTitle: "Release ${spaces}",
19
+ /** Used when releasing more than 2 packages */
20
+ PRTitleMany: "Release ${count} packages: ${spaces}",
18
21
  PRBody: "## Changelog\n\n${changelog}",
19
- batchPRBody: "\n## ${name} ${newVersion}\n${changelog}\n",
22
+ /**
23
+ * Per-workspace section in the PR body (single and multi package).
24
+ * `${changeTypeSection}` is Patch/Minor/Major Changes from increment.
25
+ */
26
+ batchPRBody: "\n### ${name}@${newVersion}\n\n#### ${changeTypeSection}\n\n${changelog}\n",
20
27
  branchName: "release/${repoName}-${releaseId}",
21
28
  releaseTagName: "release-tag-${count}-patch-${releaseId}",
22
29
  commitMessage: "chore(release): ${spaces}",
23
30
  label: {
24
31
  name: "CI-Release",
25
32
  color: "1A7F37",
26
- description: "Release PR"
33
+ description: "Automated release PR (merge to publish)"
27
34
  }
28
35
  },
29
36
  changesetVersion: {
@@ -541,6 +548,9 @@ function workspaceVersionSummary(workspace) {
541
548
  function shouldProcessWorkspace(workspace, ignoreNonUpdatedPackages) {
542
549
  return !(ignoreNonUpdatedPackages && workspace.dependencyRelease);
543
550
  }
551
+ function isPrivateWorkspace(workspace) {
552
+ return workspace.packageJson?.private === true;
553
+ }
544
554
  function readJson(path) {
545
555
  const packageJsonContent = readFileSync(path, "utf-8");
546
556
  return JSON.parse(packageJsonContent);
@@ -1682,9 +1692,18 @@ ${workspaces?.map((workspace) => workspace.toString()).join("\n")}`
1682
1692
  this.logDryRun("Would run: changeset publish, publish packages and tags");
1683
1693
  const preview = this.mergeWorkspaces(workspaces);
1684
1694
  this.context.setWorkspaces(preview);
1685
- const previewTags = preview.filter(
1686
- (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && workspace.tagName
1695
+ const previewTags = this.collectPushableReleaseTags(preview);
1696
+ const privateTags = preview.filter(
1697
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1687
1698
  ).map((workspace) => workspace.tagName);
1699
+ if (privateTags.length > 0) {
1700
+ this.logDryRun(
1701
+ `Would create local tag(s) for private package(s):
1702
+ ${[
1703
+ ...new Set(privateTags)
1704
+ ].map((tag) => ` ${tag}`).join("\n")}`
1705
+ );
1706
+ }
1688
1707
  this.logDryRun(
1689
1708
  previewTags.length === 0 ? "Would push release tag(s) from workspace.tagName: none" : `Would push release tag(s) from workspace.tagName:
1690
1709
  ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
@@ -1698,37 +1717,106 @@ ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
1698
1717
  if (workspaces.length > 0) {
1699
1718
  this.syncWorkspaces(workspaces);
1700
1719
  }
1720
+ const syncedWorkspaces = this.context.workspaces ?? workspaces;
1721
+ await this.step({
1722
+ label: "Create Private Package Tags",
1723
+ task: () => this.ensurePrivateReleaseTags(syncedWorkspaces)
1724
+ });
1701
1725
  await this.step({
1702
1726
  label: "Push Release Tags",
1703
- task: () => this.pushWorkspaceReleaseTags(this.context.workspaces ?? workspaces)
1727
+ task: () => this.pushWorkspaceReleaseTags(syncedWorkspaces)
1704
1728
  });
1705
1729
  }
1706
1730
  /**
1707
- * Push release tags from synced workspaces' `tagName` fields.
1731
+ * Collect tag names that should be pushed after `changeset publish`.
1708
1732
  *
1709
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
1710
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1733
+ * Includes public packages (tagged by changesets) and private packages
1734
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1735
+ * workspaces.
1711
1736
  */
1712
- async pushWorkspaceReleaseTags(workspaces) {
1737
+ collectPushableReleaseTags(workspaces) {
1713
1738
  const tags = workspaces.filter(
1714
1739
  (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && !!workspace.tagName
1715
1740
  ).map((workspace) => workspace.tagName);
1716
- const uniqueTags = [...new Set(tags)];
1741
+ return [...new Set(tags)];
1742
+ }
1743
+ /**
1744
+ * List local git tag names (short refs under `refs/tags/`).
1745
+ */
1746
+ async listLocalTags() {
1747
+ const output = await this.shell.exec(
1748
+ 'git for-each-ref --format="%(refname:short)" refs/tags/',
1749
+ { dryRun: false, silent: true }
1750
+ );
1751
+ const tags = output.split("\n").map((line) => line.trim()).filter(Boolean);
1752
+ return new Set(tags);
1753
+ }
1754
+ /**
1755
+ * Create local git tags for private packages after `changeset publish`.
1756
+ *
1757
+ * Changesets skips npm publish and tagging for `private: true` packages.
1758
+ * Release still needs those tags as changelog baselines, so create them here.
1759
+ */
1760
+ async ensurePrivateReleaseTags(workspaces) {
1761
+ const privateWorkspaces = workspaces.filter(
1762
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1763
+ );
1764
+ if (privateWorkspaces.length === 0) {
1765
+ return;
1766
+ }
1767
+ const localTags = await this.listLocalTags();
1768
+ for (const workspace of privateWorkspaces) {
1769
+ const tagName = workspace.tagName;
1770
+ if (localTags.has(tagName)) {
1771
+ this.logger.debug(`Private package tag already exists: ${tagName}`);
1772
+ continue;
1773
+ }
1774
+ this.logger.info(
1775
+ `Create local release tag for private package: ${tagName}`
1776
+ );
1777
+ await this.shell.exec(["git", "tag", tagName]);
1778
+ localTags.add(tagName);
1779
+ }
1780
+ }
1781
+ /**
1782
+ * Push release tags from synced workspaces' `tagName` fields.
1783
+ *
1784
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1785
+ * tags that exist as local refs. Missing public-package tags are skipped
1786
+ * (changeset may have skipped publish); they are not invented here.
1787
+ */
1788
+ async pushWorkspaceReleaseTags(workspaces) {
1789
+ const uniqueTags = this.collectPushableReleaseTags(workspaces);
1717
1790
  if (uniqueTags.length === 0) {
1718
1791
  this.logger.warn(
1719
1792
  "No workspace.tagName found after publish sync; skip tag push"
1720
1793
  );
1721
1794
  return;
1722
1795
  }
1796
+ const localTags = await this.listLocalTags();
1797
+ const existingTags = uniqueTags.filter((tag) => localTags.has(tag));
1798
+ const missingTags = uniqueTags.filter((tag) => !localTags.has(tag));
1799
+ if (missingTags.length > 0) {
1800
+ this.logger.warn(
1801
+ `Skip push for missing local tag(s):
1802
+ ${missingTags.map((tag) => ` ${tag}`).join("\n")}`
1803
+ );
1804
+ }
1805
+ if (existingTags.length === 0) {
1806
+ this.logger.warn(
1807
+ "No local release tags found to push after publish; skip tag push"
1808
+ );
1809
+ return;
1810
+ }
1723
1811
  this.logger.info(
1724
- `Pushing ${uniqueTags.length} release tag(s) to origin:
1725
- ${uniqueTags.map((tag) => ` ${tag}`).join("\n")}`
1812
+ `Pushing ${existingTags.length} release tag(s) to origin:
1813
+ ${existingTags.map((tag) => ` ${tag}`).join("\n")}`
1726
1814
  );
1727
1815
  await this.shell.exec([
1728
1816
  "git",
1729
1817
  "push",
1730
1818
  "origin",
1731
- ...uniqueTags.map((tag) => `"refs/tags/${tag}"`)
1819
+ ...existingTags.map((tag) => `"refs/tags/${tag}"`)
1732
1820
  ]);
1733
1821
  }
1734
1822
  syncWorkspaces(workspaces) {
@@ -3331,23 +3419,33 @@ var ReleaseFormatter = class {
3331
3419
  if (workspaces.length === 0) {
3332
3420
  return "";
3333
3421
  }
3334
- if (workspaces.length === 1) {
3335
- return workspaces[0].changelog || "";
3336
- }
3422
+ const changeTypeSection = this.getChangeTypeSection();
3423
+ const batchTpl = this.config.batchPRBody || releaseJson.github.batchPRBody;
3337
3424
  return workspaces.map(
3338
- (workspace) => this.templateEngine.render(
3339
- this.config.batchPRBody || releaseJson.github.batchPRBody,
3340
- {
3341
- ...workspace,
3342
- // Prefer bumped version in PR changelog section headers
3343
- version: workspace.newVersion || workspace.version,
3344
- newVersion: workspace.newVersion || workspace.version
3345
- }
3346
- )
3425
+ (workspace) => this.templateEngine.render(batchTpl, {
3426
+ ...workspace,
3427
+ // Prefer bumped version in PR changelog section headers
3428
+ version: workspace.newVersion || workspace.version,
3429
+ newVersion: workspace.newVersion || workspace.version,
3430
+ changeTypeSection
3431
+ })
3347
3432
  ).join("\n");
3348
3433
  }
3434
+ /**
3435
+ * Map semver increment to Changesets-style section title.
3436
+ */
3437
+ getChangeTypeSection() {
3438
+ const increment = (this.config.increment || "patch").toLowerCase();
3439
+ if (increment === "major") {
3440
+ return "Major Changes";
3441
+ }
3442
+ if (increment === "minor") {
3443
+ return "Minor Changes";
3444
+ }
3445
+ return "Patch Changes";
3446
+ }
3349
3447
  formatPRTagName(workspaces, releaseBranchResult) {
3350
- if (workspaces.length <= 1) {
3448
+ if (workspaces.length === 0) {
3351
3449
  return releaseBranchResult.releaseTagName;
3352
3450
  }
3353
3451
  return workspaces.map(
@@ -3355,7 +3453,8 @@ var ReleaseFormatter = class {
3355
3453
  ).join(" ");
3356
3454
  }
3357
3455
  getPRTitle(releaseBranchResult, context, workspaces = []) {
3358
- const prTitleTpl = this.config.PRTitle || releaseJson.github.PRTitle;
3456
+ const count = workspaces.length;
3457
+ const prTitleTpl = count > 2 ? this.config.PRTitleMany || this.config.PRTitle || releaseJson.github.PRTitleMany : this.config.PRTitle || releaseJson.github.PRTitle;
3359
3458
  return this.templateEngine.render(
3360
3459
  prTitleTpl,
3361
3460
  this.buildPRTemplateVars(workspaces, releaseBranchResult, context)
@@ -3393,7 +3492,8 @@ var Github = class extends GitBase {
3393
3492
  repoName: this.context.options.repoName,
3394
3493
  authorName: this.context.options.authorName,
3395
3494
  env: this.context.options.releaseEnv,
3396
- releaseId: this.context.releaseId
3495
+ releaseId: this.context.releaseId,
3496
+ increment: this.context.parameters.changesetVersion?.increment || releaseJson.changesetVersion.increment
3397
3497
  });
3398
3498
  this.logger.info(`Github mode: ${this.mode}`);
3399
3499
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qlover/fe-release",
3
3
  "description": "A tool for releasing front-end projects, supporting multiple release modes and configurations, simplifying the release process and improving efficiency.",
4
- "version": "5.0.4",
4
+ "version": "5.0.6",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "homepage": "https://github.com/qlover/fe-base/tree/master/packages/fe-release",
@@ -60,9 +60,9 @@
60
60
  "p-limit": "^6.2.0",
61
61
  "semver": "^7.5.4",
62
62
  "@qlover/env-loader": "1.0.2",
63
- "@qlover/fe-corekit": "3.3.0",
64
- "@qlover/scripts-context": "2.3.3",
65
- "@qlover/logger": "1.2.1"
63
+ "@qlover/fe-corekit": "3.4.4",
64
+ "@qlover/logger": "1.2.1",
65
+ "@qlover/scripts-context": "2.3.3"
66
66
  },
67
67
  "scripts": {
68
68
  "lint": "eslint src --fix",