@exadev/semantic-release-workspace 1.3.8 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/dist/cli.js +64 -22
- package/dist/index.cjs +40 -10
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +40 -11
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ Run it from the workspace root (or pass `--root <directory>`). A dry run analyse
|
|
|
206
206
|
| `--commit-strategy <mode>` | `per-package` (default) or `single` — see [Commit strategies](#commit-strategies) |
|
|
207
207
|
| `--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
208
|
| `--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`,
|
|
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, 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
210
|
|
|
211
211
|
`resume` (a separate subcommand, not a `release` flag) finishes publishing what a `--gate-publish` run tagged and pushed:
|
|
212
212
|
|
|
@@ -219,6 +219,27 @@ Listing `@semantic-release/commit-analyzer` or `@semantic-release/release-notes-
|
|
|
219
219
|
|
|
220
220
|
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
221
|
|
|
222
|
+
### Per-package publish plugins
|
|
223
|
+
|
|
224
|
+
`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.
|
|
225
|
+
|
|
226
|
+
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:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// release-workspace.config.ts
|
|
230
|
+
import { DEFAULT_PUBLISH_PLUGINS, type ReleaseWorkspaceOptions } from '@exadev/semantic-release-workspace';
|
|
231
|
+
|
|
232
|
+
const config: ReleaseWorkspaceOptions = {
|
|
233
|
+
packagePlugins: {
|
|
234
|
+
'@acme/web-console': DEFAULT_PUBLISH_PLUGINS.filter((plugin) => plugin !== '@semantic-release/github'),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export default config;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
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`.
|
|
242
|
+
|
|
222
243
|
### Programmatic API
|
|
223
244
|
|
|
224
245
|
```ts
|
|
@@ -255,6 +276,7 @@ Every stage is also exported individually — `discoverWorkspace`, `buildDepende
|
|
|
255
276
|
|
|
256
277
|
### Repository requirements
|
|
257
278
|
|
|
279
|
+
- Node 22.18 or later on the 22 line, or Node 24 and later: the range the config loader (cosmiconfig) supports, and the first 22 release that strips TypeScript types without a flag.
|
|
258
280
|
- A git repository with a pushable `origin` (semantic-release verifies push access even in dry runs, and pushes tags and release commits in real ones). The workspace does not need to sit at the repository's toplevel -- discovery resolves its own prefix within the repository and scopes commit filtering against it -- but it does need to sit inside one.
|
|
259
281
|
- A git identity (`user.name`/`user.email`) in CI for the `[skip ci]` bump commits, or the semantic-release-bot fallback identity is used automatically.
|
|
260
282
|
- A branch checkout, not a detached HEAD: dependency-bump commits are pushed to the current branch by name, so a detached HEAD stops the run with a `WorkspaceStateError` rather than pushing `HEAD:HEAD` at the remote. CI checkouts that default to a detached HEAD need the branch checked out explicitly.
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import {
|
|
4
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
5
5
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
6
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { detachRelease, resumeRelease } from "@exadev/release-gate";
|
|
@@ -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 = "1.
|
|
17
|
+
var version = "2.1.0";
|
|
18
18
|
//#endregion
|
|
19
19
|
//#region src/errors.ts
|
|
20
20
|
/**
|
|
@@ -792,6 +792,26 @@ function resolvePublishPlugins(specs, workspaceRoot, options) {
|
|
|
792
792
|
return resolved;
|
|
793
793
|
}
|
|
794
794
|
/**
|
|
795
|
+
* 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
|
+
*
|
|
797
|
+
* 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
|
+
*/
|
|
799
|
+
function resolveWorkspacePublishPlugins(packageNames, specs, workspaceRoot, options) {
|
|
800
|
+
const overrides = specs.packagePlugins === void 0 ? [] : Object.entries(specs.packagePlugins);
|
|
801
|
+
const known = new Set(packageNames);
|
|
802
|
+
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: ${packageNames.join(", ")}.`);
|
|
804
|
+
const workspaceWide = resolvePublishPlugins(specs.plugins, workspaceRoot, options);
|
|
805
|
+
const resolvedOverrides = /* @__PURE__ */ new Map();
|
|
806
|
+
for (const [name, list] of overrides) try {
|
|
807
|
+
resolvedOverrides.set(name, resolvePublishPlugins(list, workspaceRoot, options));
|
|
808
|
+
} catch (cause) {
|
|
809
|
+
if (cause instanceof ReleaseConfigurationError) throw new ReleaseConfigurationError(`packagePlugins for "${name}": ${cause.message}`);
|
|
810
|
+
throw cause;
|
|
811
|
+
}
|
|
812
|
+
return new Map(packageNames.map((name) => [name, resolvedOverrides.get(name) ?? workspaceWide]));
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
795
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.
|
|
796
816
|
*/
|
|
797
817
|
function resolvePluginModule(name, requireFromTool, requireFromWorkspace) {
|
|
@@ -851,7 +871,10 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
851
871
|
validateDependencyRanges(graph);
|
|
852
872
|
const order = topologicalOrder(graph);
|
|
853
873
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
854
|
-
const resolvedPlugins =
|
|
874
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
875
|
+
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
876
|
+
packagePlugins: options.packagePlugins
|
|
877
|
+
}, workspace.root, {
|
|
855
878
|
requireGitPlugin: false,
|
|
856
879
|
forbidGitPlugin: true
|
|
857
880
|
});
|
|
@@ -870,7 +893,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
870
893
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
871
894
|
pendingBumps.delete(name);
|
|
872
895
|
const nextRelease = await analysePackage(pkg, {
|
|
873
|
-
resolvedPlugins,
|
|
896
|
+
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
874
897
|
analyzeCommitsConfig,
|
|
875
898
|
generateNotesConfig,
|
|
876
899
|
bumpsForThisPackage,
|
|
@@ -931,7 +954,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
931
954
|
log
|
|
932
955
|
};
|
|
933
956
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
934
|
-
for (const release of planned) for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
957
|
+
for (const release of planned) for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
935
958
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
936
959
|
if (plugin.verifyConditions) await plugin.verifyConditions(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
937
960
|
}
|
|
@@ -942,7 +965,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
942
965
|
await writeDependencyRange(release.pkg.manifestPath, bump.field, bump.dependency, bump.range);
|
|
943
966
|
anyRangeRewritten = true;
|
|
944
967
|
}
|
|
945
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
968
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
946
969
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
947
970
|
if (plugin.prepare) await plugin.prepare(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
948
971
|
}
|
|
@@ -962,14 +985,14 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
962
985
|
log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
|
|
963
986
|
for (const release of planned) {
|
|
964
987
|
const releases = [];
|
|
965
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
988
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
966
989
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
967
990
|
if (plugin.publish) {
|
|
968
991
|
const result = await plugin.publish(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
969
992
|
if (result !== false && result !== void 0) releases.push(result);
|
|
970
993
|
}
|
|
971
994
|
}
|
|
972
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
995
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
973
996
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
974
997
|
if (plugin.success) await plugin.success(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
975
998
|
}
|
|
@@ -1130,14 +1153,17 @@ async function releaseWorkspace(options = {}) {
|
|
|
1130
1153
|
validateDependencyRanges(graph);
|
|
1131
1154
|
const order = topologicalOrder(graph);
|
|
1132
1155
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1133
|
-
const publishPlugins =
|
|
1156
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1157
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1158
|
+
packagePlugins: options.packagePlugins
|
|
1159
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1134
1160
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1135
1161
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1136
1162
|
return {
|
|
1137
1163
|
order,
|
|
1138
1164
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1139
1165
|
const result = await runPackageRelease(pkg, {
|
|
1140
|
-
publishPlugins,
|
|
1166
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1141
1167
|
analyzeCommitsConfig,
|
|
1142
1168
|
generateNotesConfig,
|
|
1143
1169
|
bumpsForThisPackage,
|
|
@@ -1293,12 +1319,15 @@ async function detachWorkspaceRelease(options) {
|
|
|
1293
1319
|
validateDependencyRanges(graph);
|
|
1294
1320
|
const order = topologicalOrder(graph);
|
|
1295
1321
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1296
|
-
const publishPlugins =
|
|
1322
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1323
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1324
|
+
packagePlugins: options.packagePlugins
|
|
1325
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1297
1326
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1298
1327
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1299
1328
|
const entries = await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1300
1329
|
const state = await runPackageDetach(pkg, {
|
|
1301
|
-
publishPlugins,
|
|
1330
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1302
1331
|
analyzeCommitsConfig,
|
|
1303
1332
|
generateNotesConfig,
|
|
1304
1333
|
bumpsForThisPackage,
|
|
@@ -1427,6 +1456,7 @@ const CONFIG_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
|
1427
1456
|
"dryRun",
|
|
1428
1457
|
"branches",
|
|
1429
1458
|
"plugins",
|
|
1459
|
+
"packagePlugins",
|
|
1430
1460
|
"analyzeCommits",
|
|
1431
1461
|
"generateNotes",
|
|
1432
1462
|
"commitStrategy",
|
|
@@ -1454,7 +1484,7 @@ function createProgram() {
|
|
|
1454
1484
|
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
1485
|
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
1486
|
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");
|
|
1487
|
+
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
1488
|
release.action(runRelease);
|
|
1459
1489
|
const resume = program.command("resume");
|
|
1460
1490
|
resume.description("Finish publishing every package a --gate-publish release tagged and pushed but did not publish.");
|
|
@@ -1471,13 +1501,14 @@ const NO_CONFIG_FILE = {
|
|
|
1471
1501
|
dryRun: void 0,
|
|
1472
1502
|
branches: void 0,
|
|
1473
1503
|
plugins: void 0,
|
|
1504
|
+
packagePlugins: void 0,
|
|
1474
1505
|
analyzeCommits: void 0,
|
|
1475
1506
|
generateNotes: void 0,
|
|
1476
1507
|
commitStrategy: void 0,
|
|
1477
1508
|
gatePublish: void 0
|
|
1478
1509
|
};
|
|
1479
1510
|
async function runRelease(flags) {
|
|
1480
|
-
const file = flags.config === void 0 ? NO_CONFIG_FILE : readReleaseConfigFile(flags.config);
|
|
1511
|
+
const file = flags.config === void 0 ? NO_CONFIG_FILE : await readReleaseConfigFile(flags.config);
|
|
1481
1512
|
const gatePublish = flags.gatePublish ?? file.gatePublish ?? false;
|
|
1482
1513
|
if (gatePublish && flags.gateStateFile === void 0) throw new InvalidArgumentError("--gate-publish requires --gate-state-file, since that is where the state a later \"resume\" run needs gets written");
|
|
1483
1514
|
const outcome = await releaseWorkspace({
|
|
@@ -1485,6 +1516,7 @@ async function runRelease(flags) {
|
|
|
1485
1516
|
dryRun: flags.dryRun ?? (file.dryRun === true ? true : void 0),
|
|
1486
1517
|
branches: flags.branches.length > 0 ? flags.branches : file.branches,
|
|
1487
1518
|
plugins: flags.plugin.length > 0 ? flags.plugin.map((spec) => parsePluginSpec(spec)) : file.plugins,
|
|
1519
|
+
packagePlugins: file.packagePlugins,
|
|
1488
1520
|
analyzeCommits: flags.analyzeCommits === void 0 ? file.analyzeCommits : parseJsonObjectFlag(flags.analyzeCommits, "--analyze-commits"),
|
|
1489
1521
|
generateNotes: flags.generateNotes === void 0 ? file.generateNotes : parseJsonObjectFlag(flags.generateNotes, "--generate-notes"),
|
|
1490
1522
|
commitStrategy: flags.commitStrategy ?? file.commitStrategy,
|
|
@@ -1534,25 +1566,26 @@ function parsePluginSpec(raw) {
|
|
|
1534
1566
|
if (!isPluginSpecTuple(parsed)) throw new InvalidArgumentError("--plugin must be a module name or a JSON array of [name, config]");
|
|
1535
1567
|
return parsed;
|
|
1536
1568
|
}
|
|
1537
|
-
function readConfigFile(path) {
|
|
1538
|
-
const explorer =
|
|
1569
|
+
async function readConfigFile(path) {
|
|
1570
|
+
const explorer = cosmiconfig("semantic-release-workspace");
|
|
1539
1571
|
let result;
|
|
1540
1572
|
try {
|
|
1541
|
-
result = explorer.load(path);
|
|
1573
|
+
result = await explorer.load(path);
|
|
1542
1574
|
} catch (cause) {
|
|
1543
1575
|
throw new InvalidArgumentError(`--config file ${path} could not be loaded: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
1544
1576
|
}
|
|
1545
1577
|
if (result === null || result.isEmpty === true) throw new InvalidArgumentError(`--config file ${path} is empty`);
|
|
1546
1578
|
return result.config;
|
|
1547
1579
|
}
|
|
1548
|
-
function readReleaseConfigFile(path) {
|
|
1549
|
-
const parsed = readConfigFile(path);
|
|
1580
|
+
async function readReleaseConfigFile(path) {
|
|
1581
|
+
const parsed = await readConfigFile(path);
|
|
1550
1582
|
if (!isJsonObject(parsed)) throw new InvalidArgumentError(`--config file ${path} must contain a JSON object`);
|
|
1551
1583
|
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;
|
|
1584
|
+
const { dryRun, branches, plugins, packagePlugins, analyzeCommits, generateNotes, commitStrategy, gatePublish } = parsed;
|
|
1553
1585
|
if (dryRun !== void 0 && typeof dryRun !== "boolean") throw new InvalidArgumentError(`--config file ${path}: "dryRun" must be a boolean`);
|
|
1554
1586
|
if (branches !== void 0 && !isStringArray(branches)) throw new InvalidArgumentError(`--config file ${path}: "branches" must be an array of branch name strings`);
|
|
1555
1587
|
if (plugins !== void 0 && !Array.isArray(plugins)) throw new InvalidArgumentError(`--config file ${path}: "plugins" must be an array`);
|
|
1588
|
+
if (packagePlugins !== void 0 && !isJsonObject(packagePlugins)) throw new InvalidArgumentError(`--config file ${path}: "packagePlugins" must be an object mapping package names to plugin arrays`);
|
|
1556
1589
|
if (analyzeCommits !== void 0 && !isJsonObject(analyzeCommits)) throw new InvalidArgumentError(`--config file ${path}: "analyzeCommits" must be an object`);
|
|
1557
1590
|
if (generateNotes !== void 0 && !isJsonObject(generateNotes)) throw new InvalidArgumentError(`--config file ${path}: "generateNotes" must be an object`);
|
|
1558
1591
|
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 +1594,25 @@ function readReleaseConfigFile(path) {
|
|
|
1561
1594
|
dryRun,
|
|
1562
1595
|
branches,
|
|
1563
1596
|
plugins: plugins === void 0 ? void 0 : plugins.map((spec) => parseConfigFilePlugin(spec, path)),
|
|
1597
|
+
packagePlugins: packagePlugins === void 0 ? void 0 : parseConfigFilePackagePlugins(packagePlugins, path),
|
|
1564
1598
|
analyzeCommits,
|
|
1565
1599
|
generateNotes,
|
|
1566
1600
|
commitStrategy,
|
|
1567
1601
|
gatePublish
|
|
1568
1602
|
};
|
|
1569
1603
|
}
|
|
1570
|
-
function
|
|
1604
|
+
function parseConfigFilePackagePlugins(packagePlugins, path) {
|
|
1605
|
+
const parsed = {};
|
|
1606
|
+
for (const [name, list] of Object.entries(packagePlugins)) {
|
|
1607
|
+
if (!Array.isArray(list)) throw new InvalidArgumentError(`--config file ${path}: the "packagePlugins" entry for "${name}" must be an array`);
|
|
1608
|
+
parsed[name] = list.map((spec) => parseConfigFilePlugin(spec, path, "packagePlugins"));
|
|
1609
|
+
}
|
|
1610
|
+
return parsed;
|
|
1611
|
+
}
|
|
1612
|
+
function parseConfigFilePlugin(spec, path, key = "plugins") {
|
|
1571
1613
|
if (typeof spec === "string") return spec;
|
|
1572
1614
|
if (isPluginSpecTuple(spec)) return spec;
|
|
1573
|
-
throw new InvalidArgumentError(`--config file ${path}: each "
|
|
1615
|
+
throw new InvalidArgumentError(`--config file ${path}: each "${key}" entry must be a module name or a [name, config] array`);
|
|
1574
1616
|
}
|
|
1575
1617
|
function parseJson(raw, flag) {
|
|
1576
1618
|
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) {
|
|
@@ -864,7 +884,10 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
864
884
|
validateDependencyRanges(graph);
|
|
865
885
|
const order = topologicalOrder(graph);
|
|
866
886
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
867
|
-
const resolvedPlugins =
|
|
887
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
888
|
+
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
889
|
+
packagePlugins: options.packagePlugins
|
|
890
|
+
}, workspace.root, {
|
|
868
891
|
requireGitPlugin: false,
|
|
869
892
|
forbidGitPlugin: true
|
|
870
893
|
});
|
|
@@ -883,7 +906,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
883
906
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
884
907
|
pendingBumps.delete(name);
|
|
885
908
|
const nextRelease = await analysePackage(pkg, {
|
|
886
|
-
resolvedPlugins,
|
|
909
|
+
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
887
910
|
analyzeCommitsConfig,
|
|
888
911
|
generateNotesConfig,
|
|
889
912
|
bumpsForThisPackage,
|
|
@@ -944,7 +967,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
944
967
|
log
|
|
945
968
|
};
|
|
946
969
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
947
|
-
for (const release of planned) for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
970
|
+
for (const release of planned) for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
948
971
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
949
972
|
if (plugin.verifyConditions) await plugin.verifyConditions(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
950
973
|
}
|
|
@@ -955,7 +978,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
955
978
|
await writeDependencyRange(release.pkg.manifestPath, bump.field, bump.dependency, bump.range);
|
|
956
979
|
anyRangeRewritten = true;
|
|
957
980
|
}
|
|
958
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
981
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
959
982
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
960
983
|
if (plugin.prepare) await plugin.prepare(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
961
984
|
}
|
|
@@ -975,14 +998,14 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
975
998
|
log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
|
|
976
999
|
for (const release of planned) {
|
|
977
1000
|
const releases = [];
|
|
978
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
1001
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
979
1002
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
980
1003
|
if (plugin.publish) {
|
|
981
1004
|
const result = await plugin.publish(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
982
1005
|
if (result !== false && result !== void 0) releases.push(result);
|
|
983
1006
|
}
|
|
984
1007
|
}
|
|
985
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
1008
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
986
1009
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
987
1010
|
if (plugin.success) await plugin.success(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
988
1011
|
}
|
|
@@ -1136,12 +1159,15 @@ async function detachWorkspaceRelease(options) {
|
|
|
1136
1159
|
validateDependencyRanges(graph);
|
|
1137
1160
|
const order = topologicalOrder(graph);
|
|
1138
1161
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1139
|
-
const publishPlugins =
|
|
1162
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1163
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1164
|
+
packagePlugins: options.packagePlugins
|
|
1165
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1140
1166
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1141
1167
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1142
1168
|
const entries = await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1143
1169
|
const state = await runPackageDetach(pkg, {
|
|
1144
|
-
publishPlugins,
|
|
1170
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1145
1171
|
analyzeCommitsConfig,
|
|
1146
1172
|
generateNotesConfig,
|
|
1147
1173
|
bumpsForThisPackage,
|
|
@@ -1274,14 +1300,17 @@ async function releaseWorkspace(options = {}) {
|
|
|
1274
1300
|
validateDependencyRanges(graph);
|
|
1275
1301
|
const order = topologicalOrder(graph);
|
|
1276
1302
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1277
|
-
const publishPlugins =
|
|
1303
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1304
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1305
|
+
packagePlugins: options.packagePlugins
|
|
1306
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1278
1307
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1279
1308
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1280
1309
|
return {
|
|
1281
1310
|
order,
|
|
1282
1311
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1283
1312
|
const result = await runPackageRelease(pkg, {
|
|
1284
|
-
publishPlugins,
|
|
1313
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1285
1314
|
analyzeCommitsConfig,
|
|
1286
1315
|
generateNotesConfig,
|
|
1287
1316
|
bumpsForThisPackage,
|
|
@@ -1441,6 +1470,7 @@ exports.packageName = packageName;
|
|
|
1441
1470
|
exports.readManifest = readManifest;
|
|
1442
1471
|
exports.releaseWorkspace = releaseWorkspace;
|
|
1443
1472
|
exports.resolvePublishPlugins = resolvePublishPlugins;
|
|
1473
|
+
exports.resolveWorkspacePublishPlugins = resolveWorkspacePublishPlugins;
|
|
1444
1474
|
exports.resumeWorkspaceRelease = resumeWorkspaceRelease;
|
|
1445
1475
|
exports.topologicalOrder = topologicalOrder;
|
|
1446
1476
|
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. */
|
|
@@ -310,4 +326,4 @@ export declare class GitCommandError extends WorkspaceReleaseError {
|
|
|
310
326
|
/** 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
327
|
export declare class WorkspaceStateError extends WorkspaceReleaseError {}
|
|
312
328
|
//#endregion
|
|
313
|
-
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackageReleaseOutcome, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
|
329
|
+
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. */
|
|
@@ -310,4 +326,4 @@ export declare class GitCommandError extends WorkspaceReleaseError {
|
|
|
310
326
|
/** 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
327
|
export declare class WorkspaceStateError extends WorkspaceReleaseError {}
|
|
312
328
|
//#endregion
|
|
313
|
-
export type { AppliedDependencyBump, CommitStrategy, DependencyBump, DependencyBumpSource, DependencyField, DependencyGraph, DependencyRangeShape, DependencyRangeUpdate, DetachedPackageRelease, PackageManifest, PackageReleaseOutcome, PublishPluginSpec, ReleaseWorkspaceOptions, ResolvedPublishPlugin, ResumeWorkspaceReleaseOptions, ScopedPlugins, Workspace, WorkspaceDependency, WorkspacePackage, WorkspaceReleaseOutcome };
|
|
329
|
+
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) {
|
|
@@ -839,7 +859,10 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
839
859
|
validateDependencyRanges(graph);
|
|
840
860
|
const order = topologicalOrder(graph);
|
|
841
861
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
842
|
-
const resolvedPlugins =
|
|
862
|
+
const resolvedPlugins = resolveWorkspacePublishPlugins(order, {
|
|
863
|
+
plugins: options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS,
|
|
864
|
+
packagePlugins: options.packagePlugins
|
|
865
|
+
}, workspace.root, {
|
|
843
866
|
requireGitPlugin: false,
|
|
844
867
|
forbidGitPlugin: true
|
|
845
868
|
});
|
|
@@ -858,7 +881,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
858
881
|
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
859
882
|
pendingBumps.delete(name);
|
|
860
883
|
const nextRelease = await analysePackage(pkg, {
|
|
861
|
-
resolvedPlugins,
|
|
884
|
+
resolvedPlugins: mustGet(resolvedPlugins, name, "publish plugins"),
|
|
862
885
|
analyzeCommitsConfig,
|
|
863
886
|
generateNotesConfig,
|
|
864
887
|
bumpsForThisPackage,
|
|
@@ -919,7 +942,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
919
942
|
log
|
|
920
943
|
};
|
|
921
944
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
922
|
-
for (const release of planned) for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
945
|
+
for (const release of planned) for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
923
946
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
924
947
|
if (plugin.verifyConditions) await plugin.verifyConditions(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
925
948
|
}
|
|
@@ -930,7 +953,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
930
953
|
await writeDependencyRange(release.pkg.manifestPath, bump.field, bump.dependency, bump.range);
|
|
931
954
|
anyRangeRewritten = true;
|
|
932
955
|
}
|
|
933
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
956
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
934
957
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
935
958
|
if (plugin.prepare) await plugin.prepare(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
936
959
|
}
|
|
@@ -950,14 +973,14 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
950
973
|
log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
|
|
951
974
|
for (const release of planned) {
|
|
952
975
|
const releases = [];
|
|
953
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
976
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
954
977
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
955
978
|
if (plugin.publish) {
|
|
956
979
|
const result = await plugin.publish(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
957
980
|
if (result !== false && result !== void 0) releases.push(result);
|
|
958
981
|
}
|
|
959
982
|
}
|
|
960
|
-
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
983
|
+
for (const [modulePath, pluginConfig] of mustGet(resolvedPlugins, release.pkg.name, "publish plugins")) {
|
|
961
984
|
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
962
985
|
if (plugin.success) await plugin.success(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
963
986
|
}
|
|
@@ -1111,12 +1134,15 @@ async function detachWorkspaceRelease(options) {
|
|
|
1111
1134
|
validateDependencyRanges(graph);
|
|
1112
1135
|
const order = topologicalOrder(graph);
|
|
1113
1136
|
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1114
|
-
const publishPlugins =
|
|
1137
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1138
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1139
|
+
packagePlugins: options.packagePlugins
|
|
1140
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1115
1141
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1116
1142
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1117
1143
|
const entries = await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1118
1144
|
const state = await runPackageDetach(pkg, {
|
|
1119
|
-
publishPlugins,
|
|
1145
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1120
1146
|
analyzeCommitsConfig,
|
|
1121
1147
|
generateNotesConfig,
|
|
1122
1148
|
bumpsForThisPackage,
|
|
@@ -1249,14 +1275,17 @@ async function releaseWorkspace(options = {}) {
|
|
|
1249
1275
|
validateDependencyRanges(graph);
|
|
1250
1276
|
const order = topologicalOrder(graph);
|
|
1251
1277
|
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1252
|
-
const publishPlugins =
|
|
1278
|
+
const publishPlugins = resolveWorkspacePublishPlugins(order, {
|
|
1279
|
+
plugins: options.plugins ?? DEFAULT_PUBLISH_PLUGINS,
|
|
1280
|
+
packagePlugins: options.packagePlugins
|
|
1281
|
+
}, workspace.root, { requireGitPlugin: !dryRun });
|
|
1253
1282
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1254
1283
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
1255
1284
|
return {
|
|
1256
1285
|
order,
|
|
1257
1286
|
packages: (await runReleaseLoop(graph, order, workspace, dryRun, log, async (pkg, bumpsForThisPackage) => {
|
|
1258
1287
|
const result = await runPackageRelease(pkg, {
|
|
1259
|
-
publishPlugins,
|
|
1288
|
+
publishPlugins: mustGet(publishPlugins, pkg.name, "publish plugins"),
|
|
1260
1289
|
analyzeCommitsConfig,
|
|
1261
1290
|
generateNotesConfig,
|
|
1262
1291
|
bumpsForThisPackage,
|
|
@@ -1398,4 +1427,4 @@ async function bumpDependents(released, version, graph, options) {
|
|
|
1398
1427
|
return applied;
|
|
1399
1428
|
}
|
|
1400
1429
|
//#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 };
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exadev/semantic-release-workspace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Independent per-package semantic-release orchestration for pnpm workspaces, without lockstep versioning.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"sideEffects": false,
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
48
|
+
"node": "^22.18 || >=24"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "turbo run _build",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@exadev/release-gate": "^1.0.0",
|
|
81
81
|
"commander": "^15.0.0",
|
|
82
|
-
"cosmiconfig": "^
|
|
82
|
+
"cosmiconfig": "^10.0.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
|
84
84
|
"validate-npm-package-name": "^8.0.0",
|
|
85
85
|
"yaml": "^2.9.0"
|