Skip to main content

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.