Language Registries Are Unstable by Default

pabs31 pts0 comments

Language Registries Are Unstable by Default | Andrew Nesbitt

Running pip install requests or npm install react against the public registry is the same operation, structurally, as running apt install -t unstable against Debian sid, and nobody involved talks about it that way. I don’t mean “unstable” as a synonym for buggy, I mean it in the specific sense Debian has used since the late nineties: a pool of packages where new versions land the moment a maintainer uploads them, with no promotion gate, no minimum residency time, and no quality bar between the upload and your machine.

On a language registry any authenticated publisher can push any version at any time, the index updates within seconds, and any resolver that hasn’t been deliberately configured otherwise will start selecting it on the next run. The path from a maintainer pressing enter to an enterprise CI pipeline executing the result has nothing in it except whatever the consumer remembered to set up, and most consumers haven’t set up anything.

Debian sid exists on the explicit understanding that you don’t point production at it. The default sources.list on a Debian install points at stable, where packages have spent months in testing, which they only entered after a mandatory stretch in unstable without release-critical bugs. Fedora has updates-testing with karma voting before promotion, Ubuntu has its -proposed pocket gated by autopkgtest, Arch has core-testing and extra-testing repos most users never enable, and FreeBSD ships both quarterly and latest branches of the ports tree.

Every system package manager I can think of presents the user with a choice of stability lane and defaults to the conservative one, with the bleeding-edge lane as something you opt into knowing what you’re getting. Language package managers offer one lane, it behaves like the bleeding-edge one, and there’s no switch on the wall to ask for the other thing.

We are well past the point where event-stream, Shai-Hulud, xz, and the current run of self-propagating GitHub Actions worms can be read as a string of unlucky one-offs. There is a malicious-package writeup from one security vendor or another most days of the week now, the GitHub Advisory Database adds malware entries faster than I can read them, and I’m tired of watching each one get treated as a clever attacker finding a surprising gap in an otherwise sound system.

A registry that accepts uploads from tens of thousands of loosely verified publishers and serves the newest upload as the default resolution target within minutes is going to ship malware to consumers at some ambient rate, because that is what an unstable pool is for. We’ve wired that pool directly to production with no promotion step, and I find the recurring surprise harder to justify than the incidents themselves, given the design is the one distributions explicitly label as the lane you run at your own risk.

The integration problem

Distributions ended up with stability channels because a distribution owns the integration problem: tens of thousands of packages have to boot a working operating system together, so somebody upstream of the user has to check that glibc, systemd, Python, and GNOME all agree on the world before any of it ships. The release team is a structural necessity, and once you have a release team you have promotion gates, and once you have promotion gates you have channels almost by accident.

Language registries made the opposite call early on by pushing the integration problem down to each consumer’s lockfile. There was never a single party whose job it was to ask whether requests 2.32.0 and urllib3 2.2.0 and certifi 2024.2.2 actually work together, so that question gets answered thousands of times a day in thousands of CI pipelines instead of once at the registry. With no upstream actor responsible for integration, there’s nobody in a natural position to run a promotion gate either, and the registries themselves have generally declined to be that actor, treating themselves as neutral pipes rather than as the governance layer a promotion policy would require.

The vocabulary gap compounds this, because “channel” is a word from distribution and toolchain land (rustup has stable, beta, and nightly; conda has defaults and conda-forge; snap exposes tracks and flatpak exposes branches; Nix has nixos-25.11 and nixos-unstable), while language registries talk about indexes, dist-tags, and pre-release markers, none of which carry the same connotation of “pick how much risk you want.” Someone who came up through Debian or Fedora packaging looks at npm and immediately sees the missing pattern, but someone who learned dependency management from package.json outward has no name for the thing that isn’t there, so it doesn’t register as an absence.

A few language ecosystems have built partial equivalents, the strongest of which is Stackage, a curated set of LTS and nightly snapshots layered over Hackage and modelled on distribution release...

language unstable promotion registries debian from

Related Articles