@exadev/semantic-release-workspace 2.0.0 → 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 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`, `.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`) |
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
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.0.0";
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 = resolvePublishPlugins(options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, workspace.root, {
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 = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
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 = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
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,6 +1501,7 @@ 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,
@@ -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,
@@ -1549,10 +1581,11 @@ async function readReleaseConfigFile(path) {
1549
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 @@ async 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 parseConfigFilePlugin(spec, path) {
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 "plugins" entry must be a module name or a [name, config] array`);
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 = resolvePublishPlugins(options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, workspace.root, {
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 = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
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 = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
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 = resolvePublishPlugins(options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, workspace.root, {
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 = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
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 = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
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": "2.0.0",
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": {