@pnpm/releasing.versioning 1100.2.6 → 1100.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @pnpm/releasing.versioning
2
2
 
3
+ ## 1100.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added `pnpm change check`. It validates the committed package versions against the `versioning.epics` bands and the `versioning.fixed` groups in `pnpm-workspace.yaml` and lists every violation. It is meant to run in CI, because `pnpm version -r` only checks the packages it releases.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies:
12
+ - @pnpm/error@1100.1.4
13
+ - @pnpm/workspace.project-manifest-reader@1100.0.27
14
+
15
+ ## 1100.2.7
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies:
20
+ - @pnpm/types@1102.1.0
21
+ - @pnpm/workspace.project-manifest-reader@1100.0.26
22
+
3
23
  ## 1100.2.6
4
24
 
5
25
  ### Patch Changes
@@ -90,6 +90,28 @@ export declare function indexProjectRefs(projects: ReadonlyArray<{
90
90
  /** The workspace-relative directory of a project, in canonical spelling. */
91
91
  export declare function toProjectDir(workspaceDir: string, rootDir: string): string;
92
92
  export declare function assembleReleasePlan(opts: AssembleReleasePlanOptions): ReleasePlan;
93
+ export interface VersioningInvariantViolation {
94
+ code: 'VERSIONING_EPIC_OUT_OF_BAND' | 'VERSIONING_FIXED_GROUP_MISMATCH';
95
+ message: string;
96
+ }
97
+ export interface CheckVersioningInvariantsOptions {
98
+ workspaceDir: string;
99
+ projects: WorkspaceProject[];
100
+ versioning?: VersioningSettings;
101
+ }
102
+ /**
103
+ * Validates that the committed versions already satisfy the invariants the
104
+ * configuration declares: every epic member's major sits inside its lead's
105
+ * band, and every fixed group shares one version. This is the static
106
+ * counterpart to the release-time enforcement in `assembleReleasePlan`, which
107
+ * only checks packages a plan actually releases — so a committed manifest that
108
+ * drifted out of band, or a fixed group that fell out of lockstep, would
109
+ * otherwise go unnoticed until a release that happens to touch it. Returns
110
+ * every violation so a caller can report them all at once; malformed
111
+ * configuration (unknown lead, epic overlap, a group straddling an epic) still
112
+ * throws, exactly as plan assembly would.
113
+ */
114
+ export declare function checkVersioningInvariants(opts: CheckVersioningInvariantsOptions): VersioningInvariantViolation[];
93
115
  /**
94
116
  * The range that pnpm materializes for a workspace: spec at pack time, given
95
117
  * the dependency's version at the dependent's previous release. Dependent
@@ -72,6 +72,55 @@ export function assembleReleasePlan(opts) {
72
72
  selection = expanded;
73
73
  }
74
74
  }
75
+ /**
76
+ * Validates that the committed versions already satisfy the invariants the
77
+ * configuration declares: every epic member's major sits inside its lead's
78
+ * band, and every fixed group shares one version. This is the static
79
+ * counterpart to the release-time enforcement in `assembleReleasePlan`, which
80
+ * only checks packages a plan actually releases — so a committed manifest that
81
+ * drifted out of band, or a fixed group that fell out of lockstep, would
82
+ * otherwise go unnoticed until a release that happens to touch it. Returns
83
+ * every violation so a caller can report them all at once; malformed
84
+ * configuration (unknown lead, epic overlap, a group straddling an epic) still
85
+ * throws, exactly as plan assembly would.
86
+ */
87
+ export function checkVersioningInvariants(opts) {
88
+ const refs = indexProjectRefs(opts.projects, opts.workspaceDir);
89
+ const participants = collectParticipants(opts.projects, refs, opts);
90
+ const lanesByDir = resolveLanes(refs, participants, opts.versioning);
91
+ const fixedGroups = resolveFixedGroups(refs, participants, opts.versioning);
92
+ validateFixedGroupLanes(fixedGroups, lanesByDir, opts.versioning);
93
+ const epics = resolveEpics(refs, participants, opts.versioning);
94
+ validateEpics(epics, fixedGroups);
95
+ // With no plan (no new versions), the band derives from the lead's current
96
+ // major, and members are checked against their current versions.
97
+ const noNewVersions = new Map();
98
+ const violations = [];
99
+ for (const epic of epics) {
100
+ const band = epicBand(epic, participants, noNewVersions);
101
+ for (const memberDir of [...epic.memberDirs].sort()) {
102
+ const member = participants.get(memberDir);
103
+ const memberMajor = Number(member.currentVersion.split('.')[0]);
104
+ if (!band.contains(memberMajor)) {
105
+ violations.push({
106
+ code: 'VERSIONING_EPIC_OUT_OF_BAND',
107
+ message: `${member.name} is at ${member.currentVersion}, whose major ${memberMajor} is outside the band ${band.low}-${band.high} of the epic led by "${epic.leadRef}" (major ${band.major}).`,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ for (const [index, group] of fixedGroups.entries()) {
113
+ const members = group.map((dir) => participants.get(dir));
114
+ if (new Set(members.map((member) => member.currentVersion)).size > 1) {
115
+ const detail = members.map((member) => `${member.name}@${member.currentVersion}`).join(', ');
116
+ violations.push({
117
+ code: 'VERSIONING_FIXED_GROUP_MISMATCH',
118
+ message: `The fixed group [${(opts.versioning?.fixed ?? [])[index].join(', ')}] is not in lockstep: ${detail}.`,
119
+ });
120
+ }
121
+ }
122
+ return violations;
123
+ }
75
124
  function assemble(ctx, selection) {
76
125
  const { participants, lanesByDir, fixedGroups, epics, opts } = ctx;
77
126
  const pendingByDir = collectPendingIntents(ctx);
@@ -603,9 +652,13 @@ function epicRebaseFloor(epic, participants, newVersions) {
603
652
  if (lead == null || newLeadVersion == null || parsePrerelease(newLeadVersion) != null)
604
653
  return null;
605
654
  const newMajor = Number(newLeadVersion.split('.')[0]);
606
- const currentMajor = Number(lead.currentVersion.split('.')[0]);
655
+ const currentMajor = epicLeadBandMajor(lead.currentVersion);
607
656
  return newMajor > currentMajor ? newMajor * 100 : null;
608
657
  }
658
+ function epicLeadBandMajor(version) {
659
+ const [major, minor, patch] = stablePart(version).split('.').map(Number);
660
+ return parsePrerelease(version) != null && minor === 0 && patch === 0 ? Math.max(0, major - 1) : major;
661
+ }
609
662
  /**
610
663
  * Overrides the computed version of every bumped epic member with the band
611
664
  * floor when its lead crosses to a new stable major. A member on a lane
@@ -627,15 +680,12 @@ function applyEpicBandVersions({ participants, state, newVersions, epics, lanesB
627
680
  }
628
681
  }
629
682
  }
630
- /**
631
- * The band of member majors an epic permits: `[leadMajor×100, leadMajor×100+99]`,
632
- * where `leadMajor` is the major the plan establishes for the lead — its
633
- * re-based major when the lead crosses to a new stable major, otherwise the
634
- * lead's current major (a prerelease lead does not open the next band).
635
- */
636
- function epicBandMajor(epic, participants, newVersions) {
683
+ function epicBand(epic, participants, newVersions) {
637
684
  const floor = epicRebaseFloor(epic, participants, newVersions);
638
- return floor != null ? floor / 100 : Number(participants.get(epic.leadDir).currentVersion.split('.')[0]);
685
+ const major = floor != null ? floor / 100 : epicLeadBandMajor(participants.get(epic.leadDir).currentVersion);
686
+ const low = major * 100;
687
+ const high = low + 99;
688
+ return { major, low, high, contains: (memberMajor) => memberMajor >= low && memberMajor <= high };
639
689
  }
640
690
  /**
641
691
  * Enforces that every released member's new major stays inside its epic's band.
@@ -646,17 +696,15 @@ function epicBandMajor(epic, participants, newVersions) {
646
696
  */
647
697
  function enforceEpicBands(epics, participants, newVersions) {
648
698
  for (const epic of epics) {
649
- const bandMajor = epicBandMajor(epic, participants, newVersions);
650
- const low = bandMajor * 100;
651
- const high = low + 99;
699
+ const band = epicBand(epic, participants, newVersions);
652
700
  for (const memberDir of epic.memberDirs) {
653
701
  const memberVersion = newVersions.get(memberDir);
654
702
  if (memberVersion == null)
655
703
  continue;
656
704
  const memberMajor = Number(memberVersion.split('.')[0]);
657
- if (memberMajor < low || memberMajor > high) {
658
- throw new PnpmError('VERSIONING_EPIC_OUT_OF_BAND', `The release plan takes ${participants.get(memberDir).name} to ${memberVersion}, whose major ${memberMajor} is outside the band ${low}-${high} of the epic led by "${epic.leadRef}" (major ${bandMajor}). ` +
659
- (memberMajor > high
705
+ if (!band.contains(memberMajor)) {
706
+ throw new PnpmError('VERSIONING_EPIC_OUT_OF_BAND', `The release plan takes ${participants.get(memberDir).name} to ${memberVersion}, whose major ${memberMajor} is outside the band ${band.low}-${band.high} of the epic led by "${epic.leadRef}" (major ${band.major}). ` +
707
+ (memberMajor > band.high
660
708
  ? 'The band is exhausted - the lead must advance to a new major to open the next band.'
661
709
  : 'Re-base the member into the band, or remove it from the epic.'));
662
710
  }
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { type AppliedRelease, applyReleasePlan, type ApplyReleasePlanOptions, changelogStorage, } from './applyReleasePlan.js';
2
- export { assembleReleasePlan, type AssembleReleasePlanOptions, type DependencyUpdate, indexProjectRefs, isDirRef, materializeWorkspaceRange, type PlannedRelease, type ProjectRefIndex, type ReleaseCause, type ReleasePlan, toProjectDir, type WorkspaceProject, } from './assembleReleasePlan.js';
2
+ export { assembleReleasePlan, type AssembleReleasePlanOptions, checkVersioningInvariants, type CheckVersioningInvariantsOptions, type DependencyUpdate, indexProjectRefs, isDirRef, materializeWorkspaceRange, type PlannedRelease, type ProjectRefIndex, type ReleaseCause, type ReleasePlan, toProjectDir, type VersioningInvariantViolation, type WorkspaceProject, } from './assembleReleasePlan.js';
3
3
  export { composeChangelogSection, prependChangelogSection, renderChangelog, } from './changelog.js';
4
4
  export { BUMP_TYPES, type ChangeIntent, CHANGES_DIR, type IntentBumpType, parseChangeIntent, readChangeIntents, type ReleaseBumpType, writeChangeIntent, type WriteChangeIntentOptions, } from './intents.js';
5
5
  export { appendToLedger, buildConsumptionIndex, type Ledger, LEDGER_FILENAME, type LedgerEntry, ledgerEntryIds, normalizeProjectDir, type PackageConsumption, readLedger, } from './ledger.js';
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { applyReleasePlan, changelogStorage, } from './applyReleasePlan.js';
2
- export { assembleReleasePlan, indexProjectRefs, isDirRef, materializeWorkspaceRange, toProjectDir, } from './assembleReleasePlan.js';
2
+ export { assembleReleasePlan, checkVersioningInvariants, indexProjectRefs, isDirRef, materializeWorkspaceRange, toProjectDir, } from './assembleReleasePlan.js';
3
3
  export { composeChangelogSection, prependChangelogSection, renderChangelog, } from './changelog.js';
4
4
  export { BUMP_TYPES, CHANGES_DIR, parseChangeIntent, readChangeIntents, writeChangeIntent, } from './intents.js';
5
5
  export { appendToLedger, buildConsumptionIndex, LEDGER_FILENAME, ledgerEntryIds, normalizeProjectDir, readLedger, } from './ledger.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/releasing.versioning",
3
- "version": "1100.2.6",
3
+ "version": "1100.3.0",
4
4
  "description": "Native workspace release management: change intents, release-plan assembly, and changelog writing",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -27,9 +27,9 @@
27
27
  "!*.map"
28
28
  ],
29
29
  "dependencies": {
30
- "@pnpm/error": "1100.1.3",
31
- "@pnpm/types": "1102.0.0",
32
- "@pnpm/workspace.project-manifest-reader": "1100.0.25",
30
+ "@pnpm/error": "1100.1.4",
31
+ "@pnpm/types": "1102.1.0",
32
+ "@pnpm/workspace.project-manifest-reader": "1100.0.27",
33
33
  "@pnpm/workspace.spec-parser": "1100.0.1",
34
34
  "human-id": "^4.2.1",
35
35
  "semver": "^7.8.5",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@jest/globals": "30.4.1",
40
- "@pnpm/releasing.versioning": "1100.2.6",
40
+ "@pnpm/releasing.versioning": "1100.3.0",
41
41
  "@types/semver": "7.8.0",
42
42
  "tempy": "3.0.0"
43
43
  },