@exadev/semantic-release-workspace 3.0.3 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -5
  2. package/dist/cli.js +24 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,11 +66,7 @@ Everything above describes `commitStrategy: 'per-package'`, the default: unchang
66
66
  | Default publish plugins | `DEFAULT_PUBLISH_PLUGINS` (changelog, npm, github, git) | `SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS` — the same list minus git |
67
67
  | Crash recovery | A bump commit already sitting in git history (from this run or an earlier one) is recognised via a machine-parseable trailer, so a run that resumes after a partial push still forces the right dependent releases | Not needed: either the whole combined commit lands and pushes, or the run fails before touching git at all, so there is never a partial push to recover from |
68
68
 
69
- Everything about *what* releases (topological order, forced-patch dependents, dependency-range classification, path-scoped commit analysis, unsupported-range rejection) is identical between the two modes — `commitStrategy` only changes how the result is committed, tagged, and pushed. ## Tag format
70
-
71
- Each package's release tag comes from the `tagFormat` option, a lodash template with `${name}` and `${version}` placeholders, defaulting to `'${name}@${version}'`. Set it via the `tagFormat` field in a `--config` file or the `tagFormat` option to `releaseWorkspace()`. The template must contain `${version}` and may contain `${name}`; anything else is rejected before any package releases.
72
-
73
- Override it when the tags are consumed as refs outside git: GitHub Actions pins composite actions as `owner/repo/path@ref`, and GitHub's workflow parser rejects a ref containing `@` -- so a repository of actions sets `'${name}-v${version}'` and every workflow pins `...@<action>-v<major>`. Note that switching an existing repository's format starts a fresh tag namespace: semantic-release will not see prior releases recorded under the old format, so rename existing tags to the new template as part of the switch.
69
+ Everything about *what* releases (topological order, forced-patch dependents, dependency-range classification, path-scoped commit analysis, unsupported-range rejection) is identical between the two modes — `commitStrategy` only changes how the result is committed, tagged, and pushed.
74
70
 
75
71
  Set it via the `--commit-strategy <mode>` CLI flag, the `commitStrategy` field in a `--config` file, or the `commitStrategy` option to `releaseWorkspace()`; omit it and nothing changes.
76
72
 
@@ -84,6 +80,18 @@ One window is outside this tool's control: semantic-release pushes a package's t
84
80
 
85
81
  If you need a run to be strictly all-or-nothing, use `commitStrategy: 'single'`, which makes one commit and one atomic push before anything publishes, and which retries the whole attempt against the new tip when that push loses a race.
86
82
 
83
+ ## Tag format
84
+
85
+ Each package's release tag comes from the `tagFormat` option, a lodash template with `${name}` and `${version}` placeholders, defaulting to `'${name}@${version}'`. Set it with the `--tag-format <template>` CLI flag, the `tagFormat` field in a `--config` file, or the `tagFormat` option to `releaseWorkspace()`; when both the flag and the config file set it, the flag wins. The template must contain `${version}` and may contain `${name}`; anything else is rejected before any package releases.
86
+
87
+ Override it when the tags are consumed as refs outside git: GitHub Actions pins composite actions as `owner/repo/path@ref`, and GitHub's workflow parser rejects a ref containing `@` -- so a repository of actions sets `'${name}-v${version}'` and every workflow pins `...@<action>-v<major>`. Note that switching an existing repository's format starts a fresh tag namespace: semantic-release will not see prior releases recorded under the old format, so rename existing tags to the new template as part of the switch.
88
+
89
+ ```sh
90
+ pnpm exec semantic-release-workspace release --tag-format '${name}-v${version}'
91
+ ```
92
+
93
+ A gated run records each package's tag in its state file, so `resume` takes no tag format of its own.
94
+
87
95
  ## Gating publish
88
96
 
89
97
  `commitStrategy` changes how a release is committed; `gatePublish` changes *when* it gets published relative to being tagged and pushed -- a different, orthogonal axis. With `gatePublish: true`, each due package is tagged and pushed via [@exadev/release-gate](https://www.npmjs.com/package/@exadev/release-gate) (a real dependency of this package -- no separate install needed) but never published: nothing calls `npm publish`, creates a GitHub Release, or runs any other configured publish step, until a separate `resume` step does so explicitly -- from the same process, or a completely different one (a later CI job, once a deploy or smoke test has confirmed the release should actually go out).
@@ -219,6 +227,7 @@ Run it from the workspace root (or pass `--root <directory>`). A dry run analyse
219
227
  | `--analyze-commits <json>` | Options for the wrapped @semantic-release/commit-analyzer (e.g. `'{"preset":"conventionalcommits","releaseRules":[...]}'`) |
220
228
  | `--generate-notes <json>` | Options for the wrapped @semantic-release/release-notes-generator |
221
229
  | `--commit-strategy <mode>` | `per-package` (default) or `single` — see [Commit strategies](#commit-strategies) |
230
+ | `--tag-format <template>` | Template for each package's release tag, with `${name}` and `${version}` placeholders; must contain `${version}` (default: `${name}@${version}`) — see [Tag format](#tag-format) |
222
231
  | `--gate-publish` | Tag and push each due package, but defer publishing — see [Gating publish](#gating-publish). Requires `--gate-state-file`; rejected with `--commit-strategy single` |
223
232
  | `--gate-state-file <path>` | With `--gate-publish`: where to write the state a later `resume` run needs |
224
233
  | `--config <file>` | A config file (`.json`, `.yaml`, `.yml`, `.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, or `.mts`, loaded via [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig)) providing any of the above, plus `packagePlugins` (see [Per-package publish plugins](#per-package-publish-plugins)), through its default export; explicit flags win. TypeScript files are run by Node's own type stripping, so they may use only erasable type syntax (annotations and `import type`, not `enum` or `namespace`) |
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { generateNotes } from "@semantic-release/release-notes-generator";
14
14
  import semanticRelease from "semantic-release";
15
15
  import { pathToFileURL } from "node:url";
16
16
  //#region package.json
17
- var version = "3.0.3";
17
+ var version = "3.1.1";
18
18
  //#endregion
19
19
  //#region src/errors.ts
20
20
  /**
@@ -1637,7 +1637,8 @@ const CONFIG_OPTION_KEYS = /* @__PURE__ */ new Set([
1637
1637
  "analyzeCommits",
1638
1638
  "generateNotes",
1639
1639
  "commitStrategy",
1640
- "gatePublish"
1640
+ "gatePublish",
1641
+ "tagFormat"
1641
1642
  ]);
1642
1643
  const COMMIT_STRATEGIES = /* @__PURE__ */ new Set(["per-package", "single"]);
1643
1644
  function isCommitStrategy(value) {
@@ -1659,9 +1660,10 @@ function createProgram() {
1659
1660
  release.option("--analyze-commits <json>", "options for the wrapped @semantic-release/commit-analyzer, as a JSON object");
1660
1661
  release.option("--generate-notes <json>", "options for the wrapped @semantic-release/release-notes-generator, as a JSON object");
1661
1662
  release.option("--commit-strategy <mode>", "how the run commits its released changes: \"per-package\" (default; today's behaviour, one commit per release plus one per dependency bump) or \"single\" (one combined commit for the whole run, tagged once per released package)", parseCommitStrategy);
1663
+ release.option("--tag-format <template>", "template for each package's release tag, with ${name} and ${version} placeholders; must contain ${version} (default: ${name}@${version})", parseTagFormat);
1662
1664
  release.option("--gate-publish", "tag and push each due package via @exadev/release-gate, but defer publishing -- requires --gate-state-file, and cannot be combined with --commit-strategy single");
1663
1665
  release.option("--gate-state-file <path>", "with --gate-publish: where to write the state a later \"resume\" run needs to finish publishing");
1664
- release.option("--config <file>", "config file (.json, .yaml, .yml, .js, .cjs, or .ts) providing any of the release options (dryRun, branches, plugins, packagePlugins, analyzeCommits, generateNotes, commitStrategy, gatePublish); explicit flags win");
1666
+ release.option("--config <file>", "config file (.json, .yaml, .yml, .js, .cjs, or .ts) providing any of the release options (dryRun, branches, plugins, packagePlugins, analyzeCommits, generateNotes, commitStrategy, tagFormat, gatePublish); explicit flags win");
1665
1667
  release.action(runRelease);
1666
1668
  const resume = program.command("resume");
1667
1669
  resume.description("Finish publishing every package a --gate-publish release tagged and pushed but did not publish.");
@@ -1674,6 +1676,20 @@ function parseCommitStrategy(value) {
1674
1676
  if (!isCommitStrategy(value)) throw new InvalidArgumentError(`--commit-strategy must be one of: ${[...COMMIT_STRATEGIES].join(", ")}`);
1675
1677
  return value;
1676
1678
  }
1679
+ function parseTagFormat(value) {
1680
+ return validateTagFormatOption(value, (message) => message);
1681
+ }
1682
+ /**
1683
+ * Runs `validateTagFormat` and turns its `ReleaseConfigurationError` into the `InvalidArgumentError` the CLI reports as a usage mistake. `describe` lets each caller say where the value came from (a flag needs no prefix, since commander already names the option; a config file entry needs the file's path).
1684
+ */
1685
+ function validateTagFormatOption(value, describe) {
1686
+ try {
1687
+ return validateTagFormat(value);
1688
+ } catch (cause) {
1689
+ if (cause instanceof ReleaseConfigurationError) throw new InvalidArgumentError(describe(cause.message));
1690
+ throw cause;
1691
+ }
1692
+ }
1677
1693
  const NO_CONFIG_FILE = {
1678
1694
  dryRun: void 0,
1679
1695
  branches: void 0,
@@ -1682,6 +1698,7 @@ const NO_CONFIG_FILE = {
1682
1698
  analyzeCommits: void 0,
1683
1699
  generateNotes: void 0,
1684
1700
  commitStrategy: void 0,
1701
+ tagFormat: void 0,
1685
1702
  gatePublish: void 0
1686
1703
  };
1687
1704
  async function runRelease(flags) {
@@ -1697,6 +1714,7 @@ async function runRelease(flags) {
1697
1714
  analyzeCommits: flags.analyzeCommits === void 0 ? file.analyzeCommits : parseJsonObjectFlag(flags.analyzeCommits, "--analyze-commits"),
1698
1715
  generateNotes: flags.generateNotes === void 0 ? file.generateNotes : parseJsonObjectFlag(flags.generateNotes, "--generate-notes"),
1699
1716
  commitStrategy: flags.commitStrategy ?? file.commitStrategy,
1717
+ tagFormat: flags.tagFormat ?? file.tagFormat,
1700
1718
  gatePublish
1701
1719
  });
1702
1720
  for (const pkg of outcome.packages) console.log(describeOutcome(pkg));
@@ -1758,7 +1776,7 @@ async function readReleaseConfigFile(path) {
1758
1776
  const parsed = await readConfigFile(path);
1759
1777
  if (!isJsonObject(parsed)) throw new InvalidArgumentError(`--config file ${path} must contain a JSON object`);
1760
1778
  for (const key of Object.keys(parsed)) if (!CONFIG_OPTION_KEYS.has(key)) throw new InvalidArgumentError(`--config file ${path} has an unknown option "${key}"; recognised options: ${[...CONFIG_OPTION_KEYS].join(", ")}`);
1761
- const { dryRun, branches, plugins, packagePlugins, analyzeCommits, generateNotes, commitStrategy, gatePublish } = parsed;
1779
+ const { dryRun, branches, plugins, packagePlugins, analyzeCommits, generateNotes, commitStrategy, tagFormat, gatePublish } = parsed;
1762
1780
  if (dryRun !== void 0 && typeof dryRun !== "boolean") throw new InvalidArgumentError(`--config file ${path}: "dryRun" must be a boolean`);
1763
1781
  if (branches !== void 0 && !isStringArray(branches)) throw new InvalidArgumentError(`--config file ${path}: "branches" must be an array of branch name strings`);
1764
1782
  if (plugins !== void 0 && !Array.isArray(plugins)) throw new InvalidArgumentError(`--config file ${path}: "plugins" must be an array`);
@@ -1766,6 +1784,7 @@ async function readReleaseConfigFile(path) {
1766
1784
  if (analyzeCommits !== void 0 && !isJsonObject(analyzeCommits)) throw new InvalidArgumentError(`--config file ${path}: "analyzeCommits" must be an object`);
1767
1785
  if (generateNotes !== void 0 && !isJsonObject(generateNotes)) throw new InvalidArgumentError(`--config file ${path}: "generateNotes" must be an object`);
1768
1786
  if (commitStrategy !== void 0 && (typeof commitStrategy !== "string" || !isCommitStrategy(commitStrategy))) throw new InvalidArgumentError(`--config file ${path}: "commitStrategy" must be one of: ${[...COMMIT_STRATEGIES].join(", ")}`);
1787
+ if (tagFormat !== void 0 && typeof tagFormat !== "string") throw new InvalidArgumentError(`--config file ${path}: "tagFormat" must be a string`);
1769
1788
  if (gatePublish !== void 0 && typeof gatePublish !== "boolean") throw new InvalidArgumentError(`--config file ${path}: "gatePublish" must be a boolean`);
1770
1789
  return {
1771
1790
  dryRun,
@@ -1775,6 +1794,7 @@ async function readReleaseConfigFile(path) {
1775
1794
  analyzeCommits,
1776
1795
  generateNotes,
1777
1796
  commitStrategy,
1797
+ tagFormat: tagFormat === void 0 ? void 0 : validateTagFormatOption(tagFormat, (message) => `--config file ${path}: ${message}`),
1778
1798
  gatePublish
1779
1799
  };
1780
1800
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exadev/semantic-release-workspace",
3
- "version": "3.0.3",
3
+ "version": "3.1.1",
4
4
  "description": "Independent per-package semantic-release orchestration for pnpm workspaces, without lockstep versioning.",
5
5
  "type": "module",
6
6
  "repository": {