Back to publishing & release Automate semantic versioning Manage release channels Harden the supply chain

Release Channels and Dist-Tags

Every npm install your-lib without a version installs whatever the latest dist-tag points to — so dist-tags, not version numbers, decide what most users get. Release channels built on dist-tags let you ship prereleases to early adopters (next, beta, rc), publish per-pull-request builds for testing (canary), and keep patching old major versions (v3-lts) without any of that reaching users who just want the stable release. Misusing them — a release candidate tagged latest, a backport that moves latest backwards — breaks installs across your user base in minutes, and the damage spreads through every lockfile regenerated while the mistake is live. This section explains how dist-tags work, how to design channels, and how to automate them safely.

How dist-tags work

A dist-tag is a named pointer from a label to one published version, stored in the package's registry metadata:

npm view your-lib dist-tags
# { latest: '3.8.2', next: '4.0.0-rc.3', canary: '4.0.0-canary.1f3c2a9', 'v2-lts': '2.19.4' }
  • npm install your-lib installs latest.
  • npm install your-lib@next installs whatever next points to.
  • Ranges such as ^3.8.0 ignore tags entirely and pick the highest matching version — but they exclude prereleases unless the range itself names one.

Tags are mutable: npm dist-tag add your-lib@3.8.3 latest moves the pointer instantly, without publishing anything. Versions are immutable. That split — immutable versions, movable pointers — is what makes channels possible. The wider release process is covered in Package Publishing & Release Engineering.

Dist-tags as pointers into one version history A package's version list with several dist-tags pointing at different versions for stable, prerelease, canary and long-term-support channels. your-lib versions 2.19.4 ... 3.8.2 ... 4.0.0-rc.3 latest 3.8.2 — default installs next 4.0.0-rc.3 — early adopters canary 4.0.0-canary.1f3c2a9 — per commit v2-lts 2.19.4 — security fixes
Each channel is just a named pointer; moving it never changes a published version.

Concept overview: designing channels

Most projects need only a few channels:

Channel Tag Versions Audience
Stable latest 3.8.2 everyone
Next major next or rc 4.0.0-rc.3 early adopters, framework partners
Experimental canary or alpha 4.0.0-canary.<sha> testing a specific change
Maintenance v3-lts 3.x patches users who cannot upgrade yet

The guides in this section implement each one: Publishing Prereleases with Changesets Pre Mode for next, Publishing Canary Releases from Pull Requests for per-change builds, Maintaining LTS Branches with Backport Releases for maintenance lines, and Fixing a Wrong latest Dist-Tag for recovery when a tag points at the wrong version.

A major release moving through channels The next major starts as canary builds, becomes a release candidate on next, is promoted to latest, and the previous major moves to an LTS tag. canary 4.0.0-canary.<sha> per PR next: rc 4.0.0-rc.1 ... rc.3 latest 4.0.0 promoted v3-lts 3.8.x keeps patches
A version climbs channels by moving tags, never by republishing.

Core initialisation and configuration

Publishing to a channel is one flag:

npm publish --tag next            # 4.0.0-rc.1 goes to next, latest is untouched
npm publish --tag canary          # experimental builds
npm publish                       # stable: implicitly --tag latest

Recent npm 11 releases refuse to publish a prerelease version without an explicit --tag, precisely to stop release candidates from becoming latest. Older npm versions will happily do it, so configure the tag explicitly in CI regardless of the npm version.

Moving tags after publishing:

npm dist-tag add your-lib@4.0.0 latest     # promote
npm dist-tag add your-lib@3.8.2 v3-lts     # label the last 3.x
npm dist-tag rm your-lib canary            # retire a channel
npm dist-tag ls your-lib                   # inspect

For packages published by release tools, set the tag in configuration rather than on the command line. In Changesets, pre mode sets it automatically; for manual control, changeset publish --tag next. In semantic-release, each branch in branches can declare a channel:

{
  "branches": [
    "main",
    { "name": "next", "channel": "next", "prerelease": "rc" },
    { "name": "3.x", "range": "3.x", "channel": "v3-lts" }
  ]
}

Architecture: how installs interact with tags and prereleases

Understanding what consumers' installs actually do prevents most channel mistakes:

  • Fresh npm install your-lib records a caret range of the latest version, ^3.8.2. Moving latest to 4.0.0 changes what new installs get, but existing ^3.8.2 ranges never jump to 4.x.
  • Ranges exclude prereleases. ^4.0.0-rc.1 matches later release candidates and 4.0.0, but ^3.8.2 never matches 4.0.0-rc.3. Prereleases reach only users who opt in.
  • Moving latest backwards — pointing it at an older version after publishing a newer one — is allowed and sometimes necessary for recovery, but range resolution still sees the newer version: ^3.8.2 resolves to 3.9.0 if it exists, regardless of where latest points.
  • Lockfiles are unaffected by tag moves; they pin exact versions.
What each install spec resolves to Shows how bare installs, caret ranges, prerelease ranges and explicit tags resolve given latest 3.8.2, next 4.0.0-rc.3 and a published 3.9.0. resolves to influenced by tags? npm i your-lib 3.8.2 (latest) yes ^3.8.2 3.9.0 (highest 3.x) no ^4.0.0-rc.1 4.0.0-rc.3 no your-lib@next 4.0.0-rc.3 yes
Tags drive bare and tagged installs; ranges follow versions and skip prereleases.

Channels in monorepos

In a monorepo that publishes many packages, channels need a consistent policy across packages, or consumers end up with mismatched prereleases. Two approaches work.

Channel per release, applied to every changed package. When the repository enters a prerelease cycle, every package released during the cycle goes to the same tag (next), so npm install @acme/react@next @acme/core@next gives a coherent set. Changesets' pre mode implements exactly this: while in pre mode, every published package gets a prerelease version and the pre tag. For fixed version groups this is natural; for independent packages it means some packages have prereleases and others do not, and consumers must understand that @next of an unchanged package simply points at its latest stable version or does not exist.

Channel per package. Packages with independent lifecycles run their own channels — @acme/cli may have a next while @acme/ui does not. This suits loosely related packages but makes "install the next versions of everything" impossible.

Whichever you choose, write down which tags exist for which packages, and check tag consistency in the release job with npm dist-tag ls for each published package.

A coordinated prerelease across monorepo packages In a coordinated cycle, core, react and vue all publish prereleases to the next tag so consumers can install a coherent set; unchanged tools stay on latest. @acme/core@next 5.0.0-rc.2 @acme/react@next 5.0.0-rc.2 @acme/vue@next 5.0.0-rc.2 consumer app installs @next for all three
Coordinated channels let consumers install a consistent set with one tag.

Testing a channel before promotion

The point of a prerelease channel is feedback before latest moves, so plan how that feedback arrives. Useful sources are your own applications (upgrade one internal application to @next for each release candidate), a small group of external early adopters who have agreed to test release candidates, and automated compatibility checks — for example, a CI job in a few representative consumer projects that installs @next nightly and runs their test suites. Set an explicit promotion rule: for instance, a release candidate is promoted when it has been on next for a week, passed the consumer test jobs, and no blocking issue was reported. Written rules make promotion a routine decision rather than a judgement call under deadline pressure.

Execution strategy: automating channels safely

Channels are only as safe as the automation that publishes to them. Three rules cover most failures:

  1. Derive the tag from the version, never from memory. In CI, a prerelease version must map to its channel tag, and only stable versions from the stable branch may use latest.
  2. Publish canaries under unique prerelease versions, such as 0.0.0-canary-<sha> or 4.0.0-canary.<sha>, so they can never be mistaken for or collide with real releases.
  3. Promote by moving tags, not by republishing. A release candidate that passed testing becomes stable by publishing the final version — or, for projects that ship identical builds, by npm dist-tag add your-lib@4.0.0 latest on an already-published final version.

A guard step in the release workflow enforces the first rule:

version=$(node -p "require('./package.json').version")
branch="${GITHUB_REF_NAME}"
if [[ "$version" == *-* ]]; then
  tag="next"; [[ "$version" == *canary* ]] && tag="canary"
elif [[ "$branch" == "main" ]]; then
  tag="latest"
else
  tag="v${version%%.*}-lts"
fi
echo "Publishing $version with tag $tag"
npm publish --tag "$tag"

Retiring channels and cleaning up tags

Tags outlive their purpose unless someone removes them. After a major release, next still points at the last release candidate until the next cycle begins — anyone installing @next gets an outdated prerelease. After a maintenance line reaches end of life, its LTS tag keeps pointing at the last patch, which is fine as a record but should be announced as unsupported. Canary tags are the worst offenders, because they point at arbitrary experimental builds.

A short checklist after each major release keeps things tidy: move next to the new stable version (or remove it until the next cycle), create the LTS tag for the previous major, remove experiment-specific tags, and deprecate prerelease versions that should no longer be installed. Deprecation is covered in Deprecating npm Package Versions.

Communicating channels to users

Most users never look at dist-tags, so channels only help if they are documented where users look. Put a short "Release channels" section in the README listing each tag, what it contains, and how stable it is. Mention the channel in every prerelease's release notes and in the install command you share (npm install your-lib@next). For LTS lines, state the support window — "3.x receives security fixes until 2027-03-31" — and link to the upgrade guide for the current major. When latest moves to a new major, announce it with migration notes; many users only discover a major version when a fresh install gives them one.

Registry behaviour and caching

Tag changes take effect on the registry immediately, but clients see them through caches. npm caches package metadata locally and respects cache headers from the registry and any CDN in between, so a user who installed a minute before you moved latest may get the old target for a short while. Proxy registries (Verdaccio, Artifactory, CodeArtifact) cache metadata too, often for longer, and serve their cached view of tags to everyone behind them. Two consequences follow. First, after a tag fix, users behind a proxy can keep receiving the wrong version until the proxy refreshes; tell platform teams to refresh metadata for the package, or wait out the cache window. Second, automation that publishes and immediately installs by tag — for example, a documentation site that installs @next right after a prerelease is published — should install by exact version instead, or poll until the registry reports the new tag, as described in Fixing npm ETARGET 'No Matching Version Found'.

Channels for applications versus libraries

Dist-tags are a library concern: they decide what consumers install. Applications that are deployed rather than installed rarely need them. When an organisation publishes internal applications as packages — CLIs distributed through a private registry, or deployable bundles — channels work the same way and give operations teams a clean way to roll out: publish 2.4.0-rc.1 to next, let a pilot group install it, then promote. For web applications deployed from a repository, the equivalent is deployment environments and feature flags, not dist-tags.

Channel design checklist

Before adding a new channel, answer four questions: who installs it, how it is published (which branch and workflow), what version scheme it uses (so it never collides with stable versions), and when it will be retired. If any answer is unclear, the channel will probably confuse users more than it helps. A small set of well-documented channels — latest, next, and one LTS line — serves most packages; add canary only if you actively test per-change builds with real consumers.

Security and isolation

Channels multiply the number of published versions and the number of workflows that publish, so apply the same controls to all of them. Canary and prerelease workflows should use trusted publishing or tokens as tightly scoped as stable releases, because a canary is installable by anyone who asks for it. Pull requests from forks must not publish canaries — they would run untrusted code with publish rights — so restrict canary publishing to branches in your own repository or to a maintainer-triggered workflow. Dist-tag changes are writes that require the same authentication as publishing, and with 2FA-required packages they need either an interactive OTP or the trusted CI identity. Treat latest as a production deployment: only a reviewed workflow on a protected branch should move it.

CI/CD integration

A single workflow can serve stable, prerelease and maintenance channels by branch:

name: release
on:
  push:
    branches: [main, next, "v*.x"]

permissions:
  contents: write
  id-token: write              # trusted publishing + provenance

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: npm
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-node@v4
        with: { node-version: 24, registry-url: https://registry.npmjs.org }
      - run: npm ci && npm run build && npm test
      - name: Compute dist-tag from branch and version
        id: tag
        run: |
          v=$(node -p "require('./package.json').version")
          case "$GITHUB_REF_NAME" in
            main) [[ "$v" == *-* ]] && { echo "prerelease on main"; exit 1; }; t=latest ;;
            next) t=next ;;
            v*.x) t="${GITHUB_REF_NAME%.x}-lts" ;;
          esac
          echo "tag=$t" >> "$GITHUB_OUTPUT"
      - run: npm publish --tag "${{ steps.tag.outputs.tag }}"
      - run: npm dist-tag ls "$(node -p "require('./package.json').name")"

Step by step: each branch maps to exactly one channel; a prerelease version on main fails the job; publishing uses the computed tag; the final step prints the resulting tags into the log so any surprise is visible immediately.

Worked example: introducing channels to a mature library

A widely used date library had only ever published to latest. Its maintainers wanted to ship a major rewrite without surprising the thousands of projects that install it without a version. They introduced three channels in one release cycle. First, every merged pull request on the v5 branch published a canary (5.0.0-canary.<sha>) so contributors could try changes in real projects. Second, feature-complete builds were published as release candidates under next, and the README gained a "Try the next major" section with npm install date-lib@next. Third, when 5.0.0 shipped, latest moved to it, the last 4.x release was tagged v4-lts, and the README stated that 4.x would receive security fixes for twelve months.

The CI workflow computed tags from branches and versions, so no release could accidentally land on latest, and a scheduled job reported every tag's target weekly. Over the release candidate period, three consumer projects run by the maintainers' employer installed @next nightly and caught two regressions before the stable release. When 5.1.0 later needed a fix backported to 4.x, the maintenance workflow published 4.12.3 under v4-lts without touching latest.

Pitfalls

Mistake Impact Remediation
Publishing a prerelease without --tag RC becomes latest for every new install Derive tag from version; use npm 11+
Backport release published as latest latest moves backwards to an old major Publish maintenance releases with an LTS tag
Canary versions that look like real releases Users pin experimental builds Unique prerelease identifiers with a SHA
Forks can trigger canary publishing Untrusted code published under your name Restrict to same-repository branches
Stale tags left behind Users install abandoned prereleases via @next Retire or move tags after each major

Guides in this topic

Every guide below solves one concrete task or error within Release Channels and Dist-Tags. Start with the one whose symptom matches what you are seeing:

Frequently Asked Questions

Can two tags point to the same version? Yes. After promoting 4.0.0, both latest and next may point to it until the next prerelease cycle starts; that is normal.

Can I delete the latest tag? No — every package must have a latest tag. You can move it to another version, but not remove it.

Do dist-tags affect GitHub Packages and private registries? Most npm-compatible registries support dist-tags with the same commands. Check your registry's documentation for tag permissions and limits.

How do consumers find out about a channel? Document channels in the README (npm install your-lib@next for the upcoming major) and announce prereleases in release notes. Tags are discoverable with npm view your-lib dist-tags, but few users look.

Why did npm install give some users a release candidate? Almost always because a prerelease was published without --tag and became latest. Move latest back to the stable version immediately, as described in Fixing a Wrong latest Dist-Tag, then fix the workflow.

Is there a limit on the number of dist-tags? There is no practical limit for normal use, but every tag is something users might install. Keep tags few and meaningful, and remove experimental ones when they are no longer needed.

Can I publish to a channel from a local machine? You can, with 2FA, but channel publishes are easy to get wrong by hand. Route every channel through the same reviewed workflow as stable releases, and keep manual publishing for emergencies with a dry run first.

Related

Package Publishing & Release Engineering