@pnpm/releasing.versioning 1100.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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # @pnpm/releasing.versioning
2
+
3
+ ## 1100.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added `versioning.epics` to `pnpm-workspace.yaml`. An epic ties a group of member packages to a lead package, constraining every member's major version to a band derived from the lead's major: while the lead is on major `M`, members live in `M*100 … M*100+99`. Members move independently inside the band (patch, minor, and a `major` intent that stays in-band); a bump that would carry a member past the band ceiling is rejected until the lead advances its own major. When a release plan takes the lead to a new stable major, every member re-bases to the band floor in the same plan. Membership is matched with pnpm's package selectors — name globs, `./`-prefixed directory globs, and `!`-prefixed negations.
8
+
9
+ - Added native workspace release management [#12952](https://github.com/pnpm/pnpm/issues/12952): the new `pnpm change` command records change intents as changesets-compatible `.changeset/*.md` files (`pnpm change status` shows the pending release plan), and the bare `pnpm version -r` consumes them — bumping versions across the workspace with dependent propagation through `workspace:` ranges, fixed groups, a `maxBump` cap, `--filter` narrowing, and `--dry-run` — writing changelogs, and recording consumed intents in a committed ledger that keeps cherry-picks and merge-backs between release branches safe. Packages can be moved onto per-package release lanes with the new `pnpm lane <name> --filter <pkg>` command and back with `pnpm lane main --filter <pkg>` (`pnpm lane` shows the membership), releasing `X.Y.Z-lane.N` prereleases from the same runs that release stable versions of the packages on the main lane. Configuration lives under the new `versioning` key of `pnpm-workspace.yaml` (`fixed`, `ignore`, `maxBump`, `lanes`, `changelog`). When two workspace projects publish the same name, intent files, `versioning.lanes`, and `versioning.fixed`/`ignore` may reference a project by its workspace-relative directory path (e.g. `"./pnpm/npm/pnpm"`) — the one additive extension to the changesets format, applied automatically by `pnpm change`.
10
+
11
+ Release changelogs default to `registry` storage (`versioning.changelog.storage`): no `CHANGELOG.md` is committed. Each release's section is composed at publish time and packed into the published tarball on top of the previously published version's changelog, and the consumed change intents are garbage-collected by a later `pnpm version -r` only once the registry confirms the version is published with its section. Set `versioning.changelog.storage: repository` to keep committed `CHANGELOG.md` files instead.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies:
16
+ - @pnpm/types@1101.4.0
17
+ - @pnpm/workspace.project-manifest-reader@1100.0.15
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2016 Rico Sta. Cruz and other contributors
4
+ Copyright (c) 2016-2026 Zoltan Kochan and other contributors
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @pnpm/releasing.versioning
2
+
3
+ > Native workspace release management: change intents, release-plan assembly, and changelog writing
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@pnpm/releasing.versioning.svg)](https://npmx.dev/package/@pnpm/releasing.versioning)
6
+
7
+ Implements the engine behind `pnpm change` and the bare `pnpm version -r`:
8
+ reading and writing changesets-compatible change-intent files from
9
+ `.changeset/*.md`, assembling a release plan (direct bumps, dependent
10
+ propagation through materialized `workspace:` ranges, fixed groups,
11
+ per-package release lanes), and applying it (manifest version updates,
12
+ changelog composition, the consumed-intents ledger, and intent-file cleanup).
13
+
14
+ See the [native monorepo versioning RFC](https://github.com/pnpm/rfcs/pull/18).
15
+
16
+ ## Installation
17
+
18
+ ```sh
19
+ pnpm add @pnpm/releasing.versioning
20
+ ```
21
+
22
+ ## License
23
+
24
+ MIT
@@ -0,0 +1,35 @@
1
+ import type { VersioningChangelogStorage, VersioningSettings } from '@pnpm/types';
2
+ import { type ReleasePlan, type WorkspaceProject } from './assembleReleasePlan.js';
3
+ import type { ChangeIntent } from './intents.js';
4
+ /**
5
+ * Registry storage is the default: no CHANGELOG.md is committed; a release's
6
+ * section is parked (see {@link writePendingChangelog}) and packed into the
7
+ * published tarball. `repository` opts back into committed changelogs.
8
+ */
9
+ export declare function changelogStorage(versioning?: VersioningSettings): VersioningChangelogStorage;
10
+ export interface ApplyReleasePlanOptions {
11
+ workspaceDir: string;
12
+ projects: WorkspaceProject[];
13
+ /**
14
+ * All intent files currently in the workspace, used to decide which files
15
+ * are fully consumed after this run and can be deleted.
16
+ */
17
+ allIntents: ChangeIntent[];
18
+ versioning?: VersioningSettings;
19
+ /**
20
+ * In `registry` changelog storage, the gate that lets a ledgered release's
21
+ * intents be garbage-collected: it must resolve `true` only once the
22
+ * registry has published `${name}@${version}` and that tarball's
23
+ * CHANGELOG.md already contains `section`. Without it, registry-mode
24
+ * releases keep their intents (the repository is still their only prose).
25
+ * Ignored in `repository` storage, where the committed changelog already
26
+ * holds the prose and the ledger alone gates deletion.
27
+ */
28
+ verifyPublished?: (name: string, version: string, section: string) => Promise<boolean>;
29
+ }
30
+ export interface AppliedRelease {
31
+ name: string;
32
+ currentVersion: string;
33
+ newVersion: string;
34
+ }
35
+ export declare function applyReleasePlan(plan: ReleasePlan, opts: ApplyReleasePlanOptions): Promise<AppliedRelease[]>;
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs/promises';
2
+ import { readProjectManifest } from '@pnpm/workspace.project-manifest-reader';
3
+ import { indexProjectRefs } from './assembleReleasePlan.js';
4
+ import { composeChangelogSection, prependChangelogSection } from './changelog.js';
5
+ import { appendToLedger, buildConsumptionIndex } from './ledger.js';
6
+ import { listPendingChangelogs, readPendingChangelog, removePendingChangelog, writePendingChangelog } from './pendingChangelog.js';
7
+ /**
8
+ * Registry storage is the default: no CHANGELOG.md is committed; a release's
9
+ * section is parked (see {@link writePendingChangelog}) and packed into the
10
+ * published tarball. `repository` opts back into committed changelogs.
11
+ */
12
+ export function changelogStorage(versioning) {
13
+ return versioning?.changelog?.storage ?? 'registry';
14
+ }
15
+ export async function applyReleasePlan(plan, opts) {
16
+ const storage = changelogStorage(opts.versioning);
17
+ const applied = await Promise.all(plan.releases.map(async (release) => {
18
+ const { manifest, writeProjectManifest } = await readProjectManifest(release.rootDir);
19
+ manifest.version = release.newVersion;
20
+ await writeProjectManifest(manifest);
21
+ return {
22
+ name: release.name,
23
+ currentVersion: release.currentVersion,
24
+ newVersion: release.newVersion,
25
+ };
26
+ }));
27
+ // In `repository` storage the section is committed to CHANGELOG.md now. In
28
+ // `registry` storage nothing is committed to the package; the section is
29
+ // parked until publish, when it is packed into the tarball.
30
+ await Promise.all(plan.releases.map(async (release) => {
31
+ const section = composeChangelogSection(release);
32
+ if (storage === 'repository') {
33
+ await prependChangelogSection(release.rootDir, release.name, section);
34
+ }
35
+ else {
36
+ await writePendingChangelog(opts.workspaceDir, release.name, release.newVersion, section);
37
+ }
38
+ }));
39
+ const newEntries = {};
40
+ for (const release of plan.releases) {
41
+ if (release.intents.length === 0)
42
+ continue;
43
+ newEntries[`${release.name}@${release.newVersion}`] = {
44
+ dir: release.dir,
45
+ intents: release.intents.map((intent) => intent.id).sort(),
46
+ };
47
+ }
48
+ const ledger = await appendToLedger(opts.workspaceDir, newEntries);
49
+ // An intent file is deletable once every project it names has a consumed
50
+ // ledger entry for it, with one exemption: while a project is still on a
51
+ // lane, entries against prerelease versions alone keep the file alive — its
52
+ // prose is still needed to compose the stable changelog section at
53
+ // graduation. Declined (`none`) entries demand no release and never block
54
+ // deletion. References here were already validated by the plan assembly,
55
+ // so an unresolvable one just keeps its file around.
56
+ //
57
+ // In `registry` storage the ledger alone does not authorize deletion: the
58
+ // repository is the only copy of the prose until the release is published,
59
+ // so an entry counts as consumed only once the registry confirms its version
60
+ // carries the composed section.
61
+ const refs = indexProjectRefs(opts.projects, opts.workspaceDir);
62
+ const consumedLedger = storage === 'repository'
63
+ ? ledger
64
+ : await confirmPublished(ledger, opts);
65
+ const consumptionOf = buildConsumptionIndex(consumedLedger, refs.nameToDirs);
66
+ const laneDirs = new Set();
67
+ for (const ref of Object.keys(opts.versioning?.lanes ?? {})) {
68
+ for (const dir of refs.refToDirs(ref)) {
69
+ laneDirs.add(dir);
70
+ }
71
+ }
72
+ const deletable = opts.allIntents.filter((intent) => Object.entries(intent.releases).every(([ref, bumpType]) => {
73
+ if (bumpType === 'none')
74
+ return true;
75
+ const dirs = refs.refToDirs(ref);
76
+ if (dirs.length !== 1)
77
+ return false;
78
+ const consumption = consumptionOf(dirs[0]);
79
+ return consumption.allIds.has(intent.id) &&
80
+ !(laneDirs.has(dirs[0]) && consumption.prereleaseOnlyIds.has(intent.id));
81
+ }));
82
+ await Promise.all(deletable.map(async (intent) => fs.rm(intent.filePath)));
83
+ return applied;
84
+ }
85
+ /**
86
+ * Confirms the parked sections whose releases the registry reports published
87
+ * with that section, deletes those section files (their prose now lives in the
88
+ * published tarball), and returns the subset of `ledger` those confirmations
89
+ * cover — the consumed ledger that gates intent deletion. Driven by the parked
90
+ * files rather than the ledger, so a dependency-propagated release (which has a
91
+ * section but no consumed intents, hence no ledger entry) still gets its
92
+ * section collected. Every parked file belongs to an as-yet-unpublished
93
+ * release, so the confirmation cost is bounded by the release backlog, not by
94
+ * history.
95
+ */
96
+ async function confirmPublished(ledger, opts) {
97
+ const { verifyPublished } = opts;
98
+ const confirmed = Object.create(null);
99
+ if (verifyPublished == null)
100
+ return confirmed;
101
+ const pending = await listPendingChangelogs(opts.workspaceDir);
102
+ await Promise.all(pending.map(async ({ name, version }) => {
103
+ const section = await readPendingChangelog(opts.workspaceDir, name, version);
104
+ if (section == null)
105
+ return;
106
+ if (!(await verifyPublished(name, version, section)))
107
+ return;
108
+ const key = `${name}@${version}`;
109
+ if (ledger[key] != null)
110
+ confirmed[key] = ledger[key];
111
+ await removePendingChangelog(opts.workspaceDir, name, version);
112
+ }));
113
+ return confirmed;
114
+ }
115
+ //# sourceMappingURL=applyReleasePlan.js.map
@@ -0,0 +1,91 @@
1
+ import type { ProjectManifest, VersioningSettings } from '@pnpm/types';
2
+ import type { ChangeIntent, ReleaseBumpType } from './intents.js';
3
+ import { type Ledger } from './ledger.js';
4
+ export interface WorkspaceProject {
5
+ rootDir: string;
6
+ manifest: ProjectManifest;
7
+ }
8
+ export type ReleaseCause = 'intent' | 'dependencies' | 'fixed' | 'epic';
9
+ export interface DependencyUpdate {
10
+ name: string;
11
+ newVersion: string;
12
+ }
13
+ export interface PlannedRelease {
14
+ name: string;
15
+ /** Workspace-relative project directory — the engine's unit of identity. */
16
+ dir: string;
17
+ rootDir: string;
18
+ currentVersion: string;
19
+ newVersion: string;
20
+ bumpType: ReleaseBumpType;
21
+ /**
22
+ * The intent files this release consumes for this package: the pending ones,
23
+ * plus — when the release graduates the package off a lane — the
24
+ * ones the ledger recorded against the lane's prerelease versions.
25
+ */
26
+ intents: ChangeIntent[];
27
+ dependencyUpdates: DependencyUpdate[];
28
+ causes: ReleaseCause[];
29
+ }
30
+ export interface ReleasePlan {
31
+ releases: PlannedRelease[];
32
+ }
33
+ export interface AssembleReleasePlanOptions {
34
+ workspaceDir: string;
35
+ projects: WorkspaceProject[];
36
+ intents: ChangeIntent[];
37
+ ledger: Ledger;
38
+ versioning?: VersioningSettings;
39
+ /**
40
+ * Workspace-relative directories of the projects selected with --filter.
41
+ * The plan is narrowed to the selected packages' portion of the pending
42
+ * work, expanded with their fixed-group companions and range-invalidated
43
+ * dependents.
44
+ */
45
+ filter?: Set<string>;
46
+ /**
47
+ * When set, every planned release gets the version `0.0.0-<suffix>` instead
48
+ * of the computed one, matching snapshot releases.
49
+ */
50
+ snapshotSuffix?: string;
51
+ /**
52
+ * Enforce that every internal production dependency uses the `workspace:`
53
+ * protocol — a prerequisite for actually releasing. The release path
54
+ * (`pnpm version -r`) sets this; read-only callers (`pnpm change status`)
55
+ * leave it off so a diagnostic never fails on an unmigrated dependency.
56
+ */
57
+ enforceWorkspaceProtocol?: boolean;
58
+ }
59
+ /**
60
+ * Whether a package reference is a workspace-relative directory path rather
61
+ * than a package name — the additive extension to the changesets format,
62
+ * needed only when workspace projects share a published name.
63
+ */
64
+ export declare function isDirRef(ref: string): boolean;
65
+ /**
66
+ * Resolves package references — bare names, or `./`-prefixed
67
+ * workspace-relative directories — against the workspace. Names are aliases:
68
+ * one that matches several projects cannot identify any of them and callers
69
+ * must treat it as an error, never a silent pick.
70
+ */
71
+ export interface ProjectRefIndex {
72
+ /** The directories a reference resolves to: `[]` unknown, 2+ ambiguous. */
73
+ refToDirs: (ref: string) => string[];
74
+ nameToDirs: (name: string) => string[];
75
+ }
76
+ export declare function indexProjectRefs(projects: ReadonlyArray<{
77
+ rootDir: string;
78
+ manifest: {
79
+ name?: string;
80
+ };
81
+ }>, workspaceDir: string): ProjectRefIndex;
82
+ /** The workspace-relative directory of a project, in canonical spelling. */
83
+ export declare function toProjectDir(workspaceDir: string, rootDir: string): string;
84
+ export declare function assembleReleasePlan(opts: AssembleReleasePlanOptions): ReleasePlan;
85
+ /**
86
+ * The range that pnpm materializes for a workspace: spec at pack time, given
87
+ * the dependency's version at the dependent's previous release. Dependent
88
+ * propagation republishes the dependent whenever the dependency's new version
89
+ * falls outside this range.
90
+ */
91
+ export declare function materializeWorkspaceRange(spec: string, depCurrentVersion: string): string | null;