Versioning
Two audiences want different things from a version number, so a distribution gives them different ones.
Months for end users
End users want to know their copy is current. 25.12.01 against a latest of
26.09.01 says nine months have passed, immediately. 1.1.1 against 1.2.1
says nothing about time, and secure software needs regular updates — so bumping
should be obviously overdue when it is.
A distribution is therefore versioned by the month it is released: YY.MM.NN,
the year, the month, and which release of that month it is. 26.09.01 is
September 2026's first release, 26.09.02 its second. The day says nothing a
reader needs, so it is not there, and neither is the 20.
Until a release is stable it goes through stages, each counted from zero:
26.09.01-alpha.0 the first release of September 2026, first alpha
26.09.01-alpha.1
26.09.01-beta.0
26.09.01 stable
26.09.02-alpha.0 the month's second release
26.10.01-alpha.0 October's first
A new month starts again at its first release, the count at zero, whatever was
unfinished before — so a distribution still in alpha publishes alpha.0,
alpha.1, … until the month turns. next-version
works out what comes next, and the publish workflow refuses a release of a month
that is over. A new distribution starts at the month's first release, in alpha.
Semver for developers
Developers consuming a module want to know how much work an upgrade is. 1.2.3
to 1.2.4 is nothing; 2.0.0 is a morning. So modules are versioned by semver
and tagged in their own repositories.
Tags go on the module, not the distribution. A monorepo that tags every package fills its history with noise; here each module has its own repository, so its tags are its own.
Where a version comes from
The submodule commit is the pin. The version is whatever the package.json
inside that checkout says. Nothing in the distribution repository restates it.
pnpm links the checkout into the workspace, so that is also the version
everything resolves against — an app depending on a package gets the checkout,
not a published copy. Modules are depended on with workspace:*, which names no
version at all.
A repository that also wrote "@acme/design": "1.4.0" somewhere would be
carrying a second copy of the same fact, free to drift from the first.
pnpm dist sync reports the version it reads out of each
checkout:
apps/widget v1.2.0 -> v1.3.0 (minor)
packages/design v1.4.0 up to date
Published distributions
A published distribution has no submodules, so its manifest carries what it shipped, written in as it is published and never committed:
{
"name": "@fairgarden/core",
"version": "26.9.1-alpha.0",
"distribution": {
"modules": {
"@fairgarden/id": {
"version": "1.2.0",
"path": "apps/id",
"repository": "https://github.com/fairgarden/id.git",
"commit": "b12276c072c405673828843f3b2d440514b72713"
}
}
}
}
The version is what an extension is compared against, and
the rest is where it takes its own modules from: the path, the repository to
clone — a fork's, with its upstream beside it — and the commit, which may be
past the release the version names. It is the only place versions are written
down.
npm takes only semver, which allows no leading zeros, so the registry spells
26.09.01-alpha.0 as 26.9.1-alpha.0: the same version, sorting the same way.
The distribution's own manifest and its tag, v26.09.01-alpha.0, keep the zeros.
An extension depending on it names npm's spelling, or a dist tag.
Maintenance branches
A mature distribution has users slow to take major upgrades, which is what an LTS branch is for. How often to cut one is a trade between the cost of maintaining branches and the cost of forcing upgrades — once or twice a year, or every two, depending on how often you break things.
Yearly
lts-2023 is created in 2023 and maintained until 2025, tagged 23.01-lts,
23.02-lts and so on. lts-2024 is created in 2024 and maintained until
2026, tagged 24.01-lts onwards.
Quarterly
lts-2023-q2 is created in Q2 2023 and maintained until Q2 2024, tagged
23.04.01-lts, 23.04.02-lts and so on. lts-2023-q4 runs Q4 2023 to Q4
2024 with 23.10.01-lts onwards, and lts-2024-q2 runs Q2 2024 to Q2 2025
with 24.04.01-lts onwards.
Modules do not get LTS
A module that changes often enough gets maintenance branches by patch version instead:
@fairgarden/id
1.2.3 the latest version
1.2.x main, until it is branched off when 1.3.0 is released
1.1.x the previous maintenance branch
So @fairgarden/core:23.04.01 depends on 1.1.x and
@fairgarden/core:23.10.01 on 1.2.x.
These are only needed when a distribution is using LTS. A module carried in an LTS distribution is one that is probably dependable enough to use on its own too.