@exadev/semantic-release-workspace 3.0.2 → 3.0.3

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/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { generateNotes } from "@semantic-release/release-notes-generator";
14
14
  import semanticRelease from "semantic-release";
15
15
  import { pathToFileURL } from "node:url";
16
16
  //#region package.json
17
- var version = "3.0.2";
17
+ var version = "3.0.3";
18
18
  //#endregion
19
19
  //#region src/errors.ts
20
20
  /**
@@ -910,6 +910,26 @@ function parsePublishPluginSpec(spec) {
910
910
  const [name, config] = spec;
911
911
  return [name, config ?? {}];
912
912
  }
913
+ /**
914
+ * Releases one package, integrating the release branch first and again whenever it moves under the attempt.
915
+ *
916
+ * This exists because semantic-release cannot tell the orchestrator apart the two reasons it declines to release. When the branch has moved since the run began, its `verifyAuth` fails, `isBranchUpToDate` reports the branch behind, and it returns the same "no release" the orchestrator gets for a package that genuinely had nothing to publish. Taking that at face value is how a run used to finish green having released only the packages that happened to come before the branch moved.
917
+ *
918
+ * The ambiguity is resolved by observation rather than by reading semantic-release's logs: if the branch did not move while the package was being released, "no release" is true. If it did, the answer is not trustworthy, so the branch is integrated and the package is released again against the new tip. That is safe to repeat because a package that really has nothing to release still has nothing after a fast-forward, and a package that does release is recorded by its tag, which every later run reads as the release having happened.
919
+ *
920
+ * A package whose answer is never established fails the run. Returning "nothing to release" for it would be a guess, and the guess that loses is exactly the silent partial release this loop exists to stop.
921
+ */
922
+ async function releasePackageAcrossBranchMoves(name, branch, attempt, log) {
923
+ for (let moves = 1;; moves += 1) {
924
+ const tipBefore = await branch.integrate();
925
+ const outcome = await attempt();
926
+ if (outcome.released) return outcome;
927
+ const tipAfter = await branch.tip();
928
+ if (tipAfter === tipBefore) return outcome;
929
+ if (moves >= 3) throw new WorkspaceStateError(`${packageName}: ${name} reported no release ${String(moves)} times, each time while the release branch was moving underneath it, so whether it had anything to release was never established. Failing rather than finishing the run as though it had nothing, which would leave this package and every one after it silently unreleased. Re-running once the branch is quieter is safe: packages that already released are recorded by their tags and are not released again.`);
930
+ log(`${name}: reported no release while the branch moved from ${tipBefore} to ${tipAfter}, which is also what a stale branch looks like; integrating and releasing it again.`);
931
+ }
932
+ }
913
933
  //#endregion
914
934
  //#region src/pnpm.ts
915
935
  /**
@@ -1341,7 +1361,7 @@ async function runReleaseLoop(graph, order, workspace, dryRun, log, releaseOne)
1341
1361
  const pkg = mustGet(graph.packages, name, "package");
1342
1362
  const bumpsForThisPackage = pendingBumps.get(name) ?? [];
1343
1363
  log(`Releasing ${name} from ${pkg.relativeDirectory}${bumpsForThisPackage.length > 0 ? ` (dependency ranges already bumped: ${bumpsForThisPackage.map((bump) => bump.dependency).join(", ")})` : ""}`);
1344
- const { released, version, result } = await releasePackageAcrossBranchMoves(name, pkg, bumpsForThisPackage, workspace, dryRun, log, releaseOne);
1364
+ const { released, version, result } = dryRun ? await releaseOne(pkg, bumpsForThisPackage) : await releasePackageAcrossBranchMoves(name, branchTracker(workspace, log), async () => await releaseOne(pkg, bumpsForThisPackage), log);
1345
1365
  entries.push({
1346
1366
  name,
1347
1367
  directory: pkg.directory,
@@ -1439,12 +1459,6 @@ async function bumpDependents(released, version, graph, options) {
1439
1459
  return applied;
1440
1460
  }
1441
1461
  /**
1442
- * How many times one package may be re-released after the branch moved underneath its own attempt, before the run gives up loudly.
1443
- *
1444
- * There is no option for this and no derivation from a measured rate, unlike `pushAttempts`, because the shape of the problem is different. Integration happens at most once per package boundary, so the loop is already bounded by the size of the workspace; this only bounds the pathological case where the branch moves during the same package's own attempt over and over. Each round costs one package's analysis, and a branch would have to move during three consecutive attempts at the same package to exhaust it, at which point the run is being starved rather than raced and saying so is more useful than trying again.
1445
- */
1446
- const MAX_BRANCH_MOVES_PER_PACKAGE = 3;
1447
- /**
1448
1462
  * Brings the checkout up to the release branch's current tip, and reports where that tip is.
1449
1463
  *
1450
1464
  * At a package boundary every commit this run has made is already on the remote (each release commit by `@semantic-release/git`'s own prepare push, each tag by semantic-release's own, each dependency bump by `pushHead`), so advancing onto a moved branch is a fast-forward and loses nothing. `fastForwardTo` refuses anything else rather than discarding local work, which would mean the assumption above had broken.
@@ -1459,24 +1473,12 @@ async function integrateBranchTip(workspace, log) {
1459
1473
  log(`${packageName}: ${branch} advanced to ${tip} during this run; integrated it before continuing.`);
1460
1474
  return tip;
1461
1475
  }
1462
- /**
1463
- * Releases one package, integrating the release branch first and again whenever it moves under the attempt.
1464
- *
1465
- * This exists because semantic-release cannot tell the orchestrator the difference between the two reasons it declines to release. When the branch has moved since the run began, its `verifyAuth` fails, `isBranchUpToDate` reports the branch behind, and it returns the same "no release" the orchestrator gets for a package that genuinely had nothing to publish. Taking that at face value is how a run used to finish green having released only the packages that happened to come before the branch moved.
1466
- *
1467
- * The ambiguity is resolved by observation rather than by reading semantic-release's logs: if the branch did not move while the package was being released, "no release" is true. If it did, the answer is not trustworthy, so the branch is integrated and the package is released again against the new tip. That is safe to repeat because a package that really has nothing to release still has nothing after a fast-forward, and a package that does release is recorded by its tag, which every later run reads as the release having happened.
1468
- */
1469
- async function releasePackageAcrossBranchMoves(name, pkg, bumpsForThisPackage, workspace, dryRun, log, releaseOne) {
1470
- if (dryRun) return await releaseOne(pkg, bumpsForThisPackage);
1471
- for (let moves = 0;; moves += 1) {
1472
- const tipBefore = await integrateBranchTip(workspace, log);
1473
- const outcome = await releaseOne(pkg, bumpsForThisPackage);
1474
- if (outcome.released) return outcome;
1475
- const tipAfter = await fetchBranchTip(await currentBranch({ cwd: workspace.root }), { cwd: workspace.root });
1476
- if (tipAfter === tipBefore) return outcome;
1477
- if (moves + 1 >= MAX_BRANCH_MOVES_PER_PACKAGE) throw new WorkspaceStateError(`${packageName}: ${name} reported no release ${String(moves + 1)} times, each time while the release branch was moving underneath it, so whether it had anything to release was never established. Failing rather than finishing the run as though it had nothing, which would leave this package and every one after it silently unreleased. Re-running once the branch is quieter is safe: packages that already released are recorded by their tags and are not released again.`);
1478
- log(`${name}: reported no release while the branch moved from ${tipBefore} to ${tipAfter}, which is also what a stale branch looks like; integrating and releasing it again.`);
1479
- }
1476
+ /** The release branch as `releasePackageAcrossBranchMoves` needs to see it, backed by the real repository. */
1477
+ function branchTracker(workspace, log) {
1478
+ return {
1479
+ integrate: async () => await integrateBranchTip(workspace, log),
1480
+ tip: async () => await fetchBranchTip(await currentBranch({ cwd: workspace.root }), { cwd: workspace.root })
1481
+ };
1480
1482
  }
1481
1483
  //#endregion
1482
1484
  //#region src/gate-publish.ts
package/dist/index.cjs CHANGED
@@ -908,6 +908,26 @@ function parsePublishPluginSpec(spec) {
908
908
  const [name, config] = spec;
909
909
  return [name, config ?? {}];
910
910
  }
911
+ /**
912
+ * Releases one package, integrating the release branch first and again whenever it moves under the attempt.
913
+ *
914
+ * This exists because semantic-release cannot tell the orchestrator apart the two reasons it declines to release. When the branch has moved since the run began, its `verifyAuth` fails, `isBranchUpToDate` reports the branch behind, and it returns the same "no release" the orchestrator gets for a package that genuinely had nothing to publish. Taking that at face value is how a run used to finish green having released only the packages that happened to come before the branch moved.
915
+ *
916
+ * The ambiguity is resolved by observation rather than by reading semantic-release's logs: if the branch did not move while the package was being released, "no release" is true. If it did, the answer is not trustworthy, so the branch is integrated and the package is released again against the new tip. That is safe to repeat because a package that really has nothing to release still has nothing after a fast-forward, and a package that does release is recorded by its tag, which every later run reads as the release having happened.
917
+ *
918
+ * A package whose answer is never established fails the run. Returning "nothing to release" for it would be a guess, and the guess that loses is exactly the silent partial release this loop exists to stop.
919
+ */
920
+ async function releasePackageAcrossBranchMoves(name, branch, attempt, log) {
921
+ for (let moves = 1;; moves += 1) {
922
+ const tipBefore = await branch.integrate();
923
+ const outcome = await attempt();
924
+ if (outcome.released) return outcome;
925
+ const tipAfter = await branch.tip();
926
+ if (tipAfter === tipBefore) return outcome;
927
+ if (moves >= 3) throw new WorkspaceStateError(`${packageName}: ${name} reported no release ${String(moves)} times, each time while the release branch was moving underneath it, so whether it had anything to release was never established. Failing rather than finishing the run as though it had nothing, which would leave this package and every one after it silently unreleased. Re-running once the branch is quieter is safe: packages that already released are recorded by their tags and are not released again.`);
928
+ log(`${name}: reported no release while the branch moved from ${tipBefore} to ${tipAfter}, which is also what a stale branch looks like; integrating and releasing it again.`);
929
+ }
930
+ }
911
931
  //#endregion
912
932
  //#region src/pnpm.ts
913
933
  /**
@@ -1489,7 +1509,7 @@ async function runReleaseLoop(graph, order, workspace, dryRun, log, releaseOne)
1489
1509
  const pkg = mustGet(graph.packages, name, "package");
1490
1510
  const bumpsForThisPackage = pendingBumps.get(name) ?? [];
1491
1511
  log(`Releasing ${name} from ${pkg.relativeDirectory}${bumpsForThisPackage.length > 0 ? ` (dependency ranges already bumped: ${bumpsForThisPackage.map((bump) => bump.dependency).join(", ")})` : ""}`);
1492
- const { released, version, result } = await releasePackageAcrossBranchMoves(name, pkg, bumpsForThisPackage, workspace, dryRun, log, releaseOne);
1512
+ const { released, version, result } = dryRun ? await releaseOne(pkg, bumpsForThisPackage) : await releasePackageAcrossBranchMoves(name, branchTracker(workspace, log), async () => await releaseOne(pkg, bumpsForThisPackage), log);
1493
1513
  entries.push({
1494
1514
  name,
1495
1515
  directory: pkg.directory,
@@ -1587,12 +1607,6 @@ async function bumpDependents(released, version, graph, options) {
1587
1607
  return applied;
1588
1608
  }
1589
1609
  /**
1590
- * How many times one package may be re-released after the branch moved underneath its own attempt, before the run gives up loudly.
1591
- *
1592
- * There is no option for this and no derivation from a measured rate, unlike `pushAttempts`, because the shape of the problem is different. Integration happens at most once per package boundary, so the loop is already bounded by the size of the workspace; this only bounds the pathological case where the branch moves during the same package's own attempt over and over. Each round costs one package's analysis, and a branch would have to move during three consecutive attempts at the same package to exhaust it, at which point the run is being starved rather than raced and saying so is more useful than trying again.
1593
- */
1594
- const MAX_BRANCH_MOVES_PER_PACKAGE = 3;
1595
- /**
1596
1610
  * Brings the checkout up to the release branch's current tip, and reports where that tip is.
1597
1611
  *
1598
1612
  * At a package boundary every commit this run has made is already on the remote (each release commit by `@semantic-release/git`'s own prepare push, each tag by semantic-release's own, each dependency bump by `pushHead`), so advancing onto a moved branch is a fast-forward and loses nothing. `fastForwardTo` refuses anything else rather than discarding local work, which would mean the assumption above had broken.
@@ -1607,24 +1621,12 @@ async function integrateBranchTip(workspace, log) {
1607
1621
  log(`${packageName}: ${branch} advanced to ${tip} during this run; integrated it before continuing.`);
1608
1622
  return tip;
1609
1623
  }
1610
- /**
1611
- * Releases one package, integrating the release branch first and again whenever it moves under the attempt.
1612
- *
1613
- * This exists because semantic-release cannot tell the orchestrator the difference between the two reasons it declines to release. When the branch has moved since the run began, its `verifyAuth` fails, `isBranchUpToDate` reports the branch behind, and it returns the same "no release" the orchestrator gets for a package that genuinely had nothing to publish. Taking that at face value is how a run used to finish green having released only the packages that happened to come before the branch moved.
1614
- *
1615
- * The ambiguity is resolved by observation rather than by reading semantic-release's logs: if the branch did not move while the package was being released, "no release" is true. If it did, the answer is not trustworthy, so the branch is integrated and the package is released again against the new tip. That is safe to repeat because a package that really has nothing to release still has nothing after a fast-forward, and a package that does release is recorded by its tag, which every later run reads as the release having happened.
1616
- */
1617
- async function releasePackageAcrossBranchMoves(name, pkg, bumpsForThisPackage, workspace, dryRun, log, releaseOne) {
1618
- if (dryRun) return await releaseOne(pkg, bumpsForThisPackage);
1619
- for (let moves = 0;; moves += 1) {
1620
- const tipBefore = await integrateBranchTip(workspace, log);
1621
- const outcome = await releaseOne(pkg, bumpsForThisPackage);
1622
- if (outcome.released) return outcome;
1623
- const tipAfter = await fetchBranchTip(await currentBranch({ cwd: workspace.root }), { cwd: workspace.root });
1624
- if (tipAfter === tipBefore) return outcome;
1625
- if (moves + 1 >= MAX_BRANCH_MOVES_PER_PACKAGE) throw new WorkspaceStateError(`${packageName}: ${name} reported no release ${String(moves + 1)} times, each time while the release branch was moving underneath it, so whether it had anything to release was never established. Failing rather than finishing the run as though it had nothing, which would leave this package and every one after it silently unreleased. Re-running once the branch is quieter is safe: packages that already released are recorded by their tags and are not released again.`);
1626
- log(`${name}: reported no release while the branch moved from ${tipBefore} to ${tipAfter}, which is also what a stale branch looks like; integrating and releasing it again.`);
1627
- }
1624
+ /** The release branch as `releasePackageAcrossBranchMoves` needs to see it, backed by the real repository. */
1625
+ function branchTracker(workspace, log) {
1626
+ return {
1627
+ integrate: async () => await integrateBranchTip(workspace, log),
1628
+ tip: async () => await fetchBranchTip(await currentBranch({ cwd: workspace.root }), { cwd: workspace.root })
1629
+ };
1628
1630
  }
1629
1631
  //#endregion
1630
1632
  exports.DEFAULT_PUBLISH_PLUGINS = DEFAULT_PUBLISH_PLUGINS;
package/dist/index.js CHANGED
@@ -883,6 +883,26 @@ function parsePublishPluginSpec(spec) {
883
883
  const [name, config] = spec;
884
884
  return [name, config ?? {}];
885
885
  }
886
+ /**
887
+ * Releases one package, integrating the release branch first and again whenever it moves under the attempt.
888
+ *
889
+ * This exists because semantic-release cannot tell the orchestrator apart the two reasons it declines to release. When the branch has moved since the run began, its `verifyAuth` fails, `isBranchUpToDate` reports the branch behind, and it returns the same "no release" the orchestrator gets for a package that genuinely had nothing to publish. Taking that at face value is how a run used to finish green having released only the packages that happened to come before the branch moved.
890
+ *
891
+ * The ambiguity is resolved by observation rather than by reading semantic-release's logs: if the branch did not move while the package was being released, "no release" is true. If it did, the answer is not trustworthy, so the branch is integrated and the package is released again against the new tip. That is safe to repeat because a package that really has nothing to release still has nothing after a fast-forward, and a package that does release is recorded by its tag, which every later run reads as the release having happened.
892
+ *
893
+ * A package whose answer is never established fails the run. Returning "nothing to release" for it would be a guess, and the guess that loses is exactly the silent partial release this loop exists to stop.
894
+ */
895
+ async function releasePackageAcrossBranchMoves(name, branch, attempt, log) {
896
+ for (let moves = 1;; moves += 1) {
897
+ const tipBefore = await branch.integrate();
898
+ const outcome = await attempt();
899
+ if (outcome.released) return outcome;
900
+ const tipAfter = await branch.tip();
901
+ if (tipAfter === tipBefore) return outcome;
902
+ if (moves >= 3) throw new WorkspaceStateError(`${packageName}: ${name} reported no release ${String(moves)} times, each time while the release branch was moving underneath it, so whether it had anything to release was never established. Failing rather than finishing the run as though it had nothing, which would leave this package and every one after it silently unreleased. Re-running once the branch is quieter is safe: packages that already released are recorded by their tags and are not released again.`);
903
+ log(`${name}: reported no release while the branch moved from ${tipBefore} to ${tipAfter}, which is also what a stale branch looks like; integrating and releasing it again.`);
904
+ }
905
+ }
886
906
  //#endregion
887
907
  //#region src/pnpm.ts
888
908
  /**
@@ -1464,7 +1484,7 @@ async function runReleaseLoop(graph, order, workspace, dryRun, log, releaseOne)
1464
1484
  const pkg = mustGet(graph.packages, name, "package");
1465
1485
  const bumpsForThisPackage = pendingBumps.get(name) ?? [];
1466
1486
  log(`Releasing ${name} from ${pkg.relativeDirectory}${bumpsForThisPackage.length > 0 ? ` (dependency ranges already bumped: ${bumpsForThisPackage.map((bump) => bump.dependency).join(", ")})` : ""}`);
1467
- const { released, version, result } = await releasePackageAcrossBranchMoves(name, pkg, bumpsForThisPackage, workspace, dryRun, log, releaseOne);
1487
+ const { released, version, result } = dryRun ? await releaseOne(pkg, bumpsForThisPackage) : await releasePackageAcrossBranchMoves(name, branchTracker(workspace, log), async () => await releaseOne(pkg, bumpsForThisPackage), log);
1468
1488
  entries.push({
1469
1489
  name,
1470
1490
  directory: pkg.directory,
@@ -1562,12 +1582,6 @@ async function bumpDependents(released, version, graph, options) {
1562
1582
  return applied;
1563
1583
  }
1564
1584
  /**
1565
- * How many times one package may be re-released after the branch moved underneath its own attempt, before the run gives up loudly.
1566
- *
1567
- * There is no option for this and no derivation from a measured rate, unlike `pushAttempts`, because the shape of the problem is different. Integration happens at most once per package boundary, so the loop is already bounded by the size of the workspace; this only bounds the pathological case where the branch moves during the same package's own attempt over and over. Each round costs one package's analysis, and a branch would have to move during three consecutive attempts at the same package to exhaust it, at which point the run is being starved rather than raced and saying so is more useful than trying again.
1568
- */
1569
- const MAX_BRANCH_MOVES_PER_PACKAGE = 3;
1570
- /**
1571
1585
  * Brings the checkout up to the release branch's current tip, and reports where that tip is.
1572
1586
  *
1573
1587
  * At a package boundary every commit this run has made is already on the remote (each release commit by `@semantic-release/git`'s own prepare push, each tag by semantic-release's own, each dependency bump by `pushHead`), so advancing onto a moved branch is a fast-forward and loses nothing. `fastForwardTo` refuses anything else rather than discarding local work, which would mean the assumption above had broken.
@@ -1582,24 +1596,12 @@ async function integrateBranchTip(workspace, log) {
1582
1596
  log(`${packageName}: ${branch} advanced to ${tip} during this run; integrated it before continuing.`);
1583
1597
  return tip;
1584
1598
  }
1585
- /**
1586
- * Releases one package, integrating the release branch first and again whenever it moves under the attempt.
1587
- *
1588
- * This exists because semantic-release cannot tell the orchestrator the difference between the two reasons it declines to release. When the branch has moved since the run began, its `verifyAuth` fails, `isBranchUpToDate` reports the branch behind, and it returns the same "no release" the orchestrator gets for a package that genuinely had nothing to publish. Taking that at face value is how a run used to finish green having released only the packages that happened to come before the branch moved.
1589
- *
1590
- * The ambiguity is resolved by observation rather than by reading semantic-release's logs: if the branch did not move while the package was being released, "no release" is true. If it did, the answer is not trustworthy, so the branch is integrated and the package is released again against the new tip. That is safe to repeat because a package that really has nothing to release still has nothing after a fast-forward, and a package that does release is recorded by its tag, which every later run reads as the release having happened.
1591
- */
1592
- async function releasePackageAcrossBranchMoves(name, pkg, bumpsForThisPackage, workspace, dryRun, log, releaseOne) {
1593
- if (dryRun) return await releaseOne(pkg, bumpsForThisPackage);
1594
- for (let moves = 0;; moves += 1) {
1595
- const tipBefore = await integrateBranchTip(workspace, log);
1596
- const outcome = await releaseOne(pkg, bumpsForThisPackage);
1597
- if (outcome.released) return outcome;
1598
- const tipAfter = await fetchBranchTip(await currentBranch({ cwd: workspace.root }), { cwd: workspace.root });
1599
- if (tipAfter === tipBefore) return outcome;
1600
- if (moves + 1 >= MAX_BRANCH_MOVES_PER_PACKAGE) throw new WorkspaceStateError(`${packageName}: ${name} reported no release ${String(moves + 1)} times, each time while the release branch was moving underneath it, so whether it had anything to release was never established. Failing rather than finishing the run as though it had nothing, which would leave this package and every one after it silently unreleased. Re-running once the branch is quieter is safe: packages that already released are recorded by their tags and are not released again.`);
1601
- log(`${name}: reported no release while the branch moved from ${tipBefore} to ${tipAfter}, which is also what a stale branch looks like; integrating and releasing it again.`);
1602
- }
1599
+ /** The release branch as `releasePackageAcrossBranchMoves` needs to see it, backed by the real repository. */
1600
+ function branchTracker(workspace, log) {
1601
+ return {
1602
+ integrate: async () => await integrateBranchTip(workspace, log),
1603
+ tip: async () => await fetchBranchTip(await currentBranch({ cwd: workspace.root }), { cwd: workspace.root })
1604
+ };
1603
1605
  }
1604
1606
  //#endregion
1605
1607
  export { DEFAULT_PUBLISH_PLUGINS, DependencyCycleError, GitCommandError, ReleaseConfigurationError, SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, UnsupportedDependencyRangeError, WorkspaceDiscoveryError, WorkspaceReleaseError, WorkspaceStateError, buildDependencyGraph, classifyDependencyRange, createScopedPlugins, discoverWorkspace, filterCommitsToDirectory, orderedPackages, packageName, readManifest, releaseWorkspace, resolvePublishPlugins, resolveWorkspacePublishPlugins, resumeWorkspaceRelease, topologicalOrder, updateDependencyRange, writeDependencyRange };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exadev/semantic-release-workspace",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "Independent per-package semantic-release orchestration for pnpm workspaces, without lockstep versioning.",
5
5
  "type": "module",
6
6
  "repository": {