Files
Zoltan KochanandClaude Opus 5.5 6f95957063 docs: lint prose with Vale (#17238)
* docs: lint prose with Vale

Add Vale with an in-repo style, .vale/styles/pnpm, that turns the
writing rules in AGENTS.md and the Google developer documentation style
guide into lint rules. The stock Vale packages (Google, Microsoft,
RedHat, write-good) were too noisy to block on, for example 1,300
passive-voice hits in the v12 docs alone, so the rules are written for
this repo, following the GitLab model. Each one links the guideline it
enforces.

Levels:
- error, fails CI: dashes, plain-word substitutions, condescending
  words, "please", "note that", hedges, puffery, optional plurals,
  exclamation marks, ellipses, curly quotes, generic link text, product
  spellings, sentence-case headings, repeated words, double spaces,
  implementation history in comments; in changesets, bold or italic
  emphasis, internal names and release-line names.
- warning: future tense, sentences over 30 words, "instead of" and
  "so that" tails in changesets.
- suggestion: passive voice.

.vale.ini scopes the rules. Docs get every rule. Contributor Markdown
and code comments keep dashes and headings as warnings until their
roughly 8,100 em dashes are rewritten. Released changesets stay in
.changeset/ and are not rewritten, so lint-prose.mjs lints only the
changesets added since PROSE_BASE (the PR base in CI).

The prose workflow installs a pinned, checksum-verified Vale release and
runs `pnpm lint:prose`. The rest of this change rewrites the existing
docs, READMEs, contributor Markdown, and code comments to pass the
error-level rules. Only prose and comment text changed. Renamed
headings keep their anchors.

Spelling stays with cspell, which checks identifiers that Vale does not
see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: keep prose lint from flattening meaning

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:48:09 +02:00

7.0 KiB

id, title
id title
versioning Release management

Added in: v11.13.0

pnpm can version and release a workspace on its own, without a separate release tool. The workflow has two halves:

  1. As you work, pnpm change records change intents: small markdown files in .changeset/ saying which packages a change affects, how each should be bumped, and a summary that becomes the changelog entry. These are committed alongside the change.
  2. At release time, the bare pnpm version -r consumes the pending intents: it bumps versions across the workspace, propagates to dependents, writes changelogs, and records what it consumed in a committed ledger.

Intent files use the changesets format, so an existing .changeset/ directory keeps working. See Using Changesets with pnpm if you would rather keep using the Changesets CLI.

Recording a change

pnpm change

This prompts for the affected packages, their bump types, and a summary, then writes a file like .changeset/calm-cats-resolve.md:

---
"@example/core": minor
---

Added a `--watch` flag to the build command.

To see what the pending intents would produce:

pnpm change status

Releasing

pnpm version -r

This applies the release plan: every package named by an intent is bumped, and so is every package that depends on it through a workspace: range. Preview it first with --dry-run, and narrow it with --filter.

Because a recursive run can bump many packages to different versions, no git commit or tag is created, because there is no single version to tag. Commit the result yourself, then publish with pnpm publish -r.

First releases

Since v11.16.0, the first release of a package publishes the version written in its manifest verbatim, instead of bumping off it. pnpm version -r and pnpm change status check the registry for each release's current version; when that version is not yet published, the package debuts at it and its pending change intents apply only from the next release. A newly added package seeded at 1100.0.0 with a minor intent is therefore published as 1100.0.0 rather than skipping straight to 1100.1.0.

Private packages ("private": true) are never published, so they skip this registry check: their pending change intents are always applied on top of the version in their manifest.

Configuration

Release behavior is configured under the versioning key of pnpm-workspace.yaml:

versioning:
  fixed:
    - ['@example/cli', '@example/napi']
  ignore:
    - '@example/internal'
  maxBump: minor
  lanes:
    '@example/cli': alpha
  changelog:
    storage: repository

Every key is described in Versioning Settings.

Where two workspace projects publish the same name, a project can be referenced by its workspace-relative directory instead of its name, with a ./ prefix (e.g. "./packages/cli"). This works in intent files and in versioning.lanes, versioning.fixed, and versioning.ignore.

Fixed groups

Packages listed together in versioning.fixed always release at one shared version: the highest current version in the group, bumped by the largest bump any member needs. A fixed group must move between lanes together, and must sit entirely inside or entirely outside an epic.

Lanes

A lane is a parallel release track. While a package is on one, it releases X.Y.Z-<lane>.N prereleases from the same runs that release stable versions of everything on the main lane. This lets a rewrite or a major-version line bake in public while the rest of the workspace keeps shipping.

pnpm lane alpha --filter @example/cli   # move onto the alpha lane
pnpm lane main --filter @example/cli    # graduate back to stable
pnpm lane                               # show membership

See pnpm lane for how the prerelease versions are computed.

Epics

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 to M*100+99.

versioning:
  epics:
    - lead: '@example/app'
      packages:
        - './packages/**'
        - '!./packages/private-*'

With the lead on 11.x, members occupy majors 1100 to 1199. Members move independently inside the band: patch, minor, and even 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. Selectors are evaluated in order and the last one to match decides, so a later include can re-admit a package an earlier negation excluded. The lead is never a member of its own band.

Checking committed versions

pnpm change check validates the versions already committed in the workspace against the versioning.epics bands and the versioning.fixed groups, without reading any change intent. Run it in CI, next to the lint and test steps, so a version that drifted out of its band is caught on the pull request that introduced it rather than at release time.

Changelogs

By default (versioning.changelog.storage: registry) 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. Consumed change intents are garbage-collected by a later pnpm version -r only once the registry confirms the version was published with its section.

Set versioning.changelog.storage: repository to keep committed CHANGELOG.md files in every package instead.

Private packages are never published, so no tarball could carry their changelog. Their releases always use repository storage, whatever is configured: the section is prepended to the package's committed CHANGELOG.md, and the release counts as consumed without a registry confirmation. An intent file that also names other projects is still deleted only once all of them have released.

The ledger

pnpm version -r records every consumed intent in .changeset/ledger.yaml, a committed, append-only file:

"@example/core@1.3.0":
  dir: packages/core
  intents:
    - calm-cats-resolve

Consumption is tracked per project: an intent file is deleted only once every project it names has released. This is what makes cherry-picks and merge-backs between release branches safe. A release branch that has already consumed an intent will not consume it again when the commit is merged forward, and a package on a lane can half-consume an intent whose prose still has a stable release to compose.