@qlover/fe-release 5.0.5 → 5.1.0

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.5";
31
+ var version = "5.1.0";
32
32
 
33
33
  // src/cli.ts
34
34
  var import_semver = __toESM(require("semver"), 1);
@@ -82,6 +82,10 @@ var releaseJson = {
82
82
  branchName: "release/${repoName}-${releaseId}",
83
83
  releaseTagName: "release-tag-${count}-patch-${releaseId}",
84
84
  commitMessage: "chore(release): ${spaces}",
85
+ /** GitHub release title template (createRelease mode) */
86
+ releaseName: "${name}@${version}",
87
+ /** Path prefixes skipped when creating GitHub releases */
88
+ ignoreReleasePaths: [],
85
89
  label: {
86
90
  name: "CI-Release",
87
91
  color: "1A7F37",
@@ -592,6 +596,9 @@ function workspaceVersionSummary(workspace) {
592
596
  function shouldProcessWorkspace(workspace, ignoreNonUpdatedPackages) {
593
597
  return !(ignoreNonUpdatedPackages && workspace.dependencyRelease);
594
598
  }
599
+ function isPrivateWorkspace(workspace) {
600
+ return workspace.packageJson?.private === true;
601
+ }
595
602
  function readJson(path) {
596
603
  const packageJsonContent = (0, import_fs.readFileSync)(path, "utf-8");
597
604
  return JSON.parse(packageJsonContent);
@@ -1733,9 +1740,18 @@ ${workspaces?.map((workspace) => workspace.toString()).join("\n")}`
1733
1740
  this.logDryRun("Would run: changeset publish, publish packages and tags");
1734
1741
  const preview = this.mergeWorkspaces(workspaces);
1735
1742
  this.context.setWorkspaces(preview);
1736
- const previewTags = preview.filter(
1737
- (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && workspace.tagName
1743
+ const previewTags = this.collectPushableReleaseTags(preview);
1744
+ const privateTags = preview.filter(
1745
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1738
1746
  ).map((workspace) => workspace.tagName);
1747
+ if (privateTags.length > 0) {
1748
+ this.logDryRun(
1749
+ `Would create local tag(s) for private package(s):
1750
+ ${[
1751
+ ...new Set(privateTags)
1752
+ ].map((tag) => ` ${tag}`).join("\n")}`
1753
+ );
1754
+ }
1739
1755
  this.logDryRun(
1740
1756
  previewTags.length === 0 ? "Would push release tag(s) from workspace.tagName: none" : `Would push release tag(s) from workspace.tagName:
1741
1757
  ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
@@ -1749,37 +1765,106 @@ ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
1749
1765
  if (workspaces.length > 0) {
1750
1766
  this.syncWorkspaces(workspaces);
1751
1767
  }
1768
+ const syncedWorkspaces = this.context.workspaces ?? workspaces;
1769
+ await this.step({
1770
+ label: "Create Private Package Tags",
1771
+ task: () => this.ensurePrivateReleaseTags(syncedWorkspaces)
1772
+ });
1752
1773
  await this.step({
1753
1774
  label: "Push Release Tags",
1754
- task: () => this.pushWorkspaceReleaseTags(this.context.workspaces ?? workspaces)
1775
+ task: () => this.pushWorkspaceReleaseTags(syncedWorkspaces)
1755
1776
  });
1756
1777
  }
1757
1778
  /**
1758
- * Push release tags from synced workspaces' `tagName` fields.
1779
+ * Collect tag names that should be pushed after `changeset publish`.
1759
1780
  *
1760
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
1761
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1781
+ * Includes public packages (tagged by changesets) and private packages
1782
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1783
+ * workspaces.
1762
1784
  */
1763
- async pushWorkspaceReleaseTags(workspaces) {
1785
+ collectPushableReleaseTags(workspaces) {
1764
1786
  const tags = workspaces.filter(
1765
1787
  (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && !!workspace.tagName
1766
1788
  ).map((workspace) => workspace.tagName);
1767
- const uniqueTags = [...new Set(tags)];
1789
+ return [...new Set(tags)];
1790
+ }
1791
+ /**
1792
+ * List local git tag names (short refs under `refs/tags/`).
1793
+ */
1794
+ async listLocalTags() {
1795
+ const output = await this.shell.exec(
1796
+ 'git for-each-ref --format="%(refname:short)" refs/tags/',
1797
+ { dryRun: false, silent: true }
1798
+ );
1799
+ const tags = output.split("\n").map((line) => line.trim()).filter(Boolean);
1800
+ return new Set(tags);
1801
+ }
1802
+ /**
1803
+ * Create local git tags for private packages after `changeset publish`.
1804
+ *
1805
+ * Changesets skips npm publish and tagging for `private: true` packages.
1806
+ * Release still needs those tags as changelog baselines, so create them here.
1807
+ */
1808
+ async ensurePrivateReleaseTags(workspaces) {
1809
+ const privateWorkspaces = workspaces.filter(
1810
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1811
+ );
1812
+ if (privateWorkspaces.length === 0) {
1813
+ return;
1814
+ }
1815
+ const localTags = await this.listLocalTags();
1816
+ for (const workspace of privateWorkspaces) {
1817
+ const tagName = workspace.tagName;
1818
+ if (localTags.has(tagName)) {
1819
+ this.logger.debug(`Private package tag already exists: ${tagName}`);
1820
+ continue;
1821
+ }
1822
+ this.logger.info(
1823
+ `Create local release tag for private package: ${tagName}`
1824
+ );
1825
+ await this.shell.exec(["git", "tag", tagName]);
1826
+ localTags.add(tagName);
1827
+ }
1828
+ }
1829
+ /**
1830
+ * Push release tags from synced workspaces' `tagName` fields.
1831
+ *
1832
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1833
+ * tags that exist as local refs. Missing public-package tags are skipped
1834
+ * (changeset may have skipped publish); they are not invented here.
1835
+ */
1836
+ async pushWorkspaceReleaseTags(workspaces) {
1837
+ const uniqueTags = this.collectPushableReleaseTags(workspaces);
1768
1838
  if (uniqueTags.length === 0) {
1769
1839
  this.logger.warn(
1770
1840
  "No workspace.tagName found after publish sync; skip tag push"
1771
1841
  );
1772
1842
  return;
1773
1843
  }
1844
+ const localTags = await this.listLocalTags();
1845
+ const existingTags = uniqueTags.filter((tag) => localTags.has(tag));
1846
+ const missingTags = uniqueTags.filter((tag) => !localTags.has(tag));
1847
+ if (missingTags.length > 0) {
1848
+ this.logger.warn(
1849
+ `Skip push for missing local tag(s):
1850
+ ${missingTags.map((tag) => ` ${tag}`).join("\n")}`
1851
+ );
1852
+ }
1853
+ if (existingTags.length === 0) {
1854
+ this.logger.warn(
1855
+ "No local release tags found to push after publish; skip tag push"
1856
+ );
1857
+ return;
1858
+ }
1774
1859
  this.logger.info(
1775
- `Pushing ${uniqueTags.length} release tag(s) to origin:
1776
- ${uniqueTags.map((tag) => ` ${tag}`).join("\n")}`
1860
+ `Pushing ${existingTags.length} release tag(s) to origin:
1861
+ ${existingTags.map((tag) => ` ${tag}`).join("\n")}`
1777
1862
  );
1778
1863
  await this.shell.exec([
1779
1864
  "git",
1780
1865
  "push",
1781
1866
  "origin",
1782
- ...uniqueTags.map((tag) => `"refs/tags/${tag}"`)
1867
+ ...existingTags.map((tag) => `"refs/tags/${tag}"`)
1783
1868
  ]);
1784
1869
  }
1785
1870
  syncWorkspaces(workspaces) {
@@ -2664,7 +2749,7 @@ var GithubManager = class {
2664
2749
  */
2665
2750
  getOctokitReleaseOptions(options) {
2666
2751
  const {
2667
- releaseName,
2752
+ releaseName = releaseJson.github.releaseName,
2668
2753
  draft = false,
2669
2754
  preRelease = false,
2670
2755
  autoGenerate = false,
@@ -2673,7 +2758,7 @@ var GithubManager = class {
2673
2758
  discussionCategoryName = void 0
2674
2759
  } = this.context.getParameters("github");
2675
2760
  const name = releaseName;
2676
- const body = autoGenerate ? "" : this.truncateBody(String(releaseNotes));
2761
+ const body = autoGenerate ? "" : this.truncateBody(String(releaseNotes ?? ""));
2677
2762
  return {
2678
2763
  name,
2679
2764
  make_latest: makeLatest.toString(),
@@ -3437,6 +3522,7 @@ var ReleaseFormatter = class {
3437
3522
  var Github = class extends GitBase {
3438
3523
  releaseFormatter;
3439
3524
  githubManager;
3525
+ pather = new Pather();
3440
3526
  constructor(context, props = {}) {
3441
3527
  super(context, "github", props);
3442
3528
  this.releaseFormatter = new ReleaseFormatter(
@@ -3448,6 +3534,9 @@ var Github = class extends GitBase {
3448
3534
  get mode() {
3449
3535
  return this.config.mode || releaseJson.github.mode;
3450
3536
  }
3537
+ get ignoreReleasePaths() {
3538
+ return this.config.ignoreReleasePaths ?? [...releaseJson.github.ignoreReleasePaths];
3539
+ }
3451
3540
  async onBefore() {
3452
3541
  await super.onBefore();
3453
3542
  await this.githubManager.validateToken();
@@ -3473,6 +3562,9 @@ var Github = class extends GitBase {
3473
3562
  if (workspace.dependencyRelease) {
3474
3563
  return workspace;
3475
3564
  }
3565
+ if (this.mode === "createRelease" && this.isIgnoredReleasePath(workspace)) {
3566
+ return workspace;
3567
+ }
3476
3568
  return githubChangelog.enrichWorkspaceChangelog(workspace);
3477
3569
  })
3478
3570
  )
@@ -3481,6 +3573,13 @@ var Github = class extends GitBase {
3481
3573
  }
3482
3574
  async onSuccess() {
3483
3575
  const workspaces = this.context.requireWorkspaces();
3576
+ if (this.mode === "createRelease") {
3577
+ await this.step({
3578
+ label: "Create GitHub Releases",
3579
+ task: () => this.createGithubReleases(workspaces)
3580
+ });
3581
+ return;
3582
+ }
3484
3583
  const releaseBranchResult = await this.step({
3485
3584
  label: "Create Release Branch",
3486
3585
  task: () => this.createReleaseBranch(workspaces)
@@ -3497,6 +3596,63 @@ var Github = class extends GitBase {
3497
3596
  }
3498
3597
  await this.handleReleasePullRequest(workspaces, releaseBranchResult);
3499
3598
  }
3599
+ /**
3600
+ * Whether this workspace path is under an ignoreReleasePaths entry.
3601
+ */
3602
+ isIgnoredReleasePath(workspace) {
3603
+ const ignorePaths = this.ignoreReleasePaths;
3604
+ if (ignorePaths.length === 0) {
3605
+ return false;
3606
+ }
3607
+ return ignorePaths.some(
3608
+ (ignorePath) => this.pather.isSubPath(workspace.path, ignorePath)
3609
+ );
3610
+ }
3611
+ /**
3612
+ * Workspaces that should receive a GitHub Release in createRelease mode.
3613
+ *
3614
+ * Requires `tagName`. Skips dependency-release packages and paths matched
3615
+ * by {@link GithubProps.ignoreReleasePaths}. Private packages are included
3616
+ * when they have a tag.
3617
+ */
3618
+ getReleaseWorkspaces(workspaces) {
3619
+ return workspaces.filter((workspace) => {
3620
+ if (!workspace.tagName) {
3621
+ return false;
3622
+ }
3623
+ if (workspace.dependencyRelease) {
3624
+ return false;
3625
+ }
3626
+ if (this.isIgnoredReleasePath(workspace)) {
3627
+ this.logger.info(
3628
+ `Skip GitHub Release for ignored path: ${workspace.path} (${workspace.tagName})`
3629
+ );
3630
+ return false;
3631
+ }
3632
+ return true;
3633
+ });
3634
+ }
3635
+ /**
3636
+ * Create a GitHub Release for each eligible workspace.
3637
+ *
3638
+ * Release notes use the enriched workspace changelog from `onExec`.
3639
+ */
3640
+ async createGithubReleases(workspaces) {
3641
+ const releaseWorkspaces = this.getReleaseWorkspaces(workspaces);
3642
+ if (releaseWorkspaces.length === 0) {
3643
+ this.logger.warn("No workspaces eligible for GitHub Release creation");
3644
+ return;
3645
+ }
3646
+ this.logger.info(
3647
+ `Creating GitHub Releases (${releaseWorkspaces.length}):
3648
+ ${releaseWorkspaces.map((w) => ` ${w.tagName}`).join("\n")}`
3649
+ );
3650
+ await Promise.all(
3651
+ releaseWorkspaces.map(
3652
+ (workspace) => this.githubManager.createRelease(workspace)
3653
+ )
3654
+ );
3655
+ }
3500
3656
  /**
3501
3657
  * Create the release PR and optionally auto-merge it.
3502
3658
  *
@@ -3852,6 +4008,22 @@ function programArgs() {
3852
4008
  ).option(
3853
4009
  "--workspaces.skip",
3854
4010
  "Skip the Workspaces plugin"
4011
+ ).option(
4012
+ "--github.mode <mode>",
4013
+ "Github mode: createPR (default) or createRelease (fe-config: release.github.mode)",
4014
+ (value) => {
4015
+ if (value !== "createPR" && value !== "createRelease") {
4016
+ throw new Error(
4017
+ 'Invalid github.mode. Must be "createPR" or "createRelease"'
4018
+ );
4019
+ }
4020
+ return value;
4021
+ },
4022
+ "createPR"
4023
+ ).option(
4024
+ "--github.ignore-release-paths <paths>",
4025
+ "Path prefixes to skip when creating GitHub Releases, comma-separated (fe-config: release.github.ignoreReleasePaths)",
4026
+ splitWithComma
3855
4027
  ).option(
3856
4028
  "--github.skip [lifecycle]",
3857
4029
  "Skip the Github plugin (no value) or a lifecycle such as onSuccess"
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.5";
8
+ var version = "5.1.0";
9
9
 
10
10
  // src/cli.ts
11
11
  import semver from "semver";
@@ -59,6 +59,10 @@ var releaseJson = {
59
59
  branchName: "release/${repoName}-${releaseId}",
60
60
  releaseTagName: "release-tag-${count}-patch-${releaseId}",
61
61
  commitMessage: "chore(release): ${spaces}",
62
+ /** GitHub release title template (createRelease mode) */
63
+ releaseName: "${name}@${version}",
64
+ /** Path prefixes skipped when creating GitHub releases */
65
+ ignoreReleasePaths: [],
62
66
  label: {
63
67
  name: "CI-Release",
64
68
  color: "1A7F37",
@@ -572,6 +576,9 @@ function workspaceVersionSummary(workspace) {
572
576
  function shouldProcessWorkspace(workspace, ignoreNonUpdatedPackages) {
573
577
  return !(ignoreNonUpdatedPackages && workspace.dependencyRelease);
574
578
  }
579
+ function isPrivateWorkspace(workspace) {
580
+ return workspace.packageJson?.private === true;
581
+ }
575
582
  function readJson(path) {
576
583
  const packageJsonContent = readFileSync(path, "utf-8");
577
584
  return JSON.parse(packageJsonContent);
@@ -1713,9 +1720,18 @@ ${workspaces?.map((workspace) => workspace.toString()).join("\n")}`
1713
1720
  this.logDryRun("Would run: changeset publish, publish packages and tags");
1714
1721
  const preview = this.mergeWorkspaces(workspaces);
1715
1722
  this.context.setWorkspaces(preview);
1716
- const previewTags = preview.filter(
1717
- (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && workspace.tagName
1723
+ const previewTags = this.collectPushableReleaseTags(preview);
1724
+ const privateTags = preview.filter(
1725
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1718
1726
  ).map((workspace) => workspace.tagName);
1727
+ if (privateTags.length > 0) {
1728
+ this.logDryRun(
1729
+ `Would create local tag(s) for private package(s):
1730
+ ${[
1731
+ ...new Set(privateTags)
1732
+ ].map((tag) => ` ${tag}`).join("\n")}`
1733
+ );
1734
+ }
1719
1735
  this.logDryRun(
1720
1736
  previewTags.length === 0 ? "Would push release tag(s) from workspace.tagName: none" : `Would push release tag(s) from workspace.tagName:
1721
1737
  ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
@@ -1729,37 +1745,106 @@ ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
1729
1745
  if (workspaces.length > 0) {
1730
1746
  this.syncWorkspaces(workspaces);
1731
1747
  }
1748
+ const syncedWorkspaces = this.context.workspaces ?? workspaces;
1749
+ await this.step({
1750
+ label: "Create Private Package Tags",
1751
+ task: () => this.ensurePrivateReleaseTags(syncedWorkspaces)
1752
+ });
1732
1753
  await this.step({
1733
1754
  label: "Push Release Tags",
1734
- task: () => this.pushWorkspaceReleaseTags(this.context.workspaces ?? workspaces)
1755
+ task: () => this.pushWorkspaceReleaseTags(syncedWorkspaces)
1735
1756
  });
1736
1757
  }
1737
1758
  /**
1738
- * Push release tags from synced workspaces' `tagName` fields.
1759
+ * Collect tag names that should be pushed after `changeset publish`.
1739
1760
  *
1740
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
1741
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1761
+ * Includes public packages (tagged by changesets) and private packages
1762
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1763
+ * workspaces.
1742
1764
  */
1743
- async pushWorkspaceReleaseTags(workspaces) {
1765
+ collectPushableReleaseTags(workspaces) {
1744
1766
  const tags = workspaces.filter(
1745
1767
  (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && !!workspace.tagName
1746
1768
  ).map((workspace) => workspace.tagName);
1747
- const uniqueTags = [...new Set(tags)];
1769
+ return [...new Set(tags)];
1770
+ }
1771
+ /**
1772
+ * List local git tag names (short refs under `refs/tags/`).
1773
+ */
1774
+ async listLocalTags() {
1775
+ const output = await this.shell.exec(
1776
+ 'git for-each-ref --format="%(refname:short)" refs/tags/',
1777
+ { dryRun: false, silent: true }
1778
+ );
1779
+ const tags = output.split("\n").map((line) => line.trim()).filter(Boolean);
1780
+ return new Set(tags);
1781
+ }
1782
+ /**
1783
+ * Create local git tags for private packages after `changeset publish`.
1784
+ *
1785
+ * Changesets skips npm publish and tagging for `private: true` packages.
1786
+ * Release still needs those tags as changelog baselines, so create them here.
1787
+ */
1788
+ async ensurePrivateReleaseTags(workspaces) {
1789
+ const privateWorkspaces = workspaces.filter(
1790
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1791
+ );
1792
+ if (privateWorkspaces.length === 0) {
1793
+ return;
1794
+ }
1795
+ const localTags = await this.listLocalTags();
1796
+ for (const workspace of privateWorkspaces) {
1797
+ const tagName = workspace.tagName;
1798
+ if (localTags.has(tagName)) {
1799
+ this.logger.debug(`Private package tag already exists: ${tagName}`);
1800
+ continue;
1801
+ }
1802
+ this.logger.info(
1803
+ `Create local release tag for private package: ${tagName}`
1804
+ );
1805
+ await this.shell.exec(["git", "tag", tagName]);
1806
+ localTags.add(tagName);
1807
+ }
1808
+ }
1809
+ /**
1810
+ * Push release tags from synced workspaces' `tagName` fields.
1811
+ *
1812
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1813
+ * tags that exist as local refs. Missing public-package tags are skipped
1814
+ * (changeset may have skipped publish); they are not invented here.
1815
+ */
1816
+ async pushWorkspaceReleaseTags(workspaces) {
1817
+ const uniqueTags = this.collectPushableReleaseTags(workspaces);
1748
1818
  if (uniqueTags.length === 0) {
1749
1819
  this.logger.warn(
1750
1820
  "No workspace.tagName found after publish sync; skip tag push"
1751
1821
  );
1752
1822
  return;
1753
1823
  }
1824
+ const localTags = await this.listLocalTags();
1825
+ const existingTags = uniqueTags.filter((tag) => localTags.has(tag));
1826
+ const missingTags = uniqueTags.filter((tag) => !localTags.has(tag));
1827
+ if (missingTags.length > 0) {
1828
+ this.logger.warn(
1829
+ `Skip push for missing local tag(s):
1830
+ ${missingTags.map((tag) => ` ${tag}`).join("\n")}`
1831
+ );
1832
+ }
1833
+ if (existingTags.length === 0) {
1834
+ this.logger.warn(
1835
+ "No local release tags found to push after publish; skip tag push"
1836
+ );
1837
+ return;
1838
+ }
1754
1839
  this.logger.info(
1755
- `Pushing ${uniqueTags.length} release tag(s) to origin:
1756
- ${uniqueTags.map((tag) => ` ${tag}`).join("\n")}`
1840
+ `Pushing ${existingTags.length} release tag(s) to origin:
1841
+ ${existingTags.map((tag) => ` ${tag}`).join("\n")}`
1757
1842
  );
1758
1843
  await this.shell.exec([
1759
1844
  "git",
1760
1845
  "push",
1761
1846
  "origin",
1762
- ...uniqueTags.map((tag) => `"refs/tags/${tag}"`)
1847
+ ...existingTags.map((tag) => `"refs/tags/${tag}"`)
1763
1848
  ]);
1764
1849
  }
1765
1850
  syncWorkspaces(workspaces) {
@@ -2644,7 +2729,7 @@ var GithubManager = class {
2644
2729
  */
2645
2730
  getOctokitReleaseOptions(options) {
2646
2731
  const {
2647
- releaseName,
2732
+ releaseName = releaseJson.github.releaseName,
2648
2733
  draft = false,
2649
2734
  preRelease = false,
2650
2735
  autoGenerate = false,
@@ -2653,7 +2738,7 @@ var GithubManager = class {
2653
2738
  discussionCategoryName = void 0
2654
2739
  } = this.context.getParameters("github");
2655
2740
  const name = releaseName;
2656
- const body = autoGenerate ? "" : this.truncateBody(String(releaseNotes));
2741
+ const body = autoGenerate ? "" : this.truncateBody(String(releaseNotes ?? ""));
2657
2742
  return {
2658
2743
  name,
2659
2744
  make_latest: makeLatest.toString(),
@@ -3417,6 +3502,7 @@ var ReleaseFormatter = class {
3417
3502
  var Github = class extends GitBase {
3418
3503
  releaseFormatter;
3419
3504
  githubManager;
3505
+ pather = new Pather();
3420
3506
  constructor(context, props = {}) {
3421
3507
  super(context, "github", props);
3422
3508
  this.releaseFormatter = new ReleaseFormatter(
@@ -3428,6 +3514,9 @@ var Github = class extends GitBase {
3428
3514
  get mode() {
3429
3515
  return this.config.mode || releaseJson.github.mode;
3430
3516
  }
3517
+ get ignoreReleasePaths() {
3518
+ return this.config.ignoreReleasePaths ?? [...releaseJson.github.ignoreReleasePaths];
3519
+ }
3431
3520
  async onBefore() {
3432
3521
  await super.onBefore();
3433
3522
  await this.githubManager.validateToken();
@@ -3453,6 +3542,9 @@ var Github = class extends GitBase {
3453
3542
  if (workspace.dependencyRelease) {
3454
3543
  return workspace;
3455
3544
  }
3545
+ if (this.mode === "createRelease" && this.isIgnoredReleasePath(workspace)) {
3546
+ return workspace;
3547
+ }
3456
3548
  return githubChangelog.enrichWorkspaceChangelog(workspace);
3457
3549
  })
3458
3550
  )
@@ -3461,6 +3553,13 @@ var Github = class extends GitBase {
3461
3553
  }
3462
3554
  async onSuccess() {
3463
3555
  const workspaces = this.context.requireWorkspaces();
3556
+ if (this.mode === "createRelease") {
3557
+ await this.step({
3558
+ label: "Create GitHub Releases",
3559
+ task: () => this.createGithubReleases(workspaces)
3560
+ });
3561
+ return;
3562
+ }
3464
3563
  const releaseBranchResult = await this.step({
3465
3564
  label: "Create Release Branch",
3466
3565
  task: () => this.createReleaseBranch(workspaces)
@@ -3477,6 +3576,63 @@ var Github = class extends GitBase {
3477
3576
  }
3478
3577
  await this.handleReleasePullRequest(workspaces, releaseBranchResult);
3479
3578
  }
3579
+ /**
3580
+ * Whether this workspace path is under an ignoreReleasePaths entry.
3581
+ */
3582
+ isIgnoredReleasePath(workspace) {
3583
+ const ignorePaths = this.ignoreReleasePaths;
3584
+ if (ignorePaths.length === 0) {
3585
+ return false;
3586
+ }
3587
+ return ignorePaths.some(
3588
+ (ignorePath) => this.pather.isSubPath(workspace.path, ignorePath)
3589
+ );
3590
+ }
3591
+ /**
3592
+ * Workspaces that should receive a GitHub Release in createRelease mode.
3593
+ *
3594
+ * Requires `tagName`. Skips dependency-release packages and paths matched
3595
+ * by {@link GithubProps.ignoreReleasePaths}. Private packages are included
3596
+ * when they have a tag.
3597
+ */
3598
+ getReleaseWorkspaces(workspaces) {
3599
+ return workspaces.filter((workspace) => {
3600
+ if (!workspace.tagName) {
3601
+ return false;
3602
+ }
3603
+ if (workspace.dependencyRelease) {
3604
+ return false;
3605
+ }
3606
+ if (this.isIgnoredReleasePath(workspace)) {
3607
+ this.logger.info(
3608
+ `Skip GitHub Release for ignored path: ${workspace.path} (${workspace.tagName})`
3609
+ );
3610
+ return false;
3611
+ }
3612
+ return true;
3613
+ });
3614
+ }
3615
+ /**
3616
+ * Create a GitHub Release for each eligible workspace.
3617
+ *
3618
+ * Release notes use the enriched workspace changelog from `onExec`.
3619
+ */
3620
+ async createGithubReleases(workspaces) {
3621
+ const releaseWorkspaces = this.getReleaseWorkspaces(workspaces);
3622
+ if (releaseWorkspaces.length === 0) {
3623
+ this.logger.warn("No workspaces eligible for GitHub Release creation");
3624
+ return;
3625
+ }
3626
+ this.logger.info(
3627
+ `Creating GitHub Releases (${releaseWorkspaces.length}):
3628
+ ${releaseWorkspaces.map((w) => ` ${w.tagName}`).join("\n")}`
3629
+ );
3630
+ await Promise.all(
3631
+ releaseWorkspaces.map(
3632
+ (workspace) => this.githubManager.createRelease(workspace)
3633
+ )
3634
+ );
3635
+ }
3480
3636
  /**
3481
3637
  * Create the release PR and optionally auto-merge it.
3482
3638
  *
@@ -3832,6 +3988,22 @@ function programArgs() {
3832
3988
  ).option(
3833
3989
  "--workspaces.skip",
3834
3990
  "Skip the Workspaces plugin"
3991
+ ).option(
3992
+ "--github.mode <mode>",
3993
+ "Github mode: createPR (default) or createRelease (fe-config: release.github.mode)",
3994
+ (value) => {
3995
+ if (value !== "createPR" && value !== "createRelease") {
3996
+ throw new Error(
3997
+ 'Invalid github.mode. Must be "createPR" or "createRelease"'
3998
+ );
3999
+ }
4000
+ return value;
4001
+ },
4002
+ "createPR"
4003
+ ).option(
4004
+ "--github.ignore-release-paths <paths>",
4005
+ "Path prefixes to skip when creating GitHub Releases, comma-separated (fe-config: release.github.ignoreReleasePaths)",
4006
+ splitWithComma
3835
4007
  ).option(
3836
4008
  "--github.skip [lifecycle]",
3837
4009
  "Skip the Github plugin (no value) or a lifecycle such as onSuccess"
package/dist/index.cjs CHANGED
@@ -79,6 +79,10 @@ var releaseJson = {
79
79
  branchName: "release/${repoName}-${releaseId}",
80
80
  releaseTagName: "release-tag-${count}-patch-${releaseId}",
81
81
  commitMessage: "chore(release): ${spaces}",
82
+ /** GitHub release title template (createRelease mode) */
83
+ releaseName: "${name}@${version}",
84
+ /** Path prefixes skipped when creating GitHub releases */
85
+ ignoreReleasePaths: [],
82
86
  label: {
83
87
  name: "CI-Release",
84
88
  color: "1A7F37",
@@ -597,6 +601,9 @@ function workspaceVersionSummary(workspace) {
597
601
  function shouldProcessWorkspace(workspace, ignoreNonUpdatedPackages) {
598
602
  return !(ignoreNonUpdatedPackages && workspace.dependencyRelease);
599
603
  }
604
+ function isPrivateWorkspace(workspace) {
605
+ return workspace.packageJson?.private === true;
606
+ }
600
607
  function readJson(path) {
601
608
  const packageJsonContent = (0, import_fs.readFileSync)(path, "utf-8");
602
609
  return JSON.parse(packageJsonContent);
@@ -1738,9 +1745,18 @@ ${workspaces?.map((workspace) => workspace.toString()).join("\n")}`
1738
1745
  this.logDryRun("Would run: changeset publish, publish packages and tags");
1739
1746
  const preview = this.mergeWorkspaces(workspaces);
1740
1747
  this.context.setWorkspaces(preview);
1741
- const previewTags = preview.filter(
1742
- (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && workspace.tagName
1748
+ const previewTags = this.collectPushableReleaseTags(preview);
1749
+ const privateTags = preview.filter(
1750
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1743
1751
  ).map((workspace) => workspace.tagName);
1752
+ if (privateTags.length > 0) {
1753
+ this.logDryRun(
1754
+ `Would create local tag(s) for private package(s):
1755
+ ${[
1756
+ ...new Set(privateTags)
1757
+ ].map((tag) => ` ${tag}`).join("\n")}`
1758
+ );
1759
+ }
1744
1760
  this.logDryRun(
1745
1761
  previewTags.length === 0 ? "Would push release tag(s) from workspace.tagName: none" : `Would push release tag(s) from workspace.tagName:
1746
1762
  ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
@@ -1754,37 +1770,106 @@ ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
1754
1770
  if (workspaces.length > 0) {
1755
1771
  this.syncWorkspaces(workspaces);
1756
1772
  }
1773
+ const syncedWorkspaces = this.context.workspaces ?? workspaces;
1774
+ await this.step({
1775
+ label: "Create Private Package Tags",
1776
+ task: () => this.ensurePrivateReleaseTags(syncedWorkspaces)
1777
+ });
1757
1778
  await this.step({
1758
1779
  label: "Push Release Tags",
1759
- task: () => this.pushWorkspaceReleaseTags(this.context.workspaces ?? workspaces)
1780
+ task: () => this.pushWorkspaceReleaseTags(syncedWorkspaces)
1760
1781
  });
1761
1782
  }
1762
1783
  /**
1763
- * Push release tags from synced workspaces' `tagName` fields.
1784
+ * Collect tag names that should be pushed after `changeset publish`.
1764
1785
  *
1765
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
1766
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1786
+ * Includes public packages (tagged by changesets) and private packages
1787
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1788
+ * workspaces.
1767
1789
  */
1768
- async pushWorkspaceReleaseTags(workspaces) {
1790
+ collectPushableReleaseTags(workspaces) {
1769
1791
  const tags = workspaces.filter(
1770
1792
  (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && !!workspace.tagName
1771
1793
  ).map((workspace) => workspace.tagName);
1772
- const uniqueTags = [...new Set(tags)];
1794
+ return [...new Set(tags)];
1795
+ }
1796
+ /**
1797
+ * List local git tag names (short refs under `refs/tags/`).
1798
+ */
1799
+ async listLocalTags() {
1800
+ const output = await this.shell.exec(
1801
+ 'git for-each-ref --format="%(refname:short)" refs/tags/',
1802
+ { dryRun: false, silent: true }
1803
+ );
1804
+ const tags = output.split("\n").map((line) => line.trim()).filter(Boolean);
1805
+ return new Set(tags);
1806
+ }
1807
+ /**
1808
+ * Create local git tags for private packages after `changeset publish`.
1809
+ *
1810
+ * Changesets skips npm publish and tagging for `private: true` packages.
1811
+ * Release still needs those tags as changelog baselines, so create them here.
1812
+ */
1813
+ async ensurePrivateReleaseTags(workspaces) {
1814
+ const privateWorkspaces = workspaces.filter(
1815
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1816
+ );
1817
+ if (privateWorkspaces.length === 0) {
1818
+ return;
1819
+ }
1820
+ const localTags = await this.listLocalTags();
1821
+ for (const workspace of privateWorkspaces) {
1822
+ const tagName = workspace.tagName;
1823
+ if (localTags.has(tagName)) {
1824
+ this.logger.debug(`Private package tag already exists: ${tagName}`);
1825
+ continue;
1826
+ }
1827
+ this.logger.info(
1828
+ `Create local release tag for private package: ${tagName}`
1829
+ );
1830
+ await this.shell.exec(["git", "tag", tagName]);
1831
+ localTags.add(tagName);
1832
+ }
1833
+ }
1834
+ /**
1835
+ * Push release tags from synced workspaces' `tagName` fields.
1836
+ *
1837
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1838
+ * tags that exist as local refs. Missing public-package tags are skipped
1839
+ * (changeset may have skipped publish); they are not invented here.
1840
+ */
1841
+ async pushWorkspaceReleaseTags(workspaces) {
1842
+ const uniqueTags = this.collectPushableReleaseTags(workspaces);
1773
1843
  if (uniqueTags.length === 0) {
1774
1844
  this.logger.warn(
1775
1845
  "No workspace.tagName found after publish sync; skip tag push"
1776
1846
  );
1777
1847
  return;
1778
1848
  }
1849
+ const localTags = await this.listLocalTags();
1850
+ const existingTags = uniqueTags.filter((tag) => localTags.has(tag));
1851
+ const missingTags = uniqueTags.filter((tag) => !localTags.has(tag));
1852
+ if (missingTags.length > 0) {
1853
+ this.logger.warn(
1854
+ `Skip push for missing local tag(s):
1855
+ ${missingTags.map((tag) => ` ${tag}`).join("\n")}`
1856
+ );
1857
+ }
1858
+ if (existingTags.length === 0) {
1859
+ this.logger.warn(
1860
+ "No local release tags found to push after publish; skip tag push"
1861
+ );
1862
+ return;
1863
+ }
1779
1864
  this.logger.info(
1780
- `Pushing ${uniqueTags.length} release tag(s) to origin:
1781
- ${uniqueTags.map((tag) => ` ${tag}`).join("\n")}`
1865
+ `Pushing ${existingTags.length} release tag(s) to origin:
1866
+ ${existingTags.map((tag) => ` ${tag}`).join("\n")}`
1782
1867
  );
1783
1868
  await this.shell.exec([
1784
1869
  "git",
1785
1870
  "push",
1786
1871
  "origin",
1787
- ...uniqueTags.map((tag) => `"refs/tags/${tag}"`)
1872
+ ...existingTags.map((tag) => `"refs/tags/${tag}"`)
1788
1873
  ]);
1789
1874
  }
1790
1875
  syncWorkspaces(workspaces) {
@@ -2669,7 +2754,7 @@ var GithubManager = class {
2669
2754
  */
2670
2755
  getOctokitReleaseOptions(options) {
2671
2756
  const {
2672
- releaseName,
2757
+ releaseName = releaseJson.github.releaseName,
2673
2758
  draft = false,
2674
2759
  preRelease = false,
2675
2760
  autoGenerate = false,
@@ -2678,7 +2763,7 @@ var GithubManager = class {
2678
2763
  discussionCategoryName = void 0
2679
2764
  } = this.context.getParameters("github");
2680
2765
  const name = releaseName;
2681
- const body = autoGenerate ? "" : this.truncateBody(String(releaseNotes));
2766
+ const body = autoGenerate ? "" : this.truncateBody(String(releaseNotes ?? ""));
2682
2767
  return {
2683
2768
  name,
2684
2769
  make_latest: makeLatest.toString(),
@@ -3442,6 +3527,7 @@ var ReleaseFormatter = class {
3442
3527
  var Github = class extends GitBase {
3443
3528
  releaseFormatter;
3444
3529
  githubManager;
3530
+ pather = new Pather();
3445
3531
  constructor(context, props = {}) {
3446
3532
  super(context, "github", props);
3447
3533
  this.releaseFormatter = new ReleaseFormatter(
@@ -3453,6 +3539,9 @@ var Github = class extends GitBase {
3453
3539
  get mode() {
3454
3540
  return this.config.mode || releaseJson.github.mode;
3455
3541
  }
3542
+ get ignoreReleasePaths() {
3543
+ return this.config.ignoreReleasePaths ?? [...releaseJson.github.ignoreReleasePaths];
3544
+ }
3456
3545
  async onBefore() {
3457
3546
  await super.onBefore();
3458
3547
  await this.githubManager.validateToken();
@@ -3478,6 +3567,9 @@ var Github = class extends GitBase {
3478
3567
  if (workspace.dependencyRelease) {
3479
3568
  return workspace;
3480
3569
  }
3570
+ if (this.mode === "createRelease" && this.isIgnoredReleasePath(workspace)) {
3571
+ return workspace;
3572
+ }
3481
3573
  return githubChangelog.enrichWorkspaceChangelog(workspace);
3482
3574
  })
3483
3575
  )
@@ -3486,6 +3578,13 @@ var Github = class extends GitBase {
3486
3578
  }
3487
3579
  async onSuccess() {
3488
3580
  const workspaces = this.context.requireWorkspaces();
3581
+ if (this.mode === "createRelease") {
3582
+ await this.step({
3583
+ label: "Create GitHub Releases",
3584
+ task: () => this.createGithubReleases(workspaces)
3585
+ });
3586
+ return;
3587
+ }
3489
3588
  const releaseBranchResult = await this.step({
3490
3589
  label: "Create Release Branch",
3491
3590
  task: () => this.createReleaseBranch(workspaces)
@@ -3502,6 +3601,63 @@ var Github = class extends GitBase {
3502
3601
  }
3503
3602
  await this.handleReleasePullRequest(workspaces, releaseBranchResult);
3504
3603
  }
3604
+ /**
3605
+ * Whether this workspace path is under an ignoreReleasePaths entry.
3606
+ */
3607
+ isIgnoredReleasePath(workspace) {
3608
+ const ignorePaths = this.ignoreReleasePaths;
3609
+ if (ignorePaths.length === 0) {
3610
+ return false;
3611
+ }
3612
+ return ignorePaths.some(
3613
+ (ignorePath) => this.pather.isSubPath(workspace.path, ignorePath)
3614
+ );
3615
+ }
3616
+ /**
3617
+ * Workspaces that should receive a GitHub Release in createRelease mode.
3618
+ *
3619
+ * Requires `tagName`. Skips dependency-release packages and paths matched
3620
+ * by {@link GithubProps.ignoreReleasePaths}. Private packages are included
3621
+ * when they have a tag.
3622
+ */
3623
+ getReleaseWorkspaces(workspaces) {
3624
+ return workspaces.filter((workspace) => {
3625
+ if (!workspace.tagName) {
3626
+ return false;
3627
+ }
3628
+ if (workspace.dependencyRelease) {
3629
+ return false;
3630
+ }
3631
+ if (this.isIgnoredReleasePath(workspace)) {
3632
+ this.logger.info(
3633
+ `Skip GitHub Release for ignored path: ${workspace.path} (${workspace.tagName})`
3634
+ );
3635
+ return false;
3636
+ }
3637
+ return true;
3638
+ });
3639
+ }
3640
+ /**
3641
+ * Create a GitHub Release for each eligible workspace.
3642
+ *
3643
+ * Release notes use the enriched workspace changelog from `onExec`.
3644
+ */
3645
+ async createGithubReleases(workspaces) {
3646
+ const releaseWorkspaces = this.getReleaseWorkspaces(workspaces);
3647
+ if (releaseWorkspaces.length === 0) {
3648
+ this.logger.warn("No workspaces eligible for GitHub Release creation");
3649
+ return;
3650
+ }
3651
+ this.logger.info(
3652
+ `Creating GitHub Releases (${releaseWorkspaces.length}):
3653
+ ${releaseWorkspaces.map((w) => ` ${w.tagName}`).join("\n")}`
3654
+ );
3655
+ await Promise.all(
3656
+ releaseWorkspaces.map(
3657
+ (workspace) => this.githubManager.createRelease(workspace)
3658
+ )
3659
+ );
3660
+ }
3505
3661
  /**
3506
3662
  * Create the release PR and optionally auto-merge it.
3507
3663
  *
package/dist/index.d.ts CHANGED
@@ -253,7 +253,7 @@ interface ReleaseFormatterConfig {
253
253
 
254
254
  /**
255
255
  * @module Github
256
- * @description GitHub changelog enrichment and release PR plugin
256
+ * @description GitHub changelog enrichment, release PR, and GitHub Release plugin
257
257
  *
258
258
  * Third plugin in the default release pipeline (after {@link Workspaces} and
259
259
  * {@link ChangesetVersion}). Extends {@link GitBase} for git operations and
@@ -262,33 +262,48 @@ interface ReleaseFormatterConfig {
262
262
  * Pipeline phases:
263
263
  * - **onBefore**: validate GitHub token; seed {@link ReleaseFormatter} context
264
264
  * - **onExec**: enrich workspace changelogs with PR/commit links via {@link GithubChangelog}
265
- * - **onSuccess**: create release branch, commit, push, and open PR (unless skipped)
265
+ * - **onSuccess**:
266
+ * - `createPR` — create release branch, commit, push, and open PR (unless skipped)
267
+ * - `createRelease` — create a GitHub Release per workspace (tag + changelog body)
266
268
  *
267
- * Release branch flow:
269
+ * Release branch flow (`createPR`):
268
270
  * 1. `ReleaseFormatter.getReleaseBranch()` — derive branch and tag names from templates
269
271
  * 2. Create branch from `sourceBranch`, commit version/changelog changes, push
270
272
  * 3. Open PR with formatted title/body and optional labels
271
273
  * 4. Auto-merge when `autoMergeReleasePr` is enabled
272
274
  *
275
+ * GitHub Release flow (`createRelease`):
276
+ * 1. Filter workspaces with `tagName`, skipping `dependencyRelease` and
277
+ * paths matched by {@link GithubProps.ignoreReleasePaths}
278
+ * 2. Call {@link GithubManager.createRelease} with workspace changelog as notes
279
+ *
273
280
  * @example Skip PR creation (local dry-run)
274
281
  * ```bash
275
282
  * fe-release --github.skip-create-release-pr --dry-run
276
283
  * ```
277
284
  *
285
+ * @example Publish + GitHub Releases (ignore examples)
286
+ * ```bash
287
+ * fe-release --changesetVersion.mode publish \
288
+ * --github.mode createRelease \
289
+ * --github.ignore-release-paths examples
290
+ * ```
291
+ *
278
292
  * @example fe-config label and merge settings
279
293
  * ```json
280
294
  * {
281
295
  * "release": {
282
296
  * "github": {
283
297
  * "autoMergeReleasePr": false,
284
- * "label": { "name": "CI-Release" }
298
+ * "label": { "name": "CI-Release" },
299
+ * "ignoreReleasePaths": ["examples"]
285
300
  * }
286
301
  * }
287
302
  * }
288
303
  * ```
289
304
  */
290
305
 
291
- type GithubMode = 'createPR';
306
+ type GithubMode = 'createPR' | 'createRelease';
292
307
  type GithubLabel = {
293
308
  /**
294
309
  * Hexadecimal color code for label appearance
@@ -345,8 +360,8 @@ interface GithubProps extends ReleaseFormatterConfig, GitBaseProps {
345
360
  /**
346
361
  * Plugin work mode
347
362
  *
348
- * Currently only `createPR` is supported: enrich changelogs in `onExec`,
349
- * then create release branch and PR in `onSuccess`.
363
+ * - `createPR`: enrich changelogs in `onExec`, then create release branch and PR in `onSuccess`
364
+ * - `createRelease`: enrich changelogs in `onExec`, then create GitHub Releases in `onSuccess`
350
365
  *
351
366
  * @default `'createPR'`
352
367
  */
@@ -423,6 +438,22 @@ interface GithubProps extends ReleaseFormatterConfig, GitBaseProps {
423
438
  * @default false
424
439
  */
425
440
  skipCreateReleasePr?: boolean;
441
+ /**
442
+ * Workspace path prefixes to skip when creating GitHub Releases.
443
+ *
444
+ * Matched with segment-aware path containment (e.g. `examples` skips
445
+ * `examples/react-seed`). Applies only in `createRelease` mode.
446
+ *
447
+ * CLI: `--github.ignore-release-paths`
448
+ * fe-config: `release.github.ignoreReleasePaths`
449
+ *
450
+ * @default `[]`
451
+ * @example
452
+ * ```json
453
+ * { "ignoreReleasePaths": ["examples", "apps/docs"] }
454
+ * ```
455
+ */
456
+ ignoreReleasePaths?: string[];
426
457
  /**
427
458
  * Configuration for release pull request labels
428
459
  *
@@ -856,9 +887,10 @@ interface ChangeLogInterface {
856
887
  * - **onSuccess**: run version and/or publish flow based on `mode`
857
888
  *
858
889
  * Publish flow (`mode: 'publish'` or second half of `'both'`):
859
- * 1. Run `changeset publish` (npm publish + local `git tag` only)
890
+ * 1. Run `changeset publish` (npm publish + local `git tag` for public packages)
860
891
  * 2. Sync workspaces from disk so `tagName` is set from the release version
861
- * 3. Push `workspace.tagName` refs to `origin`
892
+ * 3. Create local tags for `private` packages (changesets skips tagging them)
893
+ * 4. Push `workspace.tagName` refs that exist locally to `origin`
862
894
  *
863
895
  * Version flow (`mode: 'version'` or first half of `'both'`):
864
896
  * 1. Write `.changeset/*.md` files for directly changed packages only
@@ -992,11 +1024,31 @@ declare class ChangesetVersion extends ScriptPlugin<ReleaseContext, ChangesetVer
992
1024
  protected logDryRun(message: string): void;
993
1025
  protected runVersionFlow(): Promise<void>;
994
1026
  protected runChangesetPublish(): Promise<void>;
1027
+ /**
1028
+ * Collect tag names that should be pushed after `changeset publish`.
1029
+ *
1030
+ * Includes public packages (tagged by changesets) and private packages
1031
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1032
+ * workspaces.
1033
+ */
1034
+ protected collectPushableReleaseTags(workspaces: WorkspaceInterface[]): string[];
1035
+ /**
1036
+ * List local git tag names (short refs under `refs/tags/`).
1037
+ */
1038
+ protected listLocalTags(): Promise<Set<string>>;
1039
+ /**
1040
+ * Create local git tags for private packages after `changeset publish`.
1041
+ *
1042
+ * Changesets skips npm publish and tagging for `private: true` packages.
1043
+ * Release still needs those tags as changelog baselines, so create them here.
1044
+ */
1045
+ protected ensurePrivateReleaseTags(workspaces: WorkspaceInterface[]): Promise<void>;
995
1046
  /**
996
1047
  * Push release tags from synced workspaces' `tagName` fields.
997
1048
  *
998
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
999
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1049
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1050
+ * tags that exist as local refs. Missing public-package tags are skipped
1051
+ * (changeset may have skipped publish); they are not invented here.
1000
1052
  */
1001
1053
  protected pushWorkspaceReleaseTags(workspaces: WorkspaceInterface[]): Promise<void>;
1002
1054
  protected syncWorkspaces(workspaces: WorkspaceInterface[]): void;
@@ -1444,6 +1496,10 @@ declare const releaseJson: {
1444
1496
  readonly branchName: "release/${repoName}-${releaseId}";
1445
1497
  readonly releaseTagName: "release-tag-${count}-patch-${releaseId}";
1446
1498
  readonly commitMessage: "chore(release): ${spaces}";
1499
+ /** GitHub release title template (createRelease mode) */
1500
+ readonly releaseName: "${name}@${version}";
1501
+ /** Path prefixes skipped when creating GitHub releases */
1502
+ readonly ignoreReleasePaths: readonly string[];
1447
1503
  readonly label: {
1448
1504
  readonly name: "CI-Release";
1449
1505
  readonly color: "1A7F37";
package/dist/index.js CHANGED
@@ -27,6 +27,10 @@ var releaseJson = {
27
27
  branchName: "release/${repoName}-${releaseId}",
28
28
  releaseTagName: "release-tag-${count}-patch-${releaseId}",
29
29
  commitMessage: "chore(release): ${spaces}",
30
+ /** GitHub release title template (createRelease mode) */
31
+ releaseName: "${name}@${version}",
32
+ /** Path prefixes skipped when creating GitHub releases */
33
+ ignoreReleasePaths: [],
30
34
  label: {
31
35
  name: "CI-Release",
32
36
  color: "1A7F37",
@@ -548,6 +552,9 @@ function workspaceVersionSummary(workspace) {
548
552
  function shouldProcessWorkspace(workspace, ignoreNonUpdatedPackages) {
549
553
  return !(ignoreNonUpdatedPackages && workspace.dependencyRelease);
550
554
  }
555
+ function isPrivateWorkspace(workspace) {
556
+ return workspace.packageJson?.private === true;
557
+ }
551
558
  function readJson(path) {
552
559
  const packageJsonContent = readFileSync(path, "utf-8");
553
560
  return JSON.parse(packageJsonContent);
@@ -1689,9 +1696,18 @@ ${workspaces?.map((workspace) => workspace.toString()).join("\n")}`
1689
1696
  this.logDryRun("Would run: changeset publish, publish packages and tags");
1690
1697
  const preview = this.mergeWorkspaces(workspaces);
1691
1698
  this.context.setWorkspaces(preview);
1692
- const previewTags = preview.filter(
1693
- (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && workspace.tagName
1699
+ const previewTags = this.collectPushableReleaseTags(preview);
1700
+ const privateTags = preview.filter(
1701
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1694
1702
  ).map((workspace) => workspace.tagName);
1703
+ if (privateTags.length > 0) {
1704
+ this.logDryRun(
1705
+ `Would create local tag(s) for private package(s):
1706
+ ${[
1707
+ ...new Set(privateTags)
1708
+ ].map((tag) => ` ${tag}`).join("\n")}`
1709
+ );
1710
+ }
1695
1711
  this.logDryRun(
1696
1712
  previewTags.length === 0 ? "Would push release tag(s) from workspace.tagName: none" : `Would push release tag(s) from workspace.tagName:
1697
1713
  ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
@@ -1705,37 +1721,106 @@ ${previewTags.map((tag) => ` ${tag}`).join("\n")}`
1705
1721
  if (workspaces.length > 0) {
1706
1722
  this.syncWorkspaces(workspaces);
1707
1723
  }
1724
+ const syncedWorkspaces = this.context.workspaces ?? workspaces;
1725
+ await this.step({
1726
+ label: "Create Private Package Tags",
1727
+ task: () => this.ensurePrivateReleaseTags(syncedWorkspaces)
1728
+ });
1708
1729
  await this.step({
1709
1730
  label: "Push Release Tags",
1710
- task: () => this.pushWorkspaceReleaseTags(this.context.workspaces ?? workspaces)
1731
+ task: () => this.pushWorkspaceReleaseTags(syncedWorkspaces)
1711
1732
  });
1712
1733
  }
1713
1734
  /**
1714
- * Push release tags from synced workspaces' `tagName` fields.
1735
+ * Collect tag names that should be pushed after `changeset publish`.
1715
1736
  *
1716
- * `changeset publish` only creates tags locally. After {@link syncWorkspaces},
1717
- * each publishable workspace should already have `tagName` (e.g. `pkg@1.2.3`).
1737
+ * Includes public packages (tagged by changesets) and private packages
1738
+ * (tagged by {@link ensurePrivateReleaseTags}). Skips dependency-release
1739
+ * workspaces.
1718
1740
  */
1719
- async pushWorkspaceReleaseTags(workspaces) {
1741
+ collectPushableReleaseTags(workspaces) {
1720
1742
  const tags = workspaces.filter(
1721
1743
  (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && !!workspace.tagName
1722
1744
  ).map((workspace) => workspace.tagName);
1723
- const uniqueTags = [...new Set(tags)];
1745
+ return [...new Set(tags)];
1746
+ }
1747
+ /**
1748
+ * List local git tag names (short refs under `refs/tags/`).
1749
+ */
1750
+ async listLocalTags() {
1751
+ const output = await this.shell.exec(
1752
+ 'git for-each-ref --format="%(refname:short)" refs/tags/',
1753
+ { dryRun: false, silent: true }
1754
+ );
1755
+ const tags = output.split("\n").map((line) => line.trim()).filter(Boolean);
1756
+ return new Set(tags);
1757
+ }
1758
+ /**
1759
+ * Create local git tags for private packages after `changeset publish`.
1760
+ *
1761
+ * Changesets skips npm publish and tagging for `private: true` packages.
1762
+ * Release still needs those tags as changelog baselines, so create them here.
1763
+ */
1764
+ async ensurePrivateReleaseTags(workspaces) {
1765
+ const privateWorkspaces = workspaces.filter(
1766
+ (workspace) => this.shouldProcessWorkspace(workspace) && !workspace.dependencyRelease && isPrivateWorkspace(workspace) && !!workspace.tagName
1767
+ );
1768
+ if (privateWorkspaces.length === 0) {
1769
+ return;
1770
+ }
1771
+ const localTags = await this.listLocalTags();
1772
+ for (const workspace of privateWorkspaces) {
1773
+ const tagName = workspace.tagName;
1774
+ if (localTags.has(tagName)) {
1775
+ this.logger.debug(`Private package tag already exists: ${tagName}`);
1776
+ continue;
1777
+ }
1778
+ this.logger.info(
1779
+ `Create local release tag for private package: ${tagName}`
1780
+ );
1781
+ await this.shell.exec(["git", "tag", tagName]);
1782
+ localTags.add(tagName);
1783
+ }
1784
+ }
1785
+ /**
1786
+ * Push release tags from synced workspaces' `tagName` fields.
1787
+ *
1788
+ * After {@link syncWorkspaces} and {@link ensurePrivateReleaseTags}, push
1789
+ * tags that exist as local refs. Missing public-package tags are skipped
1790
+ * (changeset may have skipped publish); they are not invented here.
1791
+ */
1792
+ async pushWorkspaceReleaseTags(workspaces) {
1793
+ const uniqueTags = this.collectPushableReleaseTags(workspaces);
1724
1794
  if (uniqueTags.length === 0) {
1725
1795
  this.logger.warn(
1726
1796
  "No workspace.tagName found after publish sync; skip tag push"
1727
1797
  );
1728
1798
  return;
1729
1799
  }
1800
+ const localTags = await this.listLocalTags();
1801
+ const existingTags = uniqueTags.filter((tag) => localTags.has(tag));
1802
+ const missingTags = uniqueTags.filter((tag) => !localTags.has(tag));
1803
+ if (missingTags.length > 0) {
1804
+ this.logger.warn(
1805
+ `Skip push for missing local tag(s):
1806
+ ${missingTags.map((tag) => ` ${tag}`).join("\n")}`
1807
+ );
1808
+ }
1809
+ if (existingTags.length === 0) {
1810
+ this.logger.warn(
1811
+ "No local release tags found to push after publish; skip tag push"
1812
+ );
1813
+ return;
1814
+ }
1730
1815
  this.logger.info(
1731
- `Pushing ${uniqueTags.length} release tag(s) to origin:
1732
- ${uniqueTags.map((tag) => ` ${tag}`).join("\n")}`
1816
+ `Pushing ${existingTags.length} release tag(s) to origin:
1817
+ ${existingTags.map((tag) => ` ${tag}`).join("\n")}`
1733
1818
  );
1734
1819
  await this.shell.exec([
1735
1820
  "git",
1736
1821
  "push",
1737
1822
  "origin",
1738
- ...uniqueTags.map((tag) => `"refs/tags/${tag}"`)
1823
+ ...existingTags.map((tag) => `"refs/tags/${tag}"`)
1739
1824
  ]);
1740
1825
  }
1741
1826
  syncWorkspaces(workspaces) {
@@ -2620,7 +2705,7 @@ var GithubManager = class {
2620
2705
  */
2621
2706
  getOctokitReleaseOptions(options) {
2622
2707
  const {
2623
- releaseName,
2708
+ releaseName = releaseJson.github.releaseName,
2624
2709
  draft = false,
2625
2710
  preRelease = false,
2626
2711
  autoGenerate = false,
@@ -2629,7 +2714,7 @@ var GithubManager = class {
2629
2714
  discussionCategoryName = void 0
2630
2715
  } = this.context.getParameters("github");
2631
2716
  const name = releaseName;
2632
- const body = autoGenerate ? "" : this.truncateBody(String(releaseNotes));
2717
+ const body = autoGenerate ? "" : this.truncateBody(String(releaseNotes ?? ""));
2633
2718
  return {
2634
2719
  name,
2635
2720
  make_latest: makeLatest.toString(),
@@ -3393,6 +3478,7 @@ var ReleaseFormatter = class {
3393
3478
  var Github = class extends GitBase {
3394
3479
  releaseFormatter;
3395
3480
  githubManager;
3481
+ pather = new Pather();
3396
3482
  constructor(context, props = {}) {
3397
3483
  super(context, "github", props);
3398
3484
  this.releaseFormatter = new ReleaseFormatter(
@@ -3404,6 +3490,9 @@ var Github = class extends GitBase {
3404
3490
  get mode() {
3405
3491
  return this.config.mode || releaseJson.github.mode;
3406
3492
  }
3493
+ get ignoreReleasePaths() {
3494
+ return this.config.ignoreReleasePaths ?? [...releaseJson.github.ignoreReleasePaths];
3495
+ }
3407
3496
  async onBefore() {
3408
3497
  await super.onBefore();
3409
3498
  await this.githubManager.validateToken();
@@ -3429,6 +3518,9 @@ var Github = class extends GitBase {
3429
3518
  if (workspace.dependencyRelease) {
3430
3519
  return workspace;
3431
3520
  }
3521
+ if (this.mode === "createRelease" && this.isIgnoredReleasePath(workspace)) {
3522
+ return workspace;
3523
+ }
3432
3524
  return githubChangelog.enrichWorkspaceChangelog(workspace);
3433
3525
  })
3434
3526
  )
@@ -3437,6 +3529,13 @@ var Github = class extends GitBase {
3437
3529
  }
3438
3530
  async onSuccess() {
3439
3531
  const workspaces = this.context.requireWorkspaces();
3532
+ if (this.mode === "createRelease") {
3533
+ await this.step({
3534
+ label: "Create GitHub Releases",
3535
+ task: () => this.createGithubReleases(workspaces)
3536
+ });
3537
+ return;
3538
+ }
3440
3539
  const releaseBranchResult = await this.step({
3441
3540
  label: "Create Release Branch",
3442
3541
  task: () => this.createReleaseBranch(workspaces)
@@ -3453,6 +3552,63 @@ var Github = class extends GitBase {
3453
3552
  }
3454
3553
  await this.handleReleasePullRequest(workspaces, releaseBranchResult);
3455
3554
  }
3555
+ /**
3556
+ * Whether this workspace path is under an ignoreReleasePaths entry.
3557
+ */
3558
+ isIgnoredReleasePath(workspace) {
3559
+ const ignorePaths = this.ignoreReleasePaths;
3560
+ if (ignorePaths.length === 0) {
3561
+ return false;
3562
+ }
3563
+ return ignorePaths.some(
3564
+ (ignorePath) => this.pather.isSubPath(workspace.path, ignorePath)
3565
+ );
3566
+ }
3567
+ /**
3568
+ * Workspaces that should receive a GitHub Release in createRelease mode.
3569
+ *
3570
+ * Requires `tagName`. Skips dependency-release packages and paths matched
3571
+ * by {@link GithubProps.ignoreReleasePaths}. Private packages are included
3572
+ * when they have a tag.
3573
+ */
3574
+ getReleaseWorkspaces(workspaces) {
3575
+ return workspaces.filter((workspace) => {
3576
+ if (!workspace.tagName) {
3577
+ return false;
3578
+ }
3579
+ if (workspace.dependencyRelease) {
3580
+ return false;
3581
+ }
3582
+ if (this.isIgnoredReleasePath(workspace)) {
3583
+ this.logger.info(
3584
+ `Skip GitHub Release for ignored path: ${workspace.path} (${workspace.tagName})`
3585
+ );
3586
+ return false;
3587
+ }
3588
+ return true;
3589
+ });
3590
+ }
3591
+ /**
3592
+ * Create a GitHub Release for each eligible workspace.
3593
+ *
3594
+ * Release notes use the enriched workspace changelog from `onExec`.
3595
+ */
3596
+ async createGithubReleases(workspaces) {
3597
+ const releaseWorkspaces = this.getReleaseWorkspaces(workspaces);
3598
+ if (releaseWorkspaces.length === 0) {
3599
+ this.logger.warn("No workspaces eligible for GitHub Release creation");
3600
+ return;
3601
+ }
3602
+ this.logger.info(
3603
+ `Creating GitHub Releases (${releaseWorkspaces.length}):
3604
+ ${releaseWorkspaces.map((w) => ` ${w.tagName}`).join("\n")}`
3605
+ );
3606
+ await Promise.all(
3607
+ releaseWorkspaces.map(
3608
+ (workspace) => this.githubManager.createRelease(workspace)
3609
+ )
3610
+ );
3611
+ }
3456
3612
  /**
3457
3613
  * Create the release PR and optionally auto-merge it.
3458
3614
  *
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.5",
4
+ "version": "5.1.0",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "homepage": "https://github.com/qlover/fe-base/tree/master/packages/fe-release",
@@ -60,7 +60,7 @@
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",
63
+ "@qlover/fe-corekit": "3.4.4",
64
64
  "@qlover/logger": "1.2.1",
65
65
  "@qlover/scripts-context": "2.3.3"
66
66
  },