@exadev/semantic-release-workspace 2.0.0 → 2.2.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 +30 -3
- package/dist/cli.js +79 -18
- package/dist/index.cjs +62 -13
- package/dist/index.d.cts +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +62 -14
- 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
|
|
|
@@ -206,7 +212,7 @@ Run it from the workspace root (or pass `--root <directory>`). A dry run analyse
|
|
|
206
212
|
| `--commit-strategy <mode>` | `per-package` (default) or `single` — see [Commit strategies](#commit-strategies) |
|
|
207
213
|
| `--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` |
|
|
208
214
|
| `--gate-state-file <path>` | With `--gate-publish`: where to write the state a later `resume` run needs |
|
|
209
|
-
| `--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 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`) |
|
|
215
|
+
| `--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`) |
|
|
210
216
|
|
|
211
217
|
`resume` (a separate subcommand, not a `release` flag) finishes publishing what a `--gate-publish` run tagged and pushed:
|
|
212
218
|
|
|
@@ -219,6 +225,27 @@ Listing `@semantic-release/commit-analyzer` or `@semantic-release/release-notes-
|
|
|
219
225
|
|
|
220
226
|
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
227
|
|
|
228
|
+
### Per-package publish plugins
|
|
229
|
+
|
|
230
|
+
`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. The override is not merged with the workspace-wide list: 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.
|
|
231
|
+
|
|
232
|
+
The case this exists for is a private package. Such a package still needs its `name@version` tag, its version bump and the dependency cascade to its dependents, because a dependent's own release can hinge on it (a private package whose build output ships inside a published one, for example). It does not need a public GitHub Release, and `@semantic-release/github` marks every release it creates from the release branch as the repository's Latest, so GitHub ends up showing whichever package the run released last, which is often a private one that depends on the rest. Leaving that plugin off the private package's list removes the Release and nothing else:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// release-workspace.config.ts
|
|
236
|
+
import { DEFAULT_PUBLISH_PLUGINS, type ReleaseWorkspaceOptions } from '@exadev/semantic-release-workspace';
|
|
237
|
+
|
|
238
|
+
const config: ReleaseWorkspaceOptions = {
|
|
239
|
+
packagePlugins: {
|
|
240
|
+
'@acme/web-console': DEFAULT_PUBLISH_PLUGINS.filter((plugin) => plugin !== '@semantic-release/github'),
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export default config;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The override behaves the same under both commit strategies and with `gatePublish`. A gated run persists each package's pipeline in its state file, so the override reaches `resume` without being repeated there. Under `commitStrategy: 'single'` start from `SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS` instead of `DEFAULT_PUBLISH_PLUGINS`.
|
|
248
|
+
|
|
222
249
|
### Programmatic API
|
|
223
250
|
|
|
224
251
|
```ts
|
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 = "2.
|
|
17
|
+
var version = "2.2.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
|
/**
|
|
@@ -792,6 +807,26 @@ function resolvePublishPlugins(specs, workspaceRoot, options) {
|
|
|
792
807
|
return resolved;
|
|
793
808
|
}
|
|
794
809
|
/**
|
|
810
|
+
* 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.
|
|
811
|
+
*
|
|
812
|
+
* 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.
|
|
813
|
+
*/
|
|
814
|
+
function resolveWorkspacePublishPlugins(packageNames, specs, workspaceRoot, options) {
|
|
815
|
+
const overrides = specs.packagePlugins === void 0 ? [] : Object.entries(specs.packagePlugins);
|
|
816
|
+
const known = new Set(packageNames);
|
|
817
|
+
const unknown = overrides.map(([name]) => name).filter((name) => !known.has(name));
|
|
818
|
+
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: ${packageNames.join(", ")}.`);
|
|
819
|
+
const workspaceWide = resolvePublishPlugins(specs.plugins, workspaceRoot, options);
|
|
820
|
+
const resolvedOverrides = /* @__PURE__ */ new Map();
|
|
821
|
+
for (const [name, list] of overrides) try {
|
|
822
|
+
resolvedOverrides.set(name, resolvePublishPlugins(list, workspaceRoot, options));
|
|
823
|
+
} catch (cause) {
|
|
824
|
+
if (cause instanceof ReleaseConfigurationError) throw new ReleaseConfigurationError(`packagePlugins for "${name}": ${cause.message}`);
|
|
825
|
+
throw cause;
|
|
826
|
+
}
|
|
827
|
+
return new Map(packageNames.map((name) => [name, resolvedOverrides.get(name) ?? workspaceWide]));
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
795
830
|
* 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.
|
|
796
831
|
*/
|
|
797
832
|
function resolvePluginModule(name, requireFromTool, requireFromWorkspace) {
|
|
@@ -851,12 +886,16 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
851
886
|
validateDependencyRanges(graph);
|
|
852
887
|
const order = topologicalOrder(graph);
|
|
853
888
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
854
|
-
const resolvedPlugins =
|
|
889
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
890
|
+
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
891
|
+
packagePlugins: options.packagePlugins
|
|
892
|
+
}, workspace.root, {
|
|
855
893
|
requireGitPlugin: false,
|
|
856
894
|
forbidGitPlugin: true
|
|
857
895
|
});
|
|
858
896
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
859
897
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
898
|
+
const tagFormat = validateTagFormat(options.tagFormat ?? "${name}@${version}");
|
|
860
899
|
const capturedCommits = /* @__PURE__ */ new Map();
|
|
861
900
|
const captured = {
|
|
862
901
|
branch: void 0,
|
|
@@ -870,7 +909,8 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
870
909
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
871
910
|
pendingBumps.delete(name);
|
|
872
911
|
const nextRelease = await analysePackage(pkg, {
|
|
873
|
-
|
|
912
|
+
tagFormat,
|
|
913
|
+
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
874
914
|
analyzeCommitsConfig,
|
|
875
915
|
generateNotesConfig,
|
|
876
916
|
bumpsForThisPackage,
|
|
@@ -931,7 +971,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
931
971
|
log
|
|
932
972
|
};
|
|
933
973
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
934
|
-
for (const release of planned) for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
974
|
+
for (const release of planned) for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
935
975
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
936
976
|
if (plugin.verifyConditions) await plugin.verifyConditions(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
937
977
|
}
|
|
@@ -942,7 +982,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
942
982
|
await writeDependencyRange(release.pkg.manifestPath, bump.field, bump.dependency, bump.range);
|
|
943
983
|
anyRangeRewritten = true;
|
|
944
984
|
}
|
|
945
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
985
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
946
986
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
947
987
|
if (plugin.prepare) await plugin.prepare(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
948
988
|
}
|
|
@@ -962,14 +1002,14 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
962
1002
|
log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
|
|
963
1003
|
for (const release of planned) {
|
|
964
1004
|
const releases = [];
|
|
965
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
1005
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
966
1006
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
967
1007
|
if (plugin.publish) {
|
|
968
1008
|
const result = await plugin.publish(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
969
1009
|
if (result !== false && result !== void 0) releases.push(result);
|
|
970
1010
|
}
|
|
971
1011
|
}
|
|
972
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
1012
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
973
1013
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
974
1014
|
if (plugin.success) await plugin.success(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
975
1015
|
}
|
|
@@ -992,7 +1032,7 @@ async function analysePackage(pkg, options) {
|
|
|
992
1032
|
onCommitsResolved: options.onCommitsResolved
|
|
993
1033
|
});
|
|
994
1034
|
const semanticReleaseOptions = {
|
|
995
|
-
tagFormat:
|
|
1035
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
996
1036
|
plugins: options.resolvedPlugins,
|
|
997
1037
|
dryRun: true,
|
|
998
1038
|
async analyzeCommits(pluginConfig, context) {
|
|
@@ -1130,14 +1170,18 @@ async function releaseWorkspace(options = {}) {
|
|
|
1130
1170
|
validateDependencyRanges(graph);
|
|
1131
1171
|
const order = topologicalOrder(graph);
|
|
1132
1172
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1133
|
-
const publishPlugins =
|
|
1173
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1174
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1175
|
+
packagePlugins: options.packagePlugins
|
|
1176
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1134
1177
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1135
1178
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1136
1179
|
return {
|
|
1137
1180
|
order,
|
|
1138
1181
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1139
1182
|
const result = await runPackageRelease(pkg, {
|
|
1140
|
-
|
|
1183
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1184
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1141
1185
|
analyzeCommitsConfig,
|
|
1142
1186
|
generateNotesConfig,
|
|
1143
1187
|
bumpsForThisPackage,
|
|
@@ -1218,7 +1262,7 @@ async function runPackageRelease(pkg, options) {
|
|
|
1218
1262
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1219
1263
|
});
|
|
1220
1264
|
const semanticReleaseOptions = {
|
|
1221
|
-
tagFormat:
|
|
1265
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1222
1266
|
plugins: options.publishPlugins,
|
|
1223
1267
|
analyzeCommits: scoped.analyzeCommits,
|
|
1224
1268
|
generateNotes: scoped.generateNotes
|
|
@@ -1293,15 +1337,19 @@ async function detachWorkspaceRelease(options) {
|
|
|
1293
1337
|
validateDependencyRanges(graph);
|
|
1294
1338
|
const order = topologicalOrder(graph);
|
|
1295
1339
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1296
|
-
const publishPlugins =
|
|
1340
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1341
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1342
|
+
packagePlugins: options.packagePlugins
|
|
1343
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1297
1344
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1298
1345
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1299
1346
|
const entries = await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1300
1347
|
const state = await runPackageDetach(pkg, {
|
|
1301
|
-
publishPlugins,
|
|
1348
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1302
1349
|
analyzeCommitsConfig,
|
|
1303
1350
|
generateNotesConfig,
|
|
1304
1351
|
bumpsForThisPackage,
|
|
1352
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1305
1353
|
dryRun,
|
|
1306
1354
|
env,
|
|
1307
1355
|
branches: options.branches
|
|
@@ -1344,7 +1392,7 @@ async function runPackageDetach(pkg, options) {
|
|
|
1344
1392
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1345
1393
|
});
|
|
1346
1394
|
const cliOptions = {
|
|
1347
|
-
tagFormat:
|
|
1395
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1348
1396
|
plugins: options.publishPlugins,
|
|
1349
1397
|
analyzeCommits: scoped.analyzeCommits,
|
|
1350
1398
|
generateNotes: scoped.generateNotes
|
|
@@ -1427,6 +1475,7 @@ const CONFIG_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
|
1427
1475
|
"dryRun",
|
|
1428
1476
|
"branches",
|
|
1429
1477
|
"plugins",
|
|
1478
|
+
"packagePlugins",
|
|
1430
1479
|
"analyzeCommits",
|
|
1431
1480
|
"generateNotes",
|
|
1432
1481
|
"commitStrategy",
|
|
@@ -1454,7 +1503,7 @@ function createProgram() {
|
|
|
1454
1503
|
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);
|
|
1455
1504
|
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");
|
|
1456
1505
|
release.option("--gate-state-file <path>", "with --gate-publish: where to write the state a later \"resume\" run needs to finish publishing");
|
|
1457
|
-
release.option("--config <file>", "config file (.json, .yaml, .yml, .js, .cjs, or .ts) providing any of the release options (dryRun, branches, plugins, analyzeCommits, generateNotes, commitStrategy, gatePublish); explicit flags win");
|
|
1506
|
+
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");
|
|
1458
1507
|
release.action(runRelease);
|
|
1459
1508
|
const resume = program.command("resume");
|
|
1460
1509
|
resume.description("Finish publishing every package a --gate-publish release tagged and pushed but did not publish.");
|
|
@@ -1471,6 +1520,7 @@ const NO_CONFIG_FILE = {
|
|
|
1471
1520
|
dryRun: void 0,
|
|
1472
1521
|
branches: void 0,
|
|
1473
1522
|
plugins: void 0,
|
|
1523
|
+
packagePlugins: void 0,
|
|
1474
1524
|
analyzeCommits: void 0,
|
|
1475
1525
|
generateNotes: void 0,
|
|
1476
1526
|
commitStrategy: void 0,
|
|
@@ -1485,6 +1535,7 @@ async function runRelease(flags) {
|
|
|
1485
1535
|
dryRun: flags.dryRun ?? (file.dryRun === true ? true : void 0),
|
|
1486
1536
|
branches: flags.branches.length > 0 ? flags.branches : file.branches,
|
|
1487
1537
|
plugins: flags.plugin.length > 0 ? flags.plugin.map((spec) => parsePluginSpec(spec)) : file.plugins,
|
|
1538
|
+
packagePlugins: file.packagePlugins,
|
|
1488
1539
|
analyzeCommits: flags.analyzeCommits === void 0 ? file.analyzeCommits : parseJsonObjectFlag(flags.analyzeCommits, "--analyze-commits"),
|
|
1489
1540
|
generateNotes: flags.generateNotes === void 0 ? file.generateNotes : parseJsonObjectFlag(flags.generateNotes, "--generate-notes"),
|
|
1490
1541
|
commitStrategy: flags.commitStrategy ?? file.commitStrategy,
|
|
@@ -1549,10 +1600,11 @@ async function readReleaseConfigFile(path) {
|
|
|
1549
1600
|
const parsed = await readConfigFile(path);
|
|
1550
1601
|
if (!isJsonObject(parsed)) throw new InvalidArgumentError(`--config file ${path} must contain a JSON object`);
|
|
1551
1602
|
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(", ")}`);
|
|
1552
|
-
const { dryRun, branches, plugins, analyzeCommits, generateNotes, commitStrategy, gatePublish } = parsed;
|
|
1603
|
+
const { dryRun, branches, plugins, packagePlugins, analyzeCommits, generateNotes, commitStrategy, gatePublish } = parsed;
|
|
1553
1604
|
if (dryRun !== void 0 && typeof dryRun !== "boolean") throw new InvalidArgumentError(`--config file ${path}: "dryRun" must be a boolean`);
|
|
1554
1605
|
if (branches !== void 0 && !isStringArray(branches)) throw new InvalidArgumentError(`--config file ${path}: "branches" must be an array of branch name strings`);
|
|
1555
1606
|
if (plugins !== void 0 && !Array.isArray(plugins)) throw new InvalidArgumentError(`--config file ${path}: "plugins" must be an array`);
|
|
1607
|
+
if (packagePlugins !== void 0 && !isJsonObject(packagePlugins)) throw new InvalidArgumentError(`--config file ${path}: "packagePlugins" must be an object mapping package names to plugin arrays`);
|
|
1556
1608
|
if (analyzeCommits !== void 0 && !isJsonObject(analyzeCommits)) throw new InvalidArgumentError(`--config file ${path}: "analyzeCommits" must be an object`);
|
|
1557
1609
|
if (generateNotes !== void 0 && !isJsonObject(generateNotes)) throw new InvalidArgumentError(`--config file ${path}: "generateNotes" must be an object`);
|
|
1558
1610
|
if (commitStrategy !== void 0 && (typeof commitStrategy !== "string" || !isCommitStrategy(commitStrategy))) throw new InvalidArgumentError(`--config file ${path}: "commitStrategy" must be one of: ${[...COMMIT_STRATEGIES].join(", ")}`);
|
|
@@ -1561,16 +1613,25 @@ async function readReleaseConfigFile(path) {
|
|
|
1561
1613
|
dryRun,
|
|
1562
1614
|
branches,
|
|
1563
1615
|
plugins: plugins === void 0 ? void 0 : plugins.map((spec) => parseConfigFilePlugin(spec, path)),
|
|
1616
|
+
packagePlugins: packagePlugins === void 0 ? void 0 : parseConfigFilePackagePlugins(packagePlugins, path),
|
|
1564
1617
|
analyzeCommits,
|
|
1565
1618
|
generateNotes,
|
|
1566
1619
|
commitStrategy,
|
|
1567
1620
|
gatePublish
|
|
1568
1621
|
};
|
|
1569
1622
|
}
|
|
1570
|
-
function
|
|
1623
|
+
function parseConfigFilePackagePlugins(packagePlugins, path) {
|
|
1624
|
+
const parsed = {};
|
|
1625
|
+
for (const [name, list] of Object.entries(packagePlugins)) {
|
|
1626
|
+
if (!Array.isArray(list)) throw new InvalidArgumentError(`--config file ${path}: the "packagePlugins" entry for "${name}" must be an array`);
|
|
1627
|
+
parsed[name] = list.map((spec) => parseConfigFilePlugin(spec, path, "packagePlugins"));
|
|
1628
|
+
}
|
|
1629
|
+
return parsed;
|
|
1630
|
+
}
|
|
1631
|
+
function parseConfigFilePlugin(spec, path, key = "plugins") {
|
|
1571
1632
|
if (typeof spec === "string") return spec;
|
|
1572
1633
|
if (isPluginSpecTuple(spec)) return spec;
|
|
1573
|
-
throw new InvalidArgumentError(`--config file ${path}: each "
|
|
1634
|
+
throw new InvalidArgumentError(`--config file ${path}: each "${key}" entry must be a module name or a [name, config] array`);
|
|
1574
1635
|
}
|
|
1575
1636
|
function parseJson(raw, flag) {
|
|
1576
1637
|
try {
|
package/dist/index.cjs
CHANGED
|
@@ -805,6 +805,26 @@ function resolvePublishPlugins(specs, workspaceRoot, options) {
|
|
|
805
805
|
return resolved;
|
|
806
806
|
}
|
|
807
807
|
/**
|
|
808
|
+
* 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
|
+
*
|
|
810
|
+
* 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
|
+
*/
|
|
812
|
+
function resolveWorkspacePublishPlugins(packageNames, specs, workspaceRoot, options) {
|
|
813
|
+
const overrides = specs.packagePlugins === void 0 ? [] : Object.entries(specs.packagePlugins);
|
|
814
|
+
const known = new Set(packageNames);
|
|
815
|
+
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: ${packageNames.join(", ")}.`);
|
|
817
|
+
const workspaceWide = resolvePublishPlugins(specs.plugins, workspaceRoot, options);
|
|
818
|
+
const resolvedOverrides = /* @__PURE__ */ new Map();
|
|
819
|
+
for (const [name, list] of overrides) try {
|
|
820
|
+
resolvedOverrides.set(name, resolvePublishPlugins(list, workspaceRoot, options));
|
|
821
|
+
} catch (cause) {
|
|
822
|
+
if (cause instanceof ReleaseConfigurationError) throw new ReleaseConfigurationError(`packagePlugins for "${name}": ${cause.message}`);
|
|
823
|
+
throw cause;
|
|
824
|
+
}
|
|
825
|
+
return new Map(packageNames.map((name) => [name, resolvedOverrides.get(name) ?? workspaceWide]));
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
808
828
|
* 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.
|
|
809
829
|
*/
|
|
810
830
|
function resolvePluginModule(name, requireFromTool, requireFromWorkspace) {
|
|
@@ -837,6 +857,21 @@ async function regenerateLockfile(options) {
|
|
|
837
857
|
throw new PnpmCommandError(options.cwd, detail);
|
|
838
858
|
}
|
|
839
859
|
}
|
|
860
|
+
/**
|
|
861
|
+
* 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.
|
|
862
|
+
*/
|
|
863
|
+
function validateTagFormat(tagFormat) {
|
|
864
|
+
if (!tagFormat.includes("${version}")) throw new ReleaseConfigurationError(`tagFormat must contain '\${version}' so each package's release tag carries its version; got: ${tagFormat}`);
|
|
865
|
+
const unknown = [...tagFormat.matchAll(/\$\{([^}]*)\}/g)].map((match) => match[1] ?? "").filter((token) => token !== "name" && token !== "version").filter((token) => token !== "");
|
|
866
|
+
if (unknown.length > 0) throw new ReleaseConfigurationError(`tagFormat supports only the '\${name}' and '\${version}' placeholders; unknown token(s): ${unknown.map((token) => `\${${token}}`).join(", ")}`);
|
|
867
|
+
return tagFormat;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* 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.
|
|
871
|
+
*/
|
|
872
|
+
function formatTagForPackage(tagFormat, name) {
|
|
873
|
+
return tagFormat.replaceAll("${name}", name);
|
|
874
|
+
}
|
|
840
875
|
//#endregion
|
|
841
876
|
//#region src/single-commit-release.ts
|
|
842
877
|
/**
|
|
@@ -864,12 +899,16 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
864
899
|
validateDependencyRanges(graph);
|
|
865
900
|
const order = topologicalOrder(graph);
|
|
866
901
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
867
|
-
const resolvedPlugins =
|
|
902
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
903
|
+
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
904
|
+
packagePlugins: options.packagePlugins
|
|
905
|
+
}, workspace.root, {
|
|
868
906
|
requireGitPlugin: false,
|
|
869
907
|
forbidGitPlugin: true
|
|
870
908
|
});
|
|
871
909
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
872
910
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
911
|
+
const tagFormat = validateTagFormat(options.tagFormat ?? "${name}@${version}");
|
|
873
912
|
const capturedCommits = /* @__PURE__ */ new Map();
|
|
874
913
|
const captured = {
|
|
875
914
|
branch: void 0,
|
|
@@ -883,7 +922,8 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
883
922
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
884
923
|
pendingBumps.delete(name);
|
|
885
924
|
const nextRelease = await analysePackage(pkg, {
|
|
886
|
-
|
|
925
|
+
tagFormat,
|
|
926
|
+
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
887
927
|
analyzeCommitsConfig,
|
|
888
928
|
generateNotesConfig,
|
|
889
929
|
bumpsForThisPackage,
|
|
@@ -944,7 +984,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
944
984
|
log
|
|
945
985
|
};
|
|
946
986
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
947
|
-
for (const release of planned) for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
987
|
+
for (const release of planned) for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
948
988
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
949
989
|
if (plugin.verifyConditions) await plugin.verifyConditions(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
950
990
|
}
|
|
@@ -955,7 +995,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
955
995
|
await writeDependencyRange(release.pkg.manifestPath, bump.field, bump.dependency, bump.range);
|
|
956
996
|
anyRangeRewritten = true;
|
|
957
997
|
}
|
|
958
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
998
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
959
999
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
960
1000
|
if (plugin.prepare) await plugin.prepare(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
961
1001
|
}
|
|
@@ -975,14 +1015,14 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
975
1015
|
log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
|
|
976
1016
|
for (const release of planned) {
|
|
977
1017
|
const releases = [];
|
|
978
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
1018
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
979
1019
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
980
1020
|
if (plugin.publish) {
|
|
981
1021
|
const result = await plugin.publish(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
982
1022
|
if (result !== false && result !== void 0) releases.push(result);
|
|
983
1023
|
}
|
|
984
1024
|
}
|
|
985
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
1025
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
986
1026
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
987
1027
|
if (plugin.success) await plugin.success(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
988
1028
|
}
|
|
@@ -1005,7 +1045,7 @@ async function analysePackage(pkg, options) {
|
|
|
1005
1045
|
onCommitsResolved: options.onCommitsResolved
|
|
1006
1046
|
});
|
|
1007
1047
|
const semanticReleaseOptions = {
|
|
1008
|
-
tagFormat:
|
|
1048
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1009
1049
|
plugins: options.resolvedPlugins,
|
|
1010
1050
|
dryRun: true,
|
|
1011
1051
|
async analyzeCommits(pluginConfig, context) {
|
|
@@ -1136,15 +1176,19 @@ async function detachWorkspaceRelease(options) {
|
|
|
1136
1176
|
validateDependencyRanges(graph);
|
|
1137
1177
|
const order = topologicalOrder(graph);
|
|
1138
1178
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1139
|
-
const publishPlugins =
|
|
1179
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1180
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1181
|
+
packagePlugins: options.packagePlugins
|
|
1182
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1140
1183
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1141
1184
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1142
1185
|
const entries = await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1143
1186
|
const state = await runPackageDetach(pkg, {
|
|
1144
|
-
publishPlugins,
|
|
1187
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1145
1188
|
analyzeCommitsConfig,
|
|
1146
1189
|
generateNotesConfig,
|
|
1147
1190
|
bumpsForThisPackage,
|
|
1191
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1148
1192
|
dryRun,
|
|
1149
1193
|
env,
|
|
1150
1194
|
branches: options.branches
|
|
@@ -1187,7 +1231,7 @@ async function runPackageDetach(pkg, options) {
|
|
|
1187
1231
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1188
1232
|
});
|
|
1189
1233
|
const cliOptions = {
|
|
1190
|
-
tagFormat:
|
|
1234
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1191
1235
|
plugins: options.publishPlugins,
|
|
1192
1236
|
analyzeCommits: scoped.analyzeCommits,
|
|
1193
1237
|
generateNotes: scoped.generateNotes
|
|
@@ -1274,14 +1318,18 @@ async function releaseWorkspace(options = {}) {
|
|
|
1274
1318
|
validateDependencyRanges(graph);
|
|
1275
1319
|
const order = topologicalOrder(graph);
|
|
1276
1320
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1277
|
-
const publishPlugins =
|
|
1321
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1322
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1323
|
+
packagePlugins: options.packagePlugins
|
|
1324
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1278
1325
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1279
1326
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1280
1327
|
return {
|
|
1281
1328
|
order,
|
|
1282
1329
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1283
1330
|
const result = await runPackageRelease(pkg, {
|
|
1284
|
-
|
|
1331
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1332
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1285
1333
|
analyzeCommitsConfig,
|
|
1286
1334
|
generateNotesConfig,
|
|
1287
1335
|
bumpsForThisPackage,
|
|
@@ -1362,7 +1410,7 @@ async function runPackageRelease(pkg, options) {
|
|
|
1362
1410
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1363
1411
|
});
|
|
1364
1412
|
const semanticReleaseOptions = {
|
|
1365
|
-
tagFormat:
|
|
1413
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1366
1414
|
plugins: options.publishPlugins,
|
|
1367
1415
|
analyzeCommits: scoped.analyzeCommits,
|
|
1368
1416
|
generateNotes: scoped.generateNotes
|
|
@@ -1441,6 +1489,7 @@ exports.packageName = packageName;
|
|
|
1441
1489
|
exports.readManifest = readManifest;
|
|
1442
1490
|
exports.releaseWorkspace = releaseWorkspace;
|
|
1443
1491
|
exports.resolvePublishPlugins = resolvePublishPlugins;
|
|
1492
|
+
exports.resolveWorkspacePublishPlugins = resolveWorkspacePublishPlugins;
|
|
1444
1493
|
exports.resumeWorkspaceRelease = resumeWorkspaceRelease;
|
|
1445
1494
|
exports.topologicalOrder = topologicalOrder;
|
|
1446
1495
|
exports.updateDependencyRange = updateDependencyRange;
|
package/dist/index.d.cts
CHANGED
|
@@ -147,6 +147,8 @@ interface DependencyBumpSource {
|
|
|
147
147
|
}
|
|
148
148
|
/** A publish-pipeline plugin entry as the orchestrator accepts it: a module name, optionally with a config object. */
|
|
149
149
|
type PublishPluginSpec = string | readonly [string] | readonly [string, Record<string, unknown>];
|
|
150
|
+
/** 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
|
+
type PackagePluginSpecs = Readonly<Record<string, readonly PublishPluginSpec[]>>;
|
|
150
152
|
/** 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. */
|
|
151
153
|
export declare const DEFAULT_PUBLISH_PLUGINS: readonly PublishPluginSpec[];
|
|
152
154
|
/** 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. */
|
|
@@ -188,6 +190,18 @@ export declare function resolvePublishPlugins(specs: readonly PublishPluginSpec[
|
|
|
188
190
|
readonly requireGitPlugin: boolean;
|
|
189
191
|
readonly forbidGitPlugin?: boolean;
|
|
190
192
|
}): readonly ResolvedPublishPlugin[];
|
|
193
|
+
/**
|
|
194
|
+
* 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
|
+
*
|
|
196
|
+
* 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
|
+
*/
|
|
198
|
+
export declare function resolveWorkspacePublishPlugins(packageNames: readonly string[], specs: {
|
|
199
|
+
readonly plugins: readonly PublishPluginSpec[];
|
|
200
|
+
readonly packagePlugins: PackagePluginSpecs | undefined;
|
|
201
|
+
}, workspaceRoot: string, options: {
|
|
202
|
+
readonly requireGitPlugin: boolean;
|
|
203
|
+
readonly forbidGitPlugin?: boolean;
|
|
204
|
+
}): ReadonlyMap<string, readonly ResolvedPublishPlugin[]>;
|
|
191
205
|
//#endregion
|
|
192
206
|
//#region src/gate-publish.d.ts
|
|
193
207
|
/**
|
|
@@ -232,6 +246,8 @@ interface ReleaseWorkspaceOptions {
|
|
|
232
246
|
readonly branches?: readonly BranchSpec[];
|
|
233
247
|
/** 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'`. */
|
|
234
248
|
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. The usual use is keeping one package out of a step the rest need, for example leaving `@semantic-release/github` off a private package so it gets its tag, version bump and dependency cascade without a public GitHub Release. Applies under every `commitStrategy` and to `gatePublish`. A name that is not a package in the workspace is rejected. */
|
|
250
|
+
readonly packagePlugins?: PackagePluginSpecs;
|
|
235
251
|
/** Options for the wrapped `@semantic-release/commit-analyzer`, applied per package after path filtering. */
|
|
236
252
|
readonly analyzeCommits?: Record<string, unknown>;
|
|
237
253
|
/** Options for the wrapped `@semantic-release/release-notes-generator`, applied per package after path filtering. */
|
|
@@ -244,6 +260,14 @@ interface ReleaseWorkspaceOptions {
|
|
|
244
260
|
* 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.
|
|
245
261
|
*/
|
|
246
262
|
readonly gatePublish?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Lodash template for each package's release tag, with `${name}` and `${version}` placeholders.
|
|
265
|
+
* Defaults to `'${name}@${version}'`. Override it when the tags double as refs consumed outside
|
|
266
|
+
* git: GitHub Actions pins composite actions as `owner/repo/path@ref`, and GitHub's workflow
|
|
267
|
+
* parser rejects a ref containing `@`, so a repository of actions sets `'${name}-v${version}'`
|
|
268
|
+
* and pins `...@<action>-v1`. Validated once per run by `validateTagFormat`.
|
|
269
|
+
*/
|
|
270
|
+
readonly tagFormat?: string;
|
|
247
271
|
}
|
|
248
272
|
/** One dependency-range change applied to a dependent package's manifest during the run, attached to the dependent's own outcome. */
|
|
249
273
|
interface AppliedDependencyBump extends DependencyBump {
|
|
@@ -310,4 +334,4 @@ export declare class GitCommandError extends WorkspaceReleaseError {
|
|
|
310
334
|
/** 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. */
|
|
311
335
|
export declare class WorkspaceStateError extends WorkspaceReleaseError {}
|
|
312
336
|
//#endregion
|
|
313
|
-
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackageReleaseOutcome, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
|
337
|
+
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackagePluginSpecs, PackageReleaseOutcome, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
package/dist/index.d.ts
CHANGED
|
@@ -147,6 +147,8 @@ interface DependencyBumpSource {
|
|
|
147
147
|
}
|
|
148
148
|
/** A publish-pipeline plugin entry as the orchestrator accepts it: a module name, optionally with a config object. */
|
|
149
149
|
type PublishPluginSpec = string | readonly [string] | readonly [string, Record<string, unknown>];
|
|
150
|
+
/** 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
|
+
type PackagePluginSpecs = Readonly<Record<string, readonly PublishPluginSpec[]>>;
|
|
150
152
|
/** 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. */
|
|
151
153
|
export declare const DEFAULT_PUBLISH_PLUGINS: readonly PublishPluginSpec[];
|
|
152
154
|
/** 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. */
|
|
@@ -188,6 +190,18 @@ export declare function resolvePublishPlugins(specs: readonly PublishPluginSpec[
|
|
|
188
190
|
readonly requireGitPlugin: boolean;
|
|
189
191
|
readonly forbidGitPlugin?: boolean;
|
|
190
192
|
}): readonly ResolvedPublishPlugin[];
|
|
193
|
+
/**
|
|
194
|
+
* 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
|
+
*
|
|
196
|
+
* 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
|
+
*/
|
|
198
|
+
export declare function resolveWorkspacePublishPlugins(packageNames: readonly string[], specs: {
|
|
199
|
+
readonly plugins: readonly PublishPluginSpec[];
|
|
200
|
+
readonly packagePlugins: PackagePluginSpecs | undefined;
|
|
201
|
+
}, workspaceRoot: string, options: {
|
|
202
|
+
readonly requireGitPlugin: boolean;
|
|
203
|
+
readonly forbidGitPlugin?: boolean;
|
|
204
|
+
}): ReadonlyMap<string, readonly ResolvedPublishPlugin[]>;
|
|
191
205
|
//#endregion
|
|
192
206
|
//#region src/gate-publish.d.ts
|
|
193
207
|
/**
|
|
@@ -232,6 +246,8 @@ interface ReleaseWorkspaceOptions {
|
|
|
232
246
|
readonly branches?: readonly BranchSpec[];
|
|
233
247
|
/** 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'`. */
|
|
234
248
|
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. The usual use is keeping one package out of a step the rest need, for example leaving `@semantic-release/github` off a private package so it gets its tag, version bump and dependency cascade without a public GitHub Release. Applies under every `commitStrategy` and to `gatePublish`. A name that is not a package in the workspace is rejected. */
|
|
250
|
+
readonly packagePlugins?: PackagePluginSpecs;
|
|
235
251
|
/** Options for the wrapped `@semantic-release/commit-analyzer`, applied per package after path filtering. */
|
|
236
252
|
readonly analyzeCommits?: Record<string, unknown>;
|
|
237
253
|
/** Options for the wrapped `@semantic-release/release-notes-generator`, applied per package after path filtering. */
|
|
@@ -244,6 +260,14 @@ interface ReleaseWorkspaceOptions {
|
|
|
244
260
|
* 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.
|
|
245
261
|
*/
|
|
246
262
|
readonly gatePublish?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Lodash template for each package's release tag, with `${name}` and `${version}` placeholders.
|
|
265
|
+
* Defaults to `'${name}@${version}'`. Override it when the tags double as refs consumed outside
|
|
266
|
+
* git: GitHub Actions pins composite actions as `owner/repo/path@ref`, and GitHub's workflow
|
|
267
|
+
* parser rejects a ref containing `@`, so a repository of actions sets `'${name}-v${version}'`
|
|
268
|
+
* and pins `...@<action>-v1`. Validated once per run by `validateTagFormat`.
|
|
269
|
+
*/
|
|
270
|
+
readonly tagFormat?: string;
|
|
247
271
|
}
|
|
248
272
|
/** One dependency-range change applied to a dependent package's manifest during the run, attached to the dependent's own outcome. */
|
|
249
273
|
interface AppliedDependencyBump extends DependencyBump {
|
|
@@ -310,4 +334,4 @@ export declare class GitCommandError extends WorkspaceReleaseError {
|
|
|
310
334
|
/** 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. */
|
|
311
335
|
export declare class WorkspaceStateError extends WorkspaceReleaseError {}
|
|
312
336
|
//#endregion
|
|
313
|
-
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackageReleaseOutcome, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
|
337
|
+
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackagePluginSpecs, PackageReleaseOutcome, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
package/dist/index.js
CHANGED
|
@@ -780,6 +780,26 @@ function resolvePublishPlugins(specs, workspaceRoot, options) {
|
|
|
780
780
|
return resolved;
|
|
781
781
|
}
|
|
782
782
|
/**
|
|
783
|
+
* 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
|
+
*
|
|
785
|
+
* 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
|
+
*/
|
|
787
|
+
function resolveWorkspacePublishPlugins(packageNames, specs, workspaceRoot, options) {
|
|
788
|
+
const overrides = specs.packagePlugins === void 0 ? [] : Object.entries(specs.packagePlugins);
|
|
789
|
+
const known = new Set(packageNames);
|
|
790
|
+
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: ${packageNames.join(", ")}.`);
|
|
792
|
+
const workspaceWide = resolvePublishPlugins(specs.plugins, workspaceRoot, options);
|
|
793
|
+
const resolvedOverrides = /* @__PURE__ */ new Map();
|
|
794
|
+
for (const [name, list] of overrides) try {
|
|
795
|
+
resolvedOverrides.set(name, resolvePublishPlugins(list, workspaceRoot, options));
|
|
796
|
+
} catch (cause) {
|
|
797
|
+
if (cause instanceof ReleaseConfigurationError) throw new ReleaseConfigurationError(`packagePlugins for "${name}": ${cause.message}`);
|
|
798
|
+
throw cause;
|
|
799
|
+
}
|
|
800
|
+
return new Map(packageNames.map((name) => [name, resolvedOverrides.get(name) ?? workspaceWide]));
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
783
803
|
* 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.
|
|
784
804
|
*/
|
|
785
805
|
function resolvePluginModule(name, requireFromTool, requireFromWorkspace) {
|
|
@@ -812,6 +832,21 @@ async function regenerateLockfile(options) {
|
|
|
812
832
|
throw new PnpmCommandError(options.cwd, detail);
|
|
813
833
|
}
|
|
814
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* 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.
|
|
837
|
+
*/
|
|
838
|
+
function validateTagFormat(tagFormat) {
|
|
839
|
+
if (!tagFormat.includes("${version}")) throw new ReleaseConfigurationError(`tagFormat must contain '\${version}' so each package's release tag carries its version; got: ${tagFormat}`);
|
|
840
|
+
const unknown = [...tagFormat.matchAll(/\$\{([^}]*)\}/g)].map((match) => match[1] ?? "").filter((token) => token !== "name" && token !== "version").filter((token) => token !== "");
|
|
841
|
+
if (unknown.length > 0) throw new ReleaseConfigurationError(`tagFormat supports only the '\${name}' and '\${version}' placeholders; unknown token(s): ${unknown.map((token) => `\${${token}}`).join(", ")}`);
|
|
842
|
+
return tagFormat;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* 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.
|
|
846
|
+
*/
|
|
847
|
+
function formatTagForPackage(tagFormat, name) {
|
|
848
|
+
return tagFormat.replaceAll("${name}", name);
|
|
849
|
+
}
|
|
815
850
|
//#endregion
|
|
816
851
|
//#region src/single-commit-release.ts
|
|
817
852
|
/**
|
|
@@ -839,12 +874,16 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
839
874
|
validateDependencyRanges(graph);
|
|
840
875
|
const order = topologicalOrder(graph);
|
|
841
876
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
842
|
-
const resolvedPlugins =
|
|
877
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
878
|
+
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
879
|
+
packagePlugins: options.packagePlugins
|
|
880
|
+
}, workspace.root, {
|
|
843
881
|
requireGitPlugin: false,
|
|
844
882
|
forbidGitPlugin: true
|
|
845
883
|
});
|
|
846
884
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
847
885
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
886
|
+
const tagFormat = validateTagFormat(options.tagFormat ?? "${name}@${version}");
|
|
848
887
|
const capturedCommits = /* @__PURE__ */ new Map();
|
|
849
888
|
const captured = {
|
|
850
889
|
branch: void 0,
|
|
@@ -858,7 +897,8 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
858
897
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
859
898
|
pendingBumps.delete(name);
|
|
860
899
|
const nextRelease = await analysePackage(pkg, {
|
|
861
|
-
|
|
900
|
+
tagFormat,
|
|
901
|
+
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
862
902
|
analyzeCommitsConfig,
|
|
863
903
|
generateNotesConfig,
|
|
864
904
|
bumpsForThisPackage,
|
|
@@ -919,7 +959,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
919
959
|
log
|
|
920
960
|
};
|
|
921
961
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
922
|
-
for (const release of planned) for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
962
|
+
for (const release of planned) for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
923
963
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
924
964
|
if (plugin.verifyConditions) await plugin.verifyConditions(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
925
965
|
}
|
|
@@ -930,7 +970,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
930
970
|
await writeDependencyRange(release.pkg.manifestPath, bump.field, bump.dependency, bump.range);
|
|
931
971
|
anyRangeRewritten = true;
|
|
932
972
|
}
|
|
933
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
973
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
934
974
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
935
975
|
if (plugin.prepare) await plugin.prepare(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
936
976
|
}
|
|
@@ -950,14 +990,14 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
950
990
|
log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
|
|
951
991
|
for (const release of planned) {
|
|
952
992
|
const releases = [];
|
|
953
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
993
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
954
994
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
955
995
|
if (plugin.publish) {
|
|
956
996
|
const result = await plugin.publish(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
957
997
|
if (result !== false && result !== void 0) releases.push(result);
|
|
958
998
|
}
|
|
959
999
|
}
|
|
960
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
1000
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
961
1001
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
962
1002
|
if (plugin.success) await plugin.success(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
963
1003
|
}
|
|
@@ -980,7 +1020,7 @@ async function analysePackage(pkg, options) {
|
|
|
980
1020
|
onCommitsResolved: options.onCommitsResolved
|
|
981
1021
|
});
|
|
982
1022
|
const semanticReleaseOptions = {
|
|
983
|
-
tagFormat:
|
|
1023
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
984
1024
|
plugins: options.resolvedPlugins,
|
|
985
1025
|
dryRun: true,
|
|
986
1026
|
async analyzeCommits(pluginConfig, context) {
|
|
@@ -1111,15 +1151,19 @@ async function detachWorkspaceRelease(options) {
|
|
|
1111
1151
|
validateDependencyRanges(graph);
|
|
1112
1152
|
const order = topologicalOrder(graph);
|
|
1113
1153
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1114
|
-
const publishPlugins =
|
|
1154
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1155
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1156
|
+
packagePlugins: options.packagePlugins
|
|
1157
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1115
1158
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1116
1159
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1117
1160
|
const entries = await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1118
1161
|
const state = await runPackageDetach(pkg, {
|
|
1119
|
-
publishPlugins,
|
|
1162
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1120
1163
|
analyzeCommitsConfig,
|
|
1121
1164
|
generateNotesConfig,
|
|
1122
1165
|
bumpsForThisPackage,
|
|
1166
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1123
1167
|
dryRun,
|
|
1124
1168
|
env,
|
|
1125
1169
|
branches: options.branches
|
|
@@ -1162,7 +1206,7 @@ async function runPackageDetach(pkg, options) {
|
|
|
1162
1206
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1163
1207
|
});
|
|
1164
1208
|
const cliOptions = {
|
|
1165
|
-
tagFormat:
|
|
1209
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1166
1210
|
plugins: options.publishPlugins,
|
|
1167
1211
|
analyzeCommits: scoped.analyzeCommits,
|
|
1168
1212
|
generateNotes: scoped.generateNotes
|
|
@@ -1249,14 +1293,18 @@ async function releaseWorkspace(options = {}) {
|
|
|
1249
1293
|
validateDependencyRanges(graph);
|
|
1250
1294
|
const order = topologicalOrder(graph);
|
|
1251
1295
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1252
|
-
const publishPlugins =
|
|
1296
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1297
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1298
|
+
packagePlugins: options.packagePlugins
|
|
1299
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1253
1300
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1254
1301
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1255
1302
|
return {
|
|
1256
1303
|
order,
|
|
1257
1304
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1258
1305
|
const result = await runPackageRelease(pkg, {
|
|
1259
|
-
|
|
1306
|
+
tagFormat: validateTagFormat(options.tagFormat ?? "${name}@${version}"),
|
|
1307
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1260
1308
|
analyzeCommitsConfig,
|
|
1261
1309
|
generateNotesConfig,
|
|
1262
1310
|
bumpsForThisPackage,
|
|
@@ -1337,7 +1385,7 @@ async function runPackageRelease(pkg, options) {
|
|
|
1337
1385
|
bumps: { bumpsFor: () => options.bumpsForThisPackage }
|
|
1338
1386
|
});
|
|
1339
1387
|
const semanticReleaseOptions = {
|
|
1340
|
-
tagFormat:
|
|
1388
|
+
tagFormat: formatTagForPackage(options.tagFormat, pkg.name),
|
|
1341
1389
|
plugins: options.publishPlugins,
|
|
1342
1390
|
analyzeCommits: scoped.analyzeCommits,
|
|
1343
1391
|
generateNotes: scoped.generateNotes
|
|
@@ -1398,4 +1446,4 @@ async function bumpDependents(released, version, graph, options) {
|
|
|
1398
1446
|
return applied;
|
|
1399
1447
|
}
|
|
1400
1448
|
//#endregion
|
|
1401
|
-
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, resumeWorkspaceRelease, topologicalOrder, updateDependencyRange, writeDependencyRange };
|
|
1449
|
+
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 };
|
package/package.json
CHANGED