DevOps · Release Engineering
Pre-release Channels and Promotion in Package Registries
How candidate builds are published under pre-release identifiers, how dist-tags and channels make the same version resolve differently for different consumers, and why promotion should never rebuild.
Between “the code is merged” and “the release is out” there is a window in which the artifact exists but should not be selected by default. Registries solve this with two independent mechanisms that are often conflated: a pre-release version identifier, which is part of the version string and therefore part of the ordering, and a mutable channel label, which is registry metadata outside the version string. Their combination determines what an installer resolves, and the two interact in ways that produce surprising installs.
The identifier lives in the version
Semantic versioning allows a hyphenated pre-release field after the patch number, and specifies that any version with such a field sorts below the same version without one. 1.5.0-rc.1 < 1.5.0-rc.2 < 1.5.0. The pre-release field is dot-separated, numeric identifiers compare numerically, and a numeric identifier sorts below an alphanumeric one.
This gives a total order with a property that matters for a build pipeline: candidates are ordered relative to one another and relative to the release they precede, using nothing but the version string. No registry metadata is needed to say that rc.2 supersedes rc.1.
The counterweight is that ranges behave unexpectedly. The common convention is that a range without a pre-release component does not match a pre-release version. A dependent asking for ^1.5.0 will not receive 1.5.1-rc.1; a dependent asking for >=1.0.0 will not receive 2.0.0-beta.3. This is deliberate, since the alternative is that any prefix range silently upgrades consumers to candidate builds. It also means a candidate can only be reached by a consumer who wrote a constraint naming the pre-release component, or who installed it explicitly. The constraint dialects that make this work, and the places where they differ from each other, are the subject of lockfiles and version resolution.
Sequencing identifiers raises its own problem. rc.10 sorts above rc.9 because the comparison is numeric, so zero-padding is unnecessary; but a pipeline that uses a commit counter or a timestamp as the identifier produces a string that must be compared consistently. A timestamp formatted as 20260325.4 is two numeric identifiers and orders correctly; a date with hyphens inside the pre-release field would split into more identifiers and still order correctly, but would be visually ambiguous. What fails is a mixed scheme where some builds use beta and some use rc: 1.5.0-beta.2 and 1.5.0-rc.1 compare by ASCII, and beta sorts below rc because b precedes r. Reusing an earlier label for a later build is the same class of error.
The channel is registry metadata
A dist-tag or channel label is a name mapped to a version, held by the registry, and mutable. latest, next, canary, legacy-1.x are typical. An installer that asks for a package by name with no version receives the version the default label points at; an installer that names a label receives that one.
The three properties worth stating precisely:
- Labels are not versions. Resolving
pkg@nextyields whatever version is currently taggednext, which can change without any new version existing. - Labels are not part of the ordering. Moving
latestfrom1.4.2to1.5.0changes nothing about how1.5.0compares with anything. - Labels are per-artifact and, in most registries, global to the package. Two different consumers asking for the same label get the same answer.
The default label is the dangerous one. Whatever an ecosystem calls it, it is the thing installed by a bare package name, and it is what every consumer who did not think about versions will receive. A promotion step that moves the default label to a release candidate defeats every mechanism the pre-release identifier provided, because the constraint that excluded candidates is not consulted when the label is explicit.
There is a second, subtler hazard: the default label and the version space can disagree. If the highest published version of a package is a pre-release and the default label still points at an older stable version, pkg installs the older one while pkg@^1.0.0 may resolve to something newer within its range. Registries that auto-advance the default label on publish make this worse, because publishing a candidate can move the label without anyone intending it.
Promotion should move a reference, not rebuild
The standard shape of a delivery pipeline is: build once, verify, then move the artifact through environments. Repositories implement this by promoting an artifact — changing which channel points at it, or copying it between repository paths — rather than building again for each stage.
The reason is that a rebuild is not a copy. Compilers are deterministic in principle and often not in practice: timestamps embedded in archives, map iteration order, absolute paths, build-id derivation, and dependency resolution against a moving index all vary between runs. A rebuild for production after validation in staging produces a different byte string, so the validation did not apply to the artifact being shipped. This is the same reasoning that makes an artifact coordinate need to be immutable, explored in immutable artifact coordinates and provenance.
Promotion by reference keeps one byte string per release. The verification evidence, recorded against a digest, applies to exactly the bytes that reach consumers. The trade is that the promoted artifact was built in the earliest environment’s conditions, including its toolchain patch level and its dependency resolution, which is why the build environment needs to be pinned rather than assumed.
Where a rebuild is unavoidable, as with a different platform ABI or a separate signing domain, the second build is a second artifact with its own digest, not a replacement, and both need their own verification. Treating it as the same release is the mistake.
Staged rollout as a channel with a denominator
A rollout percentage is a channel whose membership is decided per-request rather than by a label. Where the registry has no such concept, the same effect is achieved by publishing a sequence of versions and moving the default label at increasing exposure, or by having clients decide from a stable hash of an installation identifier.
The property that makes staged rollout safe is that membership is stable for a given client. If the decision is a random draw per request, a client alternates between two versions within one session, and the comparison between cohorts measures that alternation rather than the release. A hash of a stable identifier gives each client a fixed side of the split. The percentage then controls how many clients are on the new side, and moving from five to fifty percent adds clients without removing any, which is what makes the cohorts nested and the comparison meaningful.
What staged rollout cannot do is bound the blast radius of a defect that manifests slowly. The denominator is installations, not requests or page views, and a defect that appears after a schema migration has run on a client takes as long as the migration to surface. The observable signal in that case comes from the version dimension of telemetry, which is why every metric needs a release identifier attached.
Failure modes that recur
Three misconfigurations account for most channel incidents.
Publishing a candidate as the default. Some publish commands move the default label automatically unless told not to. The candidate then becomes what a bare install retrieves, and the pre-release identifier’s exclusion from ranges is bypassed by installations that name no version.
Reusing a version number. Registries that permit overwriting an existing version allow a candidate and a release to share a coordinate, at which point the channel mapping is meaningless and any cached artifact is ambiguous. The version string is the only thing a range resolver sees; if two different byte strings can occupy it, every reproducibility claim downstream is void.
Two candidates with the same identifier. A pipeline that restarts after a failure and re-derives the pre-release sequence from a counter that resets will publish a second artifact under an existing identifier. The mitigation is that the identifier includes something monotone and globally unique, either a build number that never resets or the commit timestamp with a per-commit sequence, and that publication is a write-once operation that refuses a coordinate already occupied.
The general principle behind all three: the version string is the only channel-independent fact, and the label is the only mutable one. Any process that lets mutability leak into the string, or immutability be assumed of the label, produces a system where two consumers asking the same question at different times receive different answers about the same release.
References
- Semantic Versioning 2.0.0 — SemVer project
- npm documentation — dist-tag — npm project
- npm documentation — package.json — npm project
- The Update Framework specification — TUF project