@exadev/semantic-release-workspace 2.1.0 → 3.0.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/README.md +22 -6
- package/dist/cli.js +42 -11
- package/dist/index.cjs +42 -10
- package/dist/index.d.cts +22 -3
- package/dist/index.d.ts +22 -3
- package/dist/index.js +42 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ One orchestrator run, five stages:
|
|
|
14
14
|
|
|
15
15
|
1. **Workspace discovery.** Reads the `packages` globs from `pnpm-workspace.yaml` and every matched package's `package.json` (`dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`), building the inter-package dependency graph keyed by package name. Packages must live in subdirectories (a package at the workspace root touches every commit and cannot be path-scoped) and names must be unique. This stage also resolves the workspace root's own path relative to the git repository's toplevel (`git rev-parse --show-prefix`), because `git log --name-only` always reports changed paths relative to that toplevel, not to `pnpm-workspace.yaml`'s own directory -- the two differ whenever the workspace is nested inside a larger repository, and every package's commit filtering is scoped against the repository-relative path, not the workspace-relative one, so nested workspaces are path-scoped correctly rather than silently matching nothing.
|
|
16
16
|
2. **Topological release ordering.** Kahn's algorithm over the discovered graph, so a package only releases after every workspace sibling it depends on has already released in this run. Ties are broken alphabetically within each dependency layer, so the same workspace always produces the same order. A dependency cycle has no valid order at all — the run fails loudly, naming the loop (`x -> y -> x`), rather than picking one of the wrong answers and publishing a package whose sibling dependency points at a version that does not exist yet.
|
|
17
|
-
3. **Per-package scoped release.** For each package in order, the orchestrator calls semantic-release's programmatic API (`require('semantic-release')`, not the CLI) with `cwd` set to the package's directory, `tagFormat`
|
|
17
|
+
3. **Per-package scoped release.** For each package in order, the orchestrator calls semantic-release's programmatic API (`require('semantic-release')`, not the CLI) with `cwd` set to the package's directory, `tagFormat` resolved from the `tagFormat` option (default `'${name}@${version}'`) so each package's tags stay distinct in the one shared tag namespace, and inline `analyzeCommits`/`generateNotes` plugins. Each wrapper runs one `git log --name-only --no-renames` pass over the same release range semantic-release already analysed (from the package's last matching tag to `HEAD`, or the whole history for a first release), maps every commit to the paths it changed, and filters the commit list down to commits touching the package's own directory **before delegating to the real @semantic-release/commit-analyzer and @semantic-release/release-notes-generator**. Conventional-commit parsing and changelog formatting stay entirely inside the standard plugins — the orchestrator only scopes what they see.
|
|
18
18
|
4. **Cross-package manifest bumping** — the heart of the design, covered in its own section below.
|
|
19
19
|
5. **Standard plugins do the publishing.** @semantic-release/npm, @semantic-release/github, @semantic-release/changelog, and @semantic-release/git run per package exactly as in a single-package repository, scoped by `cwd`. The orchestrator coordinates and sequences them; it does not reimplement npm publishing, GitHub release creation, or changelog file writing.
|
|
20
20
|
|
|
@@ -66,7 +66,13 @@ 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.
|
|
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.
|
|
74
|
+
|
|
75
|
+
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.
|
|
70
76
|
|
|
71
77
|
`'single'` mode still runs each package's own configured publish plugins afterward (npm publish with provenance, GitHub releases), scoped per package exactly as `'per-package'` mode does — it just runs their `verifyConditions`/`publish`/`success` steps directly against the already-committed-and-tagged repository state, since by that point semantic-release's own top-level orchestrator would misread the tag this mode already created as an existing release. `addChannel` and `fail` are not called in this mode (pre-release channel promotion and posting an automated failure comment/issue, respectively) — a deliberate scope boundary, not a silent gap: raise an issue if your workflow needs them.
|
|
72
78
|
|
|
@@ -152,6 +158,7 @@ The core technique is the same one multi-semantic-release proved in production:
|
|
|
152
158
|
- **Bump-only dependents always release.** multi-semantic-release rewrites dependency ranges in the working tree without committing them, so a dependent whose only change is a dependency update can go unreleased until some other commit triggers it. Here the bump is committed before the dependent's turn and a patch release is forced deterministically (see the timing section above).
|
|
153
159
|
- **Loud failures by design.** A dependency cycle, an unsupported dependency range, a publish pipeline without @semantic-release/git (which would leave released manifests uncommitted), an unresolvable plugin, a duplicate package name — each stops the run with a specific error rather than degrading silently. There is deliberately no "skip this package and carry on" path: a partially-consistent set of publishes is worse than none.
|
|
154
160
|
- **Only installable specifiers reach the registry.** `workspace:`, `catalog:`, `link:`, and `file:` specifiers in a publishable package's installed dependency fields are rejected instead of being published as written. In private packages and `devDependencies`, `workspace:*`/`workspace:^`/`workspace:~` are understood as naming no version (bump the release, not the manifest text), and `catalog:` and `npm:` aliases on a workspace sibling are rejected with an explanation instead of being mangled.
|
|
161
|
+
- **Private packages release without advertising themselves.** A package marked `private` takes part in the run exactly as any other, tag and version bump and dependency cascade included, but creates no GitHub Release, because a Release for a package that never reaches a registry both points at nothing installable and takes the repository's Latest label off a package that does.
|
|
155
162
|
- **Workspace-agnostic discovery.** Everything comes from `pnpm-workspace.yaml` and the manifests its globs match; pointing the orchestrator at any pnpm workspace is the entire configuration.
|
|
156
163
|
|
|
157
164
|
Out of scope, on purpose: parallelising independent branches of the dependency graph (packages release sequentially in topological order for correctness first — a real future optimisation, not attempted here), and any Changesets-style explicit-changeset mode, which is a different paradigm rather than a missing feature.
|
|
@@ -219,11 +226,17 @@ Listing `@semantic-release/commit-analyzer` or `@semantic-release/release-notes-
|
|
|
219
226
|
|
|
220
227
|
Note that the orchestrator sets `tagFormat`, `plugins`, `analyzeCommits`, and `generateNotes` explicitly on every per-package run, so those keys in any `release.config.*` found in the workspace are overridden by construction — configure the release through the orchestrator, not through a leftover single-package config.
|
|
221
228
|
|
|
222
|
-
###
|
|
229
|
+
### Private packages and GitHub Releases
|
|
223
230
|
|
|
224
|
-
|
|
231
|
+
A package whose manifest sets `"private": true` gets the workspace-wide plugin list minus `@semantic-release/github`, without being configured to. Nothing else about its release changes: it still gets its version bump, its `name@version` tag and the dependency cascade to its dependents, all of which a dependent's own release can hinge on (a private package whose build output ships inside a published one, for example). What it loses is a public GitHub Release for something nobody can install.
|
|
232
|
+
|
|
233
|
+
That release is not merely redundant. `@semantic-release/github` sets the REST API's `make_latest` from the release branch alone, with no option to opt out, so every release it creates claims the repository's Latest label and the last one created keeps it. Topological order puts a package that depends on the published ones at the end of the run, which is exactly where a private package usually sits, so without this rule the repository's front page advertises an unpublishable package as its current release.
|
|
234
|
+
|
|
235
|
+
To keep the Release for a private package anyway, name it in `packagePlugins` below: an explicit list is taken exactly as written.
|
|
236
|
+
|
|
237
|
+
### Per-package publish plugins
|
|
225
238
|
|
|
226
|
-
|
|
239
|
+
`plugins` is one list for the whole workspace. `packagePlugins` (config file and programmatic API only, since a list keyed by package name has no natural flag form) replaces that list outright for the packages it names, and every other package keeps the workspace-wide list, or the private-package variant of it described above. The override is not merged with either: it is the complete list for that package, subject to the same rules as any other (for instance `@semantic-release/git` is required under `commitStrategy: 'per-package'` and rejected under `'single'`). A name that is not a package in the workspace is rejected, so a misspelling cannot leave a package on the default list unnoticed.
|
|
227
240
|
|
|
228
241
|
```ts
|
|
229
242
|
// release-workspace.config.ts
|
|
@@ -231,7 +244,10 @@ import { DEFAULT_PUBLISH_PLUGINS, type ReleaseWorkspaceOptions } from '@exadev/s
|
|
|
231
244
|
|
|
232
245
|
const config: ReleaseWorkspaceOptions = {
|
|
233
246
|
packagePlugins: {
|
|
234
|
-
|
|
247
|
+
// A private package that does want its GitHub Release, opting back in to the list every public package gets.
|
|
248
|
+
'@acme/internal-tooling': DEFAULT_PUBLISH_PLUGINS,
|
|
249
|
+
// A published package kept out of a step the rest need.
|
|
250
|
+
'@acme/docs-site': DEFAULT_PUBLISH_PLUGINS.filter((plugin) => plugin !== '@semantic-release/npm'),
|
|
235
251
|
},
|
|
236
252
|
};
|
|
237
253
|
|
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 = "
|
|
17
|
+
var version = "3.0.0";
|
|
18
18
|
//#endregion
|
|
19
19
|
//#region src/errors.ts
|
|
20
20
|
/**
|
|
@@ -59,6 +59,21 @@ var PnpmCommandError = class extends WorkspaceReleaseError {
|
|
|
59
59
|
super(`pnpm install --lockfile-only failed in ${cwd}: ${detail}`);
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Validate a caller-supplied tagFormat once per run, before any package releases. The template must contain `${version}` (semantic-release interpolates it; without it every package would compute the same tag and overwrite its predecessor) and may contain `${name}`, which the orchestrator substitutes per package. Any other `${...}` token is a typo or a placeholder semantic-release does not support in tagFormat, so it is rejected here rather than released as a literal into a tag name.
|
|
64
|
+
*/
|
|
65
|
+
function validateTagFormat(tagFormat) {
|
|
66
|
+
if (!tagFormat.includes("${version}")) throw new ReleaseConfigurationError(`tagFormat must contain '\${version}' so each package's release tag carries its version; got: ${tagFormat}`);
|
|
67
|
+
const unknown = [...tagFormat.matchAll(/\$\{([^}]*)\}/g)].map((match) => match[1] ?? "").filter((token) => token !== "name" && token !== "version").filter((token) => token !== "");
|
|
68
|
+
if (unknown.length > 0) throw new ReleaseConfigurationError(`tagFormat supports only the '\${name}' and '\${version}' placeholders; unknown token(s): ${unknown.map((token) => `\${${token}}`).join(", ")}`);
|
|
69
|
+
return tagFormat;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Substitute `${name}` for one package. Called per package at release time; the returned template still carries the literal `${version}` for semantic-release to interpolate, exactly as the default `name@version` format always has.
|
|
73
|
+
*/
|
|
74
|
+
function formatTagForPackage(tagFormat, name) {
|
|
75
|
+
return tagFormat.replaceAll("${name}", name);
|
|
76
|
+
}
|
|
62
77
|
//#endregion
|
|
63
78
|
//#region src/exec-file.ts
|
|
64
79
|
/**
|
|
@@ -587,6 +602,10 @@ function topologicalOrder(graph) {
|
|
|
587
602
|
if (pending.size > 0) throw new DependencyCycleError(findCycle(graph, new Set(pending.keys())));
|
|
588
603
|
return ordered;
|
|
589
604
|
}
|
|
605
|
+
/** The packages a release order names, in that order, for the stages that need each package's manifest rather than only its name. */
|
|
606
|
+
function orderedPackages(graph, order) {
|
|
607
|
+
return order.map((name) => mustGet(graph.packages, name, "package"));
|
|
608
|
+
}
|
|
590
609
|
/**
|
|
591
610
|
* Walks dependency edges between the packages Kahn's algorithm could not place, until it revisits one, so the error can name a concrete loop rather than just a set of packages. Every unplaced package is unplaced precisely because at least one of its own dependencies is too, so the walk always reaches a repeat.
|
|
592
611
|
*/
|
|
@@ -668,6 +687,8 @@ function matchTrailerLine(message, key) {
|
|
|
668
687
|
}
|
|
669
688
|
//#endregion
|
|
670
689
|
//#region src/plugins.ts
|
|
690
|
+
/** The one plugin a private package is kept off by default: see `resolveWorkspacePublishPlugins` for why a package that never reaches a registry does not get a public GitHub Release either. */
|
|
691
|
+
const GITHUB_RELEASE_PLUGIN = "@semantic-release/github";
|
|
671
692
|
/** semantic-release's own `getLastRelease` returns `{}` for a package with no prior tag -- not `undefined`, and not a fully-populated `LastRelease` -- contradicting the `gitHead: string` its own type declares. Narrows structurally rather than trusting that declared type, so a first-release context's `lastRelease` (correctly, at runtime) never claims a `gitHead` it does not have. */
|
|
672
693
|
function hasGitHead(lastRelease) {
|
|
673
694
|
return typeof lastRelease === "object" && lastRelease !== null && "gitHead" in lastRelease && typeof lastRelease.gitHead === "string";
|
|
@@ -794,13 +815,17 @@ function resolvePublishPlugins(specs, workspaceRoot, options) {
|
|
|
794
815
|
/**
|
|
795
816
|
* Resolves the publish plugin list of every package in the workspace: the workspace-wide list for each package, except where `packagePlugins` names a package, whose own list replaces it. Every list is held to the same rules as `resolvePublishPlugins` applies to a workspace-wide one, and a failure in an override names the package it belongs to.
|
|
796
817
|
*
|
|
818
|
+
* A package whose manifest sets `private: true` and which no `packagePlugins` entry names gets the workspace-wide list minus `@semantic-release/github`. A private package is never published, so a public GitHub Release for it advertises something nobody can install, and that release is not merely redundant: `@semantic-release/github` sets `make_latest` from the release branch alone, with no option to opt out, so every release it creates claims the repository's Latest label and the last one created keeps it. A private package sitting at the end of the topological order (which is where a package that depends on the published ones necessarily sits) therefore takes the label on every run. Everything the private package actually needs from the run is untouched: its version bump, its tag, and the dependency cascade to its dependents all come from the other plugins and from the orchestrator itself.
|
|
819
|
+
*
|
|
820
|
+
* A `packagePlugins` entry naming a private package is taken exactly as written, `@semantic-release/github` included, so a workspace that does want a Release for a private package can still say so.
|
|
821
|
+
*
|
|
797
822
|
* A `packagePlugins` key that matches no package is rejected rather than ignored: a misspelt name would otherwise leave the package it meant on the workspace-wide list with nothing to say so, which for the intended use (keeping a package out of a step such as GitHub Release creation) is a silent wrong result.
|
|
798
823
|
*/
|
|
799
|
-
function resolveWorkspacePublishPlugins(
|
|
824
|
+
function resolveWorkspacePublishPlugins(packages, specs, workspaceRoot, options) {
|
|
800
825
|
const overrides = specs.packagePlugins === void 0 ? [] : Object.entries(specs.packagePlugins);
|
|
801
|
-
const known = new Set(
|
|
826
|
+
const known = new Set(packages.map((pkg) => pkg.name));
|
|
802
827
|
const unknown = overrides.map(([name]) => name).filter((name) => !known.has(name));
|
|
803
|
-
if (unknown.length > 0) throw new ReleaseConfigurationError(`packagePlugins names ${unknown.map((name) => `"${name}"`).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a package in this workspace. Packages: ${
|
|
828
|
+
if (unknown.length > 0) throw new ReleaseConfigurationError(`packagePlugins names ${unknown.map((name) => `"${name}"`).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a package in this workspace. Packages: ${[...known].join(", ")}.`);
|
|
804
829
|
const workspaceWide = resolvePublishPlugins(specs.plugins, workspaceRoot, options);
|
|
805
830
|
const resolvedOverrides = /* @__PURE__ */ new Map();
|
|
806
831
|
for (const [name, list] of overrides) try {
|
|
@@ -809,7 +834,9 @@ function resolveWorkspacePublishPlugins(packageNames, specs, workspaceRoot, opti
|
|
|
809
834
|
if (cause instanceof ReleaseConfigurationError) throw new ReleaseConfigurationError(`packagePlugins for "${name}": ${cause.message}`);
|
|
810
835
|
throw cause;
|
|
811
836
|
}
|
|
812
|
-
|
|
837
|
+
const privateSpecs = specs.plugins.filter((spec) => parsePublishPluginSpec(spec)[0] !== GITHUB_RELEASE_PLUGIN);
|
|
838
|
+
const forPrivatePackages = privateSpecs.length === specs.plugins.length ? workspaceWide : resolvePublishPlugins(privateSpecs, workspaceRoot, options);
|
|
839
|
+
return new Map(packages.map((pkg) => [pkg.name, resolvedOverrides.get(pkg.name) ?? (pkg.private ? forPrivatePackages : workspaceWide)]));
|
|
813
840
|
}
|
|
814
841
|
/**
|
|
815
842
|
* Resolves a plugin module name to an absolute file path, first from this tool's own module context (its peer dependencies, which every workspace installing the orchestrator must provide) and then from the workspace root (a workspace's own plugin dependencies, such as a custom changelog plugin). Both bases are named in the error when neither can resolve the name.
|
|
@@ -871,7 +898,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
871
898
|
validateDependencyRanges(graph);
|
|
872
899
|
const order = topologicalOrder(graph);
|
|
873
900
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
874
|
-
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
901
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
875
902
|
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
876
903
|
packagePlugins: options.packagePlugins
|
|
877
904
|
}, workspace.root, {
|
|
@@ -880,6 +907,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
880
907
|
});
|
|
881
908
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
882
909
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
910
|
+
const tagFormat = validateTagFormat(options.tagFormat ?? "${name}@${version}");
|
|
883
911
|
const capturedCommits = /* @__PURE__ */ new Map();
|
|
884
912
|
const captured = {
|
|
885
913
|
branch: void 0,
|
|
@@ -893,6 +921,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
893
921
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
894
922
|
pendingBumps.delete(name);
|
|
895
923
|
const nextRelease = await analysePackage(pkg, {
|
|
924
|
+
tagFormat,
|
|
896
925
|
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
897
926
|
analyzeCommitsConfig,
|
|
898
927
|
generateNotesConfig,
|
|
@@ -1015,7 +1044,7 @@ async function analysePackage(pkg, options) {
|
|
|
1015
1044
|
onCommitsResolved: options.onCommitsResolved
|
|
1016
1045
|
});
|
|
1017
1046
|
const semanticReleaseOptions = {
|
|
1018
|
-
tagFormat:
|
|
1047
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1019
1048
|
plugins: options.resolvedPlugins,
|
|
1020
1049
|
dryRun: true,
|
|
1021
1050
|
async analyzeCommits(pluginConfig, context) {
|
|
@@ -1153,7 +1182,7 @@ async function releaseWorkspace(options = {}) {
|
|
|
1153
1182
|
validateDependencyRanges(graph);
|
|
1154
1183
|
const order = topologicalOrder(graph);
|
|
1155
1184
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1156
|
-
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1185
|
+
const publishPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
1157
1186
|
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1158
1187
|
packagePlugins: options.packagePlugins
|
|
1159
1188
|
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
@@ -1163,6 +1192,7 @@ async function releaseWorkspace(options = {}) {
|
|
|
1163
1192
|
order,
|
|
1164
1193
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1165
1194
|
const result = await runPackageRelease(pkg, {
|
|
1195
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1166
1196
|
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1167
1197
|
analyzeCommitsConfig,
|
|
1168
1198
|
generateNotesConfig,
|
|
@@ -1244,7 +1274,7 @@ async function runPackageRelease(pkg, options) {
|
|
|
1244
1274
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1245
1275
|
});
|
|
1246
1276
|
const semanticReleaseOptions = {
|
|
1247
|
-
tagFormat:
|
|
1277
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1248
1278
|
plugins: options.publishPlugins,
|
|
1249
1279
|
analyzeCommits: scoped.analyzeCommits,
|
|
1250
1280
|
generateNotes: scoped.generateNotes
|
|
@@ -1319,7 +1349,7 @@ async function detachWorkspaceRelease(options) {
|
|
|
1319
1349
|
validateDependencyRanges(graph);
|
|
1320
1350
|
const order = topologicalOrder(graph);
|
|
1321
1351
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1322
|
-
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1352
|
+
const publishPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
1323
1353
|
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1324
1354
|
packagePlugins: options.packagePlugins
|
|
1325
1355
|
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
@@ -1331,6 +1361,7 @@ async function detachWorkspaceRelease(options) {
|
|
|
1331
1361
|
analyzeCommitsConfig,
|
|
1332
1362
|
generateNotesConfig,
|
|
1333
1363
|
bumpsForThisPackage,
|
|
1364
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1334
1365
|
dryRun,
|
|
1335
1366
|
env,
|
|
1336
1367
|
branches: options.branches
|
|
@@ -1373,7 +1404,7 @@ async function runPackageDetach(pkg, options) {
|
|
|
1373
1404
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1374
1405
|
});
|
|
1375
1406
|
const cliOptions = {
|
|
1376
|
-
tagFormat:
|
|
1407
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1377
1408
|
plugins: options.publishPlugins,
|
|
1378
1409
|
analyzeCommits: scoped.analyzeCommits,
|
|
1379
1410
|
generateNotes: scoped.generateNotes
|
package/dist/index.cjs
CHANGED
|
@@ -603,6 +603,10 @@ function topologicalOrder(graph) {
|
|
|
603
603
|
if (pending.size > 0) throw new DependencyCycleError(findCycle(graph, new Set(pending.keys())));
|
|
604
604
|
return ordered;
|
|
605
605
|
}
|
|
606
|
+
/** The packages a release order names, in that order, for the stages that need each package's manifest rather than only its name. */
|
|
607
|
+
function orderedPackages(graph, order) {
|
|
608
|
+
return order.map((name) => mustGet(graph.packages, name, "package"));
|
|
609
|
+
}
|
|
606
610
|
/**
|
|
607
611
|
* Walks dependency edges between the packages Kahn's algorithm could not place, until it revisits one, so the error can name a concrete loop rather than just a set of packages. Every unplaced package is unplaced precisely because at least one of its own dependencies is too, so the walk always reaches a repeat.
|
|
608
612
|
*/
|
|
@@ -681,6 +685,8 @@ function matchTrailerLine(message, key) {
|
|
|
681
685
|
}
|
|
682
686
|
//#endregion
|
|
683
687
|
//#region src/plugins.ts
|
|
688
|
+
/** The one plugin a private package is kept off by default: see `resolveWorkspacePublishPlugins` for why a package that never reaches a registry does not get a public GitHub Release either. */
|
|
689
|
+
const GITHUB_RELEASE_PLUGIN = "@semantic-release/github";
|
|
684
690
|
/** semantic-release's own `getLastRelease` returns `{}` for a package with no prior tag -- not `undefined`, and not a fully-populated `LastRelease` -- contradicting the `gitHead: string` its own type declares. Narrows structurally rather than trusting that declared type, so a first-release context's `lastRelease` (correctly, at runtime) never claims a `gitHead` it does not have. */
|
|
685
691
|
function hasGitHead(lastRelease) {
|
|
686
692
|
return typeof lastRelease === "object" && lastRelease !== null && "gitHead" in lastRelease && typeof lastRelease.gitHead === "string";
|
|
@@ -807,13 +813,17 @@ function resolvePublishPlugins(specs, workspaceRoot, options) {
|
|
|
807
813
|
/**
|
|
808
814
|
* Resolves the publish plugin list of every package in the workspace: the workspace-wide list for each package, except where `packagePlugins` names a package, whose own list replaces it. Every list is held to the same rules as `resolvePublishPlugins` applies to a workspace-wide one, and a failure in an override names the package it belongs to.
|
|
809
815
|
*
|
|
816
|
+
* A package whose manifest sets `private: true` and which no `packagePlugins` entry names gets the workspace-wide list minus `@semantic-release/github`. A private package is never published, so a public GitHub Release for it advertises something nobody can install, and that release is not merely redundant: `@semantic-release/github` sets `make_latest` from the release branch alone, with no option to opt out, so every release it creates claims the repository's Latest label and the last one created keeps it. A private package sitting at the end of the topological order (which is where a package that depends on the published ones necessarily sits) therefore takes the label on every run. Everything the private package actually needs from the run is untouched: its version bump, its tag, and the dependency cascade to its dependents all come from the other plugins and from the orchestrator itself.
|
|
817
|
+
*
|
|
818
|
+
* A `packagePlugins` entry naming a private package is taken exactly as written, `@semantic-release/github` included, so a workspace that does want a Release for a private package can still say so.
|
|
819
|
+
*
|
|
810
820
|
* A `packagePlugins` key that matches no package is rejected rather than ignored: a misspelt name would otherwise leave the package it meant on the workspace-wide list with nothing to say so, which for the intended use (keeping a package out of a step such as GitHub Release creation) is a silent wrong result.
|
|
811
821
|
*/
|
|
812
|
-
function resolveWorkspacePublishPlugins(
|
|
822
|
+
function resolveWorkspacePublishPlugins(packages, specs, workspaceRoot, options) {
|
|
813
823
|
const overrides = specs.packagePlugins === void 0 ? [] : Object.entries(specs.packagePlugins);
|
|
814
|
-
const known = new Set(
|
|
824
|
+
const known = new Set(packages.map((pkg) => pkg.name));
|
|
815
825
|
const unknown = overrides.map(([name]) => name).filter((name) => !known.has(name));
|
|
816
|
-
if (unknown.length > 0) throw new ReleaseConfigurationError(`packagePlugins names ${unknown.map((name) => `"${name}"`).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a package in this workspace. Packages: ${
|
|
826
|
+
if (unknown.length > 0) throw new ReleaseConfigurationError(`packagePlugins names ${unknown.map((name) => `"${name}"`).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a package in this workspace. Packages: ${[...known].join(", ")}.`);
|
|
817
827
|
const workspaceWide = resolvePublishPlugins(specs.plugins, workspaceRoot, options);
|
|
818
828
|
const resolvedOverrides = /* @__PURE__ */ new Map();
|
|
819
829
|
for (const [name, list] of overrides) try {
|
|
@@ -822,7 +832,9 @@ function resolveWorkspacePublishPlugins(packageNames, specs, workspaceRoot, opti
|
|
|
822
832
|
if (cause instanceof ReleaseConfigurationError) throw new ReleaseConfigurationError(`packagePlugins for "${name}": ${cause.message}`);
|
|
823
833
|
throw cause;
|
|
824
834
|
}
|
|
825
|
-
|
|
835
|
+
const privateSpecs = specs.plugins.filter((spec) => parsePublishPluginSpec(spec)[0] !== GITHUB_RELEASE_PLUGIN);
|
|
836
|
+
const forPrivatePackages = privateSpecs.length === specs.plugins.length ? workspaceWide : resolvePublishPlugins(privateSpecs, workspaceRoot, options);
|
|
837
|
+
return new Map(packages.map((pkg) => [pkg.name, resolvedOverrides.get(pkg.name) ?? (pkg.private ? forPrivatePackages : workspaceWide)]));
|
|
826
838
|
}
|
|
827
839
|
/**
|
|
828
840
|
* Resolves a plugin module name to an absolute file path, first from this tool's own module context (its peer dependencies, which every workspace installing the orchestrator must provide) and then from the workspace root (a workspace's own plugin dependencies, such as a custom changelog plugin). Both bases are named in the error when neither can resolve the name.
|
|
@@ -857,6 +869,21 @@ async function regenerateLockfile(options) {
|
|
|
857
869
|
throw new PnpmCommandError(options.cwd, detail);
|
|
858
870
|
}
|
|
859
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Validate a caller-supplied tagFormat once per run, before any package releases. The template must contain `${version}` (semantic-release interpolates it; without it every package would compute the same tag and overwrite its predecessor) and may contain `${name}`, which the orchestrator substitutes per package. Any other `${...}` token is a typo or a placeholder semantic-release does not support in tagFormat, so it is rejected here rather than released as a literal into a tag name.
|
|
874
|
+
*/
|
|
875
|
+
function validateTagFormat(tagFormat) {
|
|
876
|
+
if (!tagFormat.includes("${version}")) throw new ReleaseConfigurationError(`tagFormat must contain '\${version}' so each package's release tag carries its version; got: ${tagFormat}`);
|
|
877
|
+
const unknown = [...tagFormat.matchAll(/\$\{([^}]*)\}/g)].map((match) => match[1] ?? "").filter((token) => token !== "name" && token !== "version").filter((token) => token !== "");
|
|
878
|
+
if (unknown.length > 0) throw new ReleaseConfigurationError(`tagFormat supports only the '\${name}' and '\${version}' placeholders; unknown token(s): ${unknown.map((token) => `\${${token}}`).join(", ")}`);
|
|
879
|
+
return tagFormat;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Substitute `${name}` for one package. Called per package at release time; the returned template still carries the literal `${version}` for semantic-release to interpolate, exactly as the default `name@version` format always has.
|
|
883
|
+
*/
|
|
884
|
+
function formatTagForPackage(tagFormat, name) {
|
|
885
|
+
return tagFormat.replaceAll("${name}", name);
|
|
886
|
+
}
|
|
860
887
|
//#endregion
|
|
861
888
|
//#region src/single-commit-release.ts
|
|
862
889
|
/**
|
|
@@ -884,7 +911,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
884
911
|
validateDependencyRanges(graph);
|
|
885
912
|
const order = topologicalOrder(graph);
|
|
886
913
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
887
|
-
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
914
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
888
915
|
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
889
916
|
packagePlugins: options.packagePlugins
|
|
890
917
|
}, workspace.root, {
|
|
@@ -893,6 +920,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
893
920
|
});
|
|
894
921
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
895
922
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
923
|
+
const tagFormat = validateTagFormat(options.tagFormat ?? "${name}@${version}");
|
|
896
924
|
const capturedCommits = /* @__PURE__ */ new Map();
|
|
897
925
|
const captured = {
|
|
898
926
|
branch: void 0,
|
|
@@ -906,6 +934,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
906
934
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
907
935
|
pendingBumps.delete(name);
|
|
908
936
|
const nextRelease = await analysePackage(pkg, {
|
|
937
|
+
tagFormat,
|
|
909
938
|
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
910
939
|
analyzeCommitsConfig,
|
|
911
940
|
generateNotesConfig,
|
|
@@ -1028,7 +1057,7 @@ async function analysePackage(pkg, options) {
|
|
|
1028
1057
|
onCommitsResolved: options.onCommitsResolved
|
|
1029
1058
|
});
|
|
1030
1059
|
const semanticReleaseOptions = {
|
|
1031
|
-
tagFormat:
|
|
1060
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1032
1061
|
plugins: options.resolvedPlugins,
|
|
1033
1062
|
dryRun: true,
|
|
1034
1063
|
async analyzeCommits(pluginConfig, context) {
|
|
@@ -1159,7 +1188,7 @@ async function detachWorkspaceRelease(options) {
|
|
|
1159
1188
|
validateDependencyRanges(graph);
|
|
1160
1189
|
const order = topologicalOrder(graph);
|
|
1161
1190
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1162
|
-
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1191
|
+
const publishPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
1163
1192
|
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1164
1193
|
packagePlugins: options.packagePlugins
|
|
1165
1194
|
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
@@ -1171,6 +1200,7 @@ async function detachWorkspaceRelease(options) {
|
|
|
1171
1200
|
analyzeCommitsConfig,
|
|
1172
1201
|
generateNotesConfig,
|
|
1173
1202
|
bumpsForThisPackage,
|
|
1203
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1174
1204
|
dryRun,
|
|
1175
1205
|
env,
|
|
1176
1206
|
branches: options.branches
|
|
@@ -1213,7 +1243,7 @@ async function runPackageDetach(pkg, options) {
|
|
|
1213
1243
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1214
1244
|
});
|
|
1215
1245
|
const cliOptions = {
|
|
1216
|
-
tagFormat:
|
|
1246
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1217
1247
|
plugins: options.publishPlugins,
|
|
1218
1248
|
analyzeCommits: scoped.analyzeCommits,
|
|
1219
1249
|
generateNotes: scoped.generateNotes
|
|
@@ -1300,7 +1330,7 @@ async function releaseWorkspace(options = {}) {
|
|
|
1300
1330
|
validateDependencyRanges(graph);
|
|
1301
1331
|
const order = topologicalOrder(graph);
|
|
1302
1332
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1303
|
-
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1333
|
+
const publishPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
1304
1334
|
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1305
1335
|
packagePlugins: options.packagePlugins
|
|
1306
1336
|
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
@@ -1310,6 +1340,7 @@ async function releaseWorkspace(options = {}) {
|
|
|
1310
1340
|
order,
|
|
1311
1341
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1312
1342
|
const result = await runPackageRelease(pkg, {
|
|
1343
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1313
1344
|
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1314
1345
|
analyzeCommitsConfig,
|
|
1315
1346
|
generateNotesConfig,
|
|
@@ -1391,7 +1422,7 @@ async function runPackageRelease(pkg, options) {
|
|
|
1391
1422
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1392
1423
|
});
|
|
1393
1424
|
const semanticReleaseOptions = {
|
|
1394
|
-
tagFormat:
|
|
1425
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1395
1426
|
plugins: options.publishPlugins,
|
|
1396
1427
|
analyzeCommits: scoped.analyzeCommits,
|
|
1397
1428
|
generateNotes: scoped.generateNotes
|
|
@@ -1466,6 +1497,7 @@ exports.classifyDependencyRange = classifyDependencyRange;
|
|
|
1466
1497
|
exports.createScopedPlugins = createScopedPlugins;
|
|
1467
1498
|
exports.discoverWorkspace = discoverWorkspace;
|
|
1468
1499
|
exports.filterCommitsToDirectory = filterCommitsToDirectory;
|
|
1500
|
+
exports.orderedPackages = orderedPackages;
|
|
1469
1501
|
exports.packageName = packageName;
|
|
1470
1502
|
exports.readManifest = readManifest;
|
|
1471
1503
|
exports.releaseWorkspace = releaseWorkspace;
|
package/dist/index.d.cts
CHANGED
|
@@ -86,6 +86,8 @@ export declare function buildDependencyGraph(packages: readonly WorkspacePackage
|
|
|
86
86
|
* A cycle has no valid order at all, so it throws rather than picking one of the wrong answers. In a release context an arbitrary order is worse than a failure: it would publish a package whose sibling dependency range points at a version that does not exist yet.
|
|
87
87
|
*/
|
|
88
88
|
export declare function topologicalOrder(graph: DependencyGraph): readonly string[];
|
|
89
|
+
/** The packages a release order names, in that order, for the stages that need each package's manifest rather than only its name. */
|
|
90
|
+
export declare function orderedPackages(graph: DependencyGraph, order: readonly string[]): readonly WorkspacePackage[];
|
|
89
91
|
//#endregion
|
|
90
92
|
//#region src/version-range.d.ts
|
|
91
93
|
/**
|
|
@@ -149,6 +151,11 @@ interface DependencyBumpSource {
|
|
|
149
151
|
type PublishPluginSpec = string | readonly [string] | readonly [string, Record<string, unknown>];
|
|
150
152
|
/** Publish plugin lists keyed by package name. Each list replaces the workspace-wide list outright for that one package; it is not merged with it. */
|
|
151
153
|
type PackagePluginSpecs = Readonly<Record<string, readonly PublishPluginSpec[]>>;
|
|
154
|
+
/** All resolving a package's publish plugins needs to know about it: its name, and whether its manifest marks it private. `WorkspacePackage` satisfies this structurally. */
|
|
155
|
+
interface PublishPluginPackage {
|
|
156
|
+
readonly name: string;
|
|
157
|
+
readonly private: boolean;
|
|
158
|
+
}
|
|
152
159
|
/** The standard publish pipeline this orchestrator coordinates when a workspace configures none of its own. Every entry reuses the corresponding official plugin -- the orchestrator scopes and sequences them per package, it does not reimplement npm publishing, GitHub release creation, or changelog writing. */
|
|
153
160
|
export declare const DEFAULT_PUBLISH_PLUGINS: readonly PublishPluginSpec[];
|
|
154
161
|
/** The standard publish pipeline for `commitStrategy: 'single'`: the same as `DEFAULT_PUBLISH_PLUGINS` minus `@semantic-release/git`, which that mode never runs -- see `resolvePublishPlugins`'s `forbidGitPlugin` option for why it is rejected outright rather than merely unused. Single-commit mode does its own committing (one combined commit for every released package), so a `prepare`-step git plugin here would create the very per-package commits that mode exists to avoid. */
|
|
@@ -193,9 +200,13 @@ export declare function resolvePublishPlugins(specs: readonly PublishPluginSpec[
|
|
|
193
200
|
/**
|
|
194
201
|
* Resolves the publish plugin list of every package in the workspace: the workspace-wide list for each package, except where `packagePlugins` names a package, whose own list replaces it. Every list is held to the same rules as `resolvePublishPlugins` applies to a workspace-wide one, and a failure in an override names the package it belongs to.
|
|
195
202
|
*
|
|
203
|
+
* A package whose manifest sets `private: true` and which no `packagePlugins` entry names gets the workspace-wide list minus `@semantic-release/github`. A private package is never published, so a public GitHub Release for it advertises something nobody can install, and that release is not merely redundant: `@semantic-release/github` sets `make_latest` from the release branch alone, with no option to opt out, so every release it creates claims the repository's Latest label and the last one created keeps it. A private package sitting at the end of the topological order (which is where a package that depends on the published ones necessarily sits) therefore takes the label on every run. Everything the private package actually needs from the run is untouched: its version bump, its tag, and the dependency cascade to its dependents all come from the other plugins and from the orchestrator itself.
|
|
204
|
+
*
|
|
205
|
+
* A `packagePlugins` entry naming a private package is taken exactly as written, `@semantic-release/github` included, so a workspace that does want a Release for a private package can still say so.
|
|
206
|
+
*
|
|
196
207
|
* A `packagePlugins` key that matches no package is rejected rather than ignored: a misspelt name would otherwise leave the package it meant on the workspace-wide list with nothing to say so, which for the intended use (keeping a package out of a step such as GitHub Release creation) is a silent wrong result.
|
|
197
208
|
*/
|
|
198
|
-
export declare function resolveWorkspacePublishPlugins(
|
|
209
|
+
export declare function resolveWorkspacePublishPlugins(packages: readonly PublishPluginPackage[], specs: {
|
|
199
210
|
readonly plugins: readonly PublishPluginSpec[];
|
|
200
211
|
readonly packagePlugins: PackagePluginSpecs | undefined;
|
|
201
212
|
}, workspaceRoot: string, options: {
|
|
@@ -246,7 +257,7 @@ interface ReleaseWorkspaceOptions {
|
|
|
246
257
|
readonly branches?: readonly BranchSpec[];
|
|
247
258
|
/** Publish-pipeline plugins (changelog, npm, GitHub, git), each scoped per package by semantic-release's own `cwd`. Defaults to the standard pipeline in DEFAULT_PUBLISH_PLUGINS for `commitStrategy: 'per-package'`, or SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS (the same list minus `@semantic-release/git`) for `commitStrategy: 'single'`. */
|
|
248
259
|
readonly plugins?: readonly PublishPluginSpec[];
|
|
249
|
-
/** Publish plugin lists for individual packages, keyed by package name, each replacing `plugins` (or its default) outright for that package. Every other package keeps the workspace-wide list
|
|
260
|
+
/** Publish plugin lists for individual packages, keyed by package name, each replacing `plugins` (or its default) outright for that package. Every other package keeps the workspace-wide list, except that a package marked `private` in its manifest drops `@semantic-release/github` from it on its own (see `resolveWorkspacePublishPlugins`), so the usual reason to reach for this is either the reverse of that default, a private package that does want a GitHub Release, or keeping a package out of some other step the rest need. Applies under every `commitStrategy` and to `gatePublish`. A name that is not a package in the workspace is rejected. */
|
|
250
261
|
readonly packagePlugins?: PackagePluginSpecs;
|
|
251
262
|
/** Options for the wrapped `@semantic-release/commit-analyzer`, applied per package after path filtering. */
|
|
252
263
|
readonly analyzeCommits?: Record<string, unknown>;
|
|
@@ -260,6 +271,14 @@ interface ReleaseWorkspaceOptions {
|
|
|
260
271
|
* Opt-in, orthogonal to `commitStrategy` -- it governs *when* publish/success run relative to tag+push, not *how many commits* the release makes. When `true`, each package's release is tagged and pushed via `@exadev/release-gate`'s `detachRelease` but never published: `WorkspaceReleaseOutcome.detached` carries the state `resumeWorkspaceRelease` needs to finish publishing later, from the same process or a different one, once an external gate confirms the release should actually go out. Rejected outright in combination with `commitStrategy: 'single'` -- `single-commit-release.ts`'s tag/publish machinery is entirely bespoke and never goes through semantic-release's own `run()`, so it has no insertion point for `release-gate`'s primitives. Defaults to `false`, today's exact existing behaviour.
|
|
261
272
|
*/
|
|
262
273
|
readonly gatePublish?: boolean;
|
|
274
|
+
/**
|
|
275
|
+
* Lodash template for each package's release tag, with `${name}` and `${version}` placeholders.
|
|
276
|
+
* Defaults to `'${name}@${version}'`. Override it when the tags double as refs consumed outside
|
|
277
|
+
* git: GitHub Actions pins composite actions as `owner/repo/path@ref`, and GitHub's workflow
|
|
278
|
+
* parser rejects a ref containing `@`, so a repository of actions sets `'${name}-v${version}'`
|
|
279
|
+
* and pins `...@<action>-v1`. Validated once per run by `validateTagFormat`.
|
|
280
|
+
*/
|
|
281
|
+
readonly tagFormat?: string;
|
|
263
282
|
}
|
|
264
283
|
/** One dependency-range change applied to a dependent package's manifest during the run, attached to the dependent's own outcome. */
|
|
265
284
|
interface AppliedDependencyBump extends DependencyBump {
|
|
@@ -326,4 +345,4 @@ export declare class GitCommandError extends WorkspaceReleaseError {
|
|
|
326
345
|
/** The workspace's git state does not support the release operation -- for example a detached HEAD, which names no branch that dependency-bump commits could be pushed to. */
|
|
327
346
|
export declare class WorkspaceStateError extends WorkspaceReleaseError {}
|
|
328
347
|
//#endregion
|
|
329
|
-
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackagePluginSpecs, PackageReleaseOutcome, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
|
348
|
+
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackagePluginSpecs, PackageReleaseOutcome, PublishPluginPackage, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
package/dist/index.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ export declare function buildDependencyGraph(packages: readonly WorkspacePackage
|
|
|
86
86
|
* A cycle has no valid order at all, so it throws rather than picking one of the wrong answers. In a release context an arbitrary order is worse than a failure: it would publish a package whose sibling dependency range points at a version that does not exist yet.
|
|
87
87
|
*/
|
|
88
88
|
export declare function topologicalOrder(graph: DependencyGraph): readonly string[];
|
|
89
|
+
/** The packages a release order names, in that order, for the stages that need each package's manifest rather than only its name. */
|
|
90
|
+
export declare function orderedPackages(graph: DependencyGraph, order: readonly string[]): readonly WorkspacePackage[];
|
|
89
91
|
//#endregion
|
|
90
92
|
//#region src/version-range.d.ts
|
|
91
93
|
/**
|
|
@@ -149,6 +151,11 @@ interface DependencyBumpSource {
|
|
|
149
151
|
type PublishPluginSpec = string | readonly [string] | readonly [string, Record<string, unknown>];
|
|
150
152
|
/** Publish plugin lists keyed by package name. Each list replaces the workspace-wide list outright for that one package; it is not merged with it. */
|
|
151
153
|
type PackagePluginSpecs = Readonly<Record<string, readonly PublishPluginSpec[]>>;
|
|
154
|
+
/** All resolving a package's publish plugins needs to know about it: its name, and whether its manifest marks it private. `WorkspacePackage` satisfies this structurally. */
|
|
155
|
+
interface PublishPluginPackage {
|
|
156
|
+
readonly name: string;
|
|
157
|
+
readonly private: boolean;
|
|
158
|
+
}
|
|
152
159
|
/** The standard publish pipeline this orchestrator coordinates when a workspace configures none of its own. Every entry reuses the corresponding official plugin -- the orchestrator scopes and sequences them per package, it does not reimplement npm publishing, GitHub release creation, or changelog writing. */
|
|
153
160
|
export declare const DEFAULT_PUBLISH_PLUGINS: readonly PublishPluginSpec[];
|
|
154
161
|
/** The standard publish pipeline for `commitStrategy: 'single'`: the same as `DEFAULT_PUBLISH_PLUGINS` minus `@semantic-release/git`, which that mode never runs -- see `resolvePublishPlugins`'s `forbidGitPlugin` option for why it is rejected outright rather than merely unused. Single-commit mode does its own committing (one combined commit for every released package), so a `prepare`-step git plugin here would create the very per-package commits that mode exists to avoid. */
|
|
@@ -193,9 +200,13 @@ export declare function resolvePublishPlugins(specs: readonly PublishPluginSpec[
|
|
|
193
200
|
/**
|
|
194
201
|
* Resolves the publish plugin list of every package in the workspace: the workspace-wide list for each package, except where `packagePlugins` names a package, whose own list replaces it. Every list is held to the same rules as `resolvePublishPlugins` applies to a workspace-wide one, and a failure in an override names the package it belongs to.
|
|
195
202
|
*
|
|
203
|
+
* A package whose manifest sets `private: true` and which no `packagePlugins` entry names gets the workspace-wide list minus `@semantic-release/github`. A private package is never published, so a public GitHub Release for it advertises something nobody can install, and that release is not merely redundant: `@semantic-release/github` sets `make_latest` from the release branch alone, with no option to opt out, so every release it creates claims the repository's Latest label and the last one created keeps it. A private package sitting at the end of the topological order (which is where a package that depends on the published ones necessarily sits) therefore takes the label on every run. Everything the private package actually needs from the run is untouched: its version bump, its tag, and the dependency cascade to its dependents all come from the other plugins and from the orchestrator itself.
|
|
204
|
+
*
|
|
205
|
+
* A `packagePlugins` entry naming a private package is taken exactly as written, `@semantic-release/github` included, so a workspace that does want a Release for a private package can still say so.
|
|
206
|
+
*
|
|
196
207
|
* A `packagePlugins` key that matches no package is rejected rather than ignored: a misspelt name would otherwise leave the package it meant on the workspace-wide list with nothing to say so, which for the intended use (keeping a package out of a step such as GitHub Release creation) is a silent wrong result.
|
|
197
208
|
*/
|
|
198
|
-
export declare function resolveWorkspacePublishPlugins(
|
|
209
|
+
export declare function resolveWorkspacePublishPlugins(packages: readonly PublishPluginPackage[], specs: {
|
|
199
210
|
readonly plugins: readonly PublishPluginSpec[];
|
|
200
211
|
readonly packagePlugins: PackagePluginSpecs | undefined;
|
|
201
212
|
}, workspaceRoot: string, options: {
|
|
@@ -246,7 +257,7 @@ interface ReleaseWorkspaceOptions {
|
|
|
246
257
|
readonly branches?: readonly BranchSpec[];
|
|
247
258
|
/** Publish-pipeline plugins (changelog, npm, GitHub, git), each scoped per package by semantic-release's own `cwd`. Defaults to the standard pipeline in DEFAULT_PUBLISH_PLUGINS for `commitStrategy: 'per-package'`, or SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS (the same list minus `@semantic-release/git`) for `commitStrategy: 'single'`. */
|
|
248
259
|
readonly plugins?: readonly PublishPluginSpec[];
|
|
249
|
-
/** Publish plugin lists for individual packages, keyed by package name, each replacing `plugins` (or its default) outright for that package. Every other package keeps the workspace-wide list
|
|
260
|
+
/** Publish plugin lists for individual packages, keyed by package name, each replacing `plugins` (or its default) outright for that package. Every other package keeps the workspace-wide list, except that a package marked `private` in its manifest drops `@semantic-release/github` from it on its own (see `resolveWorkspacePublishPlugins`), so the usual reason to reach for this is either the reverse of that default, a private package that does want a GitHub Release, or keeping a package out of some other step the rest need. Applies under every `commitStrategy` and to `gatePublish`. A name that is not a package in the workspace is rejected. */
|
|
250
261
|
readonly packagePlugins?: PackagePluginSpecs;
|
|
251
262
|
/** Options for the wrapped `@semantic-release/commit-analyzer`, applied per package after path filtering. */
|
|
252
263
|
readonly analyzeCommits?: Record<string, unknown>;
|
|
@@ -260,6 +271,14 @@ interface ReleaseWorkspaceOptions {
|
|
|
260
271
|
* Opt-in, orthogonal to `commitStrategy` -- it governs *when* publish/success run relative to tag+push, not *how many commits* the release makes. When `true`, each package's release is tagged and pushed via `@exadev/release-gate`'s `detachRelease` but never published: `WorkspaceReleaseOutcome.detached` carries the state `resumeWorkspaceRelease` needs to finish publishing later, from the same process or a different one, once an external gate confirms the release should actually go out. Rejected outright in combination with `commitStrategy: 'single'` -- `single-commit-release.ts`'s tag/publish machinery is entirely bespoke and never goes through semantic-release's own `run()`, so it has no insertion point for `release-gate`'s primitives. Defaults to `false`, today's exact existing behaviour.
|
|
261
272
|
*/
|
|
262
273
|
readonly gatePublish?: boolean;
|
|
274
|
+
/**
|
|
275
|
+
* Lodash template for each package's release tag, with `${name}` and `${version}` placeholders.
|
|
276
|
+
* Defaults to `'${name}@${version}'`. Override it when the tags double as refs consumed outside
|
|
277
|
+
* git: GitHub Actions pins composite actions as `owner/repo/path@ref`, and GitHub's workflow
|
|
278
|
+
* parser rejects a ref containing `@`, so a repository of actions sets `'${name}-v${version}'`
|
|
279
|
+
* and pins `...@<action>-v1`. Validated once per run by `validateTagFormat`.
|
|
280
|
+
*/
|
|
281
|
+
readonly tagFormat?: string;
|
|
263
282
|
}
|
|
264
283
|
/** One dependency-range change applied to a dependent package's manifest during the run, attached to the dependent's own outcome. */
|
|
265
284
|
interface AppliedDependencyBump extends DependencyBump {
|
|
@@ -326,4 +345,4 @@ export declare class GitCommandError extends WorkspaceReleaseError {
|
|
|
326
345
|
/** The workspace's git state does not support the release operation -- for example a detached HEAD, which names no branch that dependency-bump commits could be pushed to. */
|
|
327
346
|
export declare class WorkspaceStateError extends WorkspaceReleaseError {}
|
|
328
347
|
//#endregion
|
|
329
|
-
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackagePluginSpecs, PackageReleaseOutcome, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
|
348
|
+
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackagePluginSpecs, PackageReleaseOutcome, PublishPluginPackage, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
package/dist/index.js
CHANGED
|
@@ -578,6 +578,10 @@ function topologicalOrder(graph) {
|
|
|
578
578
|
if (pending.size > 0) throw new DependencyCycleError(findCycle(graph, new Set(pending.keys())));
|
|
579
579
|
return ordered;
|
|
580
580
|
}
|
|
581
|
+
/** The packages a release order names, in that order, for the stages that need each package's manifest rather than only its name. */
|
|
582
|
+
function orderedPackages(graph, order) {
|
|
583
|
+
return order.map((name) => mustGet(graph.packages, name, "package"));
|
|
584
|
+
}
|
|
581
585
|
/**
|
|
582
586
|
* Walks dependency edges between the packages Kahn's algorithm could not place, until it revisits one, so the error can name a concrete loop rather than just a set of packages. Every unplaced package is unplaced precisely because at least one of its own dependencies is too, so the walk always reaches a repeat.
|
|
583
587
|
*/
|
|
@@ -656,6 +660,8 @@ function matchTrailerLine(message, key) {
|
|
|
656
660
|
}
|
|
657
661
|
//#endregion
|
|
658
662
|
//#region src/plugins.ts
|
|
663
|
+
/** The one plugin a private package is kept off by default: see `resolveWorkspacePublishPlugins` for why a package that never reaches a registry does not get a public GitHub Release either. */
|
|
664
|
+
const GITHUB_RELEASE_PLUGIN = "@semantic-release/github";
|
|
659
665
|
/** semantic-release's own `getLastRelease` returns `{}` for a package with no prior tag -- not `undefined`, and not a fully-populated `LastRelease` -- contradicting the `gitHead: string` its own type declares. Narrows structurally rather than trusting that declared type, so a first-release context's `lastRelease` (correctly, at runtime) never claims a `gitHead` it does not have. */
|
|
660
666
|
function hasGitHead(lastRelease) {
|
|
661
667
|
return typeof lastRelease === "object" && lastRelease !== null && "gitHead" in lastRelease && typeof lastRelease.gitHead === "string";
|
|
@@ -782,13 +788,17 @@ function resolvePublishPlugins(specs, workspaceRoot, options) {
|
|
|
782
788
|
/**
|
|
783
789
|
* Resolves the publish plugin list of every package in the workspace: the workspace-wide list for each package, except where `packagePlugins` names a package, whose own list replaces it. Every list is held to the same rules as `resolvePublishPlugins` applies to a workspace-wide one, and a failure in an override names the package it belongs to.
|
|
784
790
|
*
|
|
791
|
+
* A package whose manifest sets `private: true` and which no `packagePlugins` entry names gets the workspace-wide list minus `@semantic-release/github`. A private package is never published, so a public GitHub Release for it advertises something nobody can install, and that release is not merely redundant: `@semantic-release/github` sets `make_latest` from the release branch alone, with no option to opt out, so every release it creates claims the repository's Latest label and the last one created keeps it. A private package sitting at the end of the topological order (which is where a package that depends on the published ones necessarily sits) therefore takes the label on every run. Everything the private package actually needs from the run is untouched: its version bump, its tag, and the dependency cascade to its dependents all come from the other plugins and from the orchestrator itself.
|
|
792
|
+
*
|
|
793
|
+
* A `packagePlugins` entry naming a private package is taken exactly as written, `@semantic-release/github` included, so a workspace that does want a Release for a private package can still say so.
|
|
794
|
+
*
|
|
785
795
|
* A `packagePlugins` key that matches no package is rejected rather than ignored: a misspelt name would otherwise leave the package it meant on the workspace-wide list with nothing to say so, which for the intended use (keeping a package out of a step such as GitHub Release creation) is a silent wrong result.
|
|
786
796
|
*/
|
|
787
|
-
function resolveWorkspacePublishPlugins(
|
|
797
|
+
function resolveWorkspacePublishPlugins(packages, specs, workspaceRoot, options) {
|
|
788
798
|
const overrides = specs.packagePlugins === void 0 ? [] : Object.entries(specs.packagePlugins);
|
|
789
|
-
const known = new Set(
|
|
799
|
+
const known = new Set(packages.map((pkg) => pkg.name));
|
|
790
800
|
const unknown = overrides.map(([name]) => name).filter((name) => !known.has(name));
|
|
791
|
-
if (unknown.length > 0) throw new ReleaseConfigurationError(`packagePlugins names ${unknown.map((name) => `"${name}"`).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a package in this workspace. Packages: ${
|
|
801
|
+
if (unknown.length > 0) throw new ReleaseConfigurationError(`packagePlugins names ${unknown.map((name) => `"${name}"`).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a package in this workspace. Packages: ${[...known].join(", ")}.`);
|
|
792
802
|
const workspaceWide = resolvePublishPlugins(specs.plugins, workspaceRoot, options);
|
|
793
803
|
const resolvedOverrides = /* @__PURE__ */ new Map();
|
|
794
804
|
for (const [name, list] of overrides) try {
|
|
@@ -797,7 +807,9 @@ function resolveWorkspacePublishPlugins(packageNames, specs, workspaceRoot, opti
|
|
|
797
807
|
if (cause instanceof ReleaseConfigurationError) throw new ReleaseConfigurationError(`packagePlugins for "${name}": ${cause.message}`);
|
|
798
808
|
throw cause;
|
|
799
809
|
}
|
|
800
|
-
|
|
810
|
+
const privateSpecs = specs.plugins.filter((spec) => parsePublishPluginSpec(spec)[0] !== GITHUB_RELEASE_PLUGIN);
|
|
811
|
+
const forPrivatePackages = privateSpecs.length === specs.plugins.length ? workspaceWide : resolvePublishPlugins(privateSpecs, workspaceRoot, options);
|
|
812
|
+
return new Map(packages.map((pkg) => [pkg.name, resolvedOverrides.get(pkg.name) ?? (pkg.private ? forPrivatePackages : workspaceWide)]));
|
|
801
813
|
}
|
|
802
814
|
/**
|
|
803
815
|
* Resolves a plugin module name to an absolute file path, first from this tool's own module context (its peer dependencies, which every workspace installing the orchestrator must provide) and then from the workspace root (a workspace's own plugin dependencies, such as a custom changelog plugin). Both bases are named in the error when neither can resolve the name.
|
|
@@ -832,6 +844,21 @@ async function regenerateLockfile(options) {
|
|
|
832
844
|
throw new PnpmCommandError(options.cwd, detail);
|
|
833
845
|
}
|
|
834
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Validate a caller-supplied tagFormat once per run, before any package releases. The template must contain `${version}` (semantic-release interpolates it; without it every package would compute the same tag and overwrite its predecessor) and may contain `${name}`, which the orchestrator substitutes per package. Any other `${...}` token is a typo or a placeholder semantic-release does not support in tagFormat, so it is rejected here rather than released as a literal into a tag name.
|
|
849
|
+
*/
|
|
850
|
+
function validateTagFormat(tagFormat) {
|
|
851
|
+
if (!tagFormat.includes("${version}")) throw new ReleaseConfigurationError(`tagFormat must contain '\${version}' so each package's release tag carries its version; got: ${tagFormat}`);
|
|
852
|
+
const unknown = [...tagFormat.matchAll(/\$\{([^}]*)\}/g)].map((match) => match[1] ?? "").filter((token) => token !== "name" && token !== "version").filter((token) => token !== "");
|
|
853
|
+
if (unknown.length > 0) throw new ReleaseConfigurationError(`tagFormat supports only the '\${name}' and '\${version}' placeholders; unknown token(s): ${unknown.map((token) => `\${${token}}`).join(", ")}`);
|
|
854
|
+
return tagFormat;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Substitute `${name}` for one package. Called per package at release time; the returned template still carries the literal `${version}` for semantic-release to interpolate, exactly as the default `name@version` format always has.
|
|
858
|
+
*/
|
|
859
|
+
function formatTagForPackage(tagFormat, name) {
|
|
860
|
+
return tagFormat.replaceAll("${name}", name);
|
|
861
|
+
}
|
|
835
862
|
//#endregion
|
|
836
863
|
//#region src/single-commit-release.ts
|
|
837
864
|
/**
|
|
@@ -859,7 +886,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
859
886
|
validateDependencyRanges(graph);
|
|
860
887
|
const order = topologicalOrder(graph);
|
|
861
888
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
862
|
-
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
889
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
863
890
|
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
864
891
|
packagePlugins: options.packagePlugins
|
|
865
892
|
}, workspace.root, {
|
|
@@ -868,6 +895,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
868
895
|
});
|
|
869
896
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
870
897
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
898
|
+
const tagFormat = validateTagFormat(options.tagFormat ?? "${name}@${version}");
|
|
871
899
|
const capturedCommits = /* @__PURE__ */ new Map();
|
|
872
900
|
const captured = {
|
|
873
901
|
branch: void 0,
|
|
@@ -881,6 +909,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
881
909
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
882
910
|
pendingBumps.delete(name);
|
|
883
911
|
const nextRelease = await analysePackage(pkg, {
|
|
912
|
+
tagFormat,
|
|
884
913
|
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
885
914
|
analyzeCommitsConfig,
|
|
886
915
|
generateNotesConfig,
|
|
@@ -1003,7 +1032,7 @@ async function analysePackage(pkg, options) {
|
|
|
1003
1032
|
onCommitsResolved: options.onCommitsResolved
|
|
1004
1033
|
});
|
|
1005
1034
|
const semanticReleaseOptions = {
|
|
1006
|
-
tagFormat:
|
|
1035
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1007
1036
|
plugins: options.resolvedPlugins,
|
|
1008
1037
|
dryRun: true,
|
|
1009
1038
|
async analyzeCommits(pluginConfig, context) {
|
|
@@ -1134,7 +1163,7 @@ async function detachWorkspaceRelease(options) {
|
|
|
1134
1163
|
validateDependencyRanges(graph);
|
|
1135
1164
|
const order = topologicalOrder(graph);
|
|
1136
1165
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1137
|
-
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1166
|
+
const publishPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
1138
1167
|
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1139
1168
|
packagePlugins: options.packagePlugins
|
|
1140
1169
|
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
@@ -1146,6 +1175,7 @@ async function detachWorkspaceRelease(options) {
|
|
|
1146
1175
|
analyzeCommitsConfig,
|
|
1147
1176
|
generateNotesConfig,
|
|
1148
1177
|
bumpsForThisPackage,
|
|
1178
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1149
1179
|
dryRun,
|
|
1150
1180
|
env,
|
|
1151
1181
|
branches: options.branches
|
|
@@ -1188,7 +1218,7 @@ async function runPackageDetach(pkg, options) {
|
|
|
1188
1218
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1189
1219
|
});
|
|
1190
1220
|
const cliOptions = {
|
|
1191
|
-
tagFormat:
|
|
1221
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1192
1222
|
plugins: options.publishPlugins,
|
|
1193
1223
|
analyzeCommits: scoped.analyzeCommits,
|
|
1194
1224
|
generateNotes: scoped.generateNotes
|
|
@@ -1275,7 +1305,7 @@ async function releaseWorkspace(options = {}) {
|
|
|
1275
1305
|
validateDependencyRanges(graph);
|
|
1276
1306
|
const order = topologicalOrder(graph);
|
|
1277
1307
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1278
|
-
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1308
|
+
const publishPlugins = resolveWorkspacePublishPlugins(orderedPackages(graph, order), {
|
|
1279
1309
|
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1280
1310
|
packagePlugins: options.packagePlugins
|
|
1281
1311
|
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
@@ -1285,6 +1315,7 @@ async function releaseWorkspace(options = {}) {
|
|
|
1285
1315
|
order,
|
|
1286
1316
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1287
1317
|
const result = await runPackageRelease(pkg, {
|
|
1318
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1288
1319
|
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1289
1320
|
analyzeCommitsConfig,
|
|
1290
1321
|
generateNotesConfig,
|
|
@@ -1366,7 +1397,7 @@ async function runPackageRelease(pkg, options) {
|
|
|
1366
1397
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1367
1398
|
});
|
|
1368
1399
|
const semanticReleaseOptions = {
|
|
1369
|
-
tagFormat:
|
|
1400
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1370
1401
|
plugins: options.publishPlugins,
|
|
1371
1402
|
analyzeCommits: scoped.analyzeCommits,
|
|
1372
1403
|
generateNotes: scoped.generateNotes
|
|
@@ -1427,4 +1458,4 @@ async function bumpDependents(released, version, graph, options) {
|
|
|
1427
1458
|
return applied;
|
|
1428
1459
|
}
|
|
1429
1460
|
//#endregion
|
|
1430
|
-
export { DEFAULT_PUBLISH_PLUGINS, DependencyCycleError, GitCommandError, ReleaseConfigurationError, SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, UnsupportedDependencyRangeError, WorkspaceDiscoveryError, WorkspaceReleaseError, WorkspaceStateError, buildDependencyGraph, classifyDependencyRange, createScopedPlugins, discoverWorkspace, filterCommitsToDirectory, packageName, readManifest, releaseWorkspace, resolvePublishPlugins, resolveWorkspacePublishPlugins, resumeWorkspaceRelease, topologicalOrder, updateDependencyRange, writeDependencyRange };
|
|
1461
|
+
export { DEFAULT_PUBLISH_PLUGINS, DependencyCycleError, GitCommandError, ReleaseConfigurationError, SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, UnsupportedDependencyRangeError, WorkspaceDiscoveryError, WorkspaceReleaseError, WorkspaceStateError, buildDependencyGraph, classifyDependencyRange, createScopedPlugins, discoverWorkspace, filterCommitsToDirectory, orderedPackages, packageName, readManifest, releaseWorkspace, resolvePublishPlugins, resolveWorkspacePublishPlugins, resumeWorkspaceRelease, topologicalOrder, updateDependencyRange, writeDependencyRange };
|
package/package.json
CHANGED