Skip to content

Versioning

Versioning

pkgist computes the next version per package from a strategy you pick, then writes it back to the source package.json before building.

The five strategies

StrategyWhat it does
"auto" (default)Bump the patch digit. 2.1.0 → 2.1.1
"patch"Identical to "auto". Bump the patch digit
"minor"Bump minor, reset patch. 2.1.0 → 2.2.0
"major"Bump major, reset minor + patch. 2.1.0 → 3.0.0
Literal semver stringUse exactly this version. version: "3.0.0" → 3.0.0

Pre-release versions follow the same rules (semver-internal):

  • "patch" on 2.1.0-beta.1 → 2.1.0 (drops pre-release)
  • An explicit "2.1.0-beta.2" is also valid

Overriding per run from the CLI

The same five strategies are available as a per-run CLI override via --bump, so you don’t have to edit the version field in pkgist.config.ts for every release:

Terminal window
pkgist build @scope/utils --bump minor # ignore config version, minor-bump this run
pkgist build:family atom --bump 6.1.0 # set the family's shared version explicitly
pkgist build:all --bump patch # patch-bump every package this run

--bump <strategy> takes the identical values to the config version field (auto / patch / minor / major / a literal semver) and overrides it for that invocation only — the config file is never mutated. For a standalone build it applies to every targeted package; for a family build it replaces the family’s single shared version (the highest-current-member rule still applies when the strategy is a bump keyword). When --bump is omitted, the config version (or its "auto" default) is used.

Named --bump, not --version, because commander reserves --version for pkgist --version. See the cli skill’s Per-run overrides section, and git-workflow for the paired --commit flag.

Standalone packages

{ name: "@scope/utils", root: "../utils", version: "auto" }
// current: 2.1.0 → next: 2.1.1
{ name: "@scope/utils", root: "../utils", version: "minor" }
// current: 2.1.0 → next: 2.2.0
{ name: "@scope/utils", root: "../utils", version: "3.0.0" }
// any current → next: 3.0.0

Family packages

Families synchronize. The family picks the highest current version across all members, then bumps it. All members land on the same new version.

// Members' current versions on disk:
// atom = 1.0.5
// react-atom = 5.1.3 ← highest
// atomic-query = 0.1.0
families: [
{ name: "atom-family", version: "patch", packages: [...] }
]
// → all three become 5.1.4

This means adding a new low-versioned package to a family is intentional surgery: it jumps to match the family on the first build. The family version is the compatibility contract — if you publish react-atom@6.0.0, every other family member must also be 6.0.0 so consumers can lock to one version.

Strategy on the family, not on members

version is family-level only. Family-member entries don’t accept their own version — it’s whatever the family decided.

families: [
{
name: "atom-family",
version: "minor", // ← family-level
packages: [
{ name: "@scope/atom", root: "../atom" }, // no version
{ name: "@scope/react-atom", root: "../react-atom" }, // no version
],
},
]

Intra-family dependency pinning

In a monorepo, sibling packages reference each other with "*" so the workspace links them to the local copy:

// @scope/react-atom — SOURCE package.json
"dependencies": { "@scope/atom": "*" }

"*" must never reach npm — it lets a consumer resolve the sibling to any published version, which breaks the “whole family on one exact version” contract and risks pulling an incompatible release. So during a family build, pkgist rewrites every dependencies / peerDependencies entry whose name is another family member to the exact shared release version in the published package.json. The source file is never mutated — it keeps "*" for workspace linking (a fresh object is written for the build).

// @scope/react-atom — PUBLISHED package.json, family released at 5.1.4
"dependencies": { "@scope/atom": "5.1.4" } // not "*", not "^5.1.4" — exact

Rules:

  • Family builds only. Standalone builds (build / the standalone half of build:all) never pin — their deps publish verbatim.
  • Applies to dependencies and peerDependencies. (devDependencies are dropped from the build entirely; optionalDependencies are not carried into the published package.json.)
  • Only family-member names are rewritten. Every non-member dependency keeps its original range exactly as written.
  • The pin is the exact shared version string (e.g. 5.1.4) with no ^ / ~ — so an installed family resolves to one coherent, same-version set.

This is what makes the family version a true compatibility contract: members share it, and their cross-references resolve to it.

Picking the right strategy

This is a semantic question, not a pkgist one. The convention:

When changes areUse
Bug fixes, internal refactors, docs, lockfile/CI updates with NO API surface change"patch"
Strictly additive: new exports, new optional params, new fields on result types"minor"
Anything a consumer’s code would have to change to absorb (removals, renames, signature changes, default-behavior flips, runtime deps bumped major)"major"

For families specifically: pick based on the most significant change in any member. If one family member made a breaking change, the family bumps major even if the others only had docs updates.

Explicit strings for unusual paths

The literal-string form unlocks:

  • First 1.0.0 release: version: "1.0.0" (rather than bumping 0.x.x)
  • Re-publishing after a botched release: version: "2.1.3" to overwrite intent (only works if npm hasn’t seen this version — never use to “force overwrite” a published version, which npm prevents anyway)
  • Pre-releases: version: "2.0.0-beta.1", then "2.0.0-beta.2", then "2.0.0" for the final cut
  • Hotfix lineage: version: "1.2.3" to ship a patch on an old major if you’ve moved past it