wandres.dev
PUBLICAR LA LIBRERÍA · versiones y provenance

dist-tags para releases: publicar next y beta sin mover latest

Publicar y distribuir son actos separados: la versión es un objeto inmutable en un registro append-only, y el dist-tag es un puntero móvil con nombre que decide qué recibe quien no pide nada. Por qué latest gobierna la instalación por defecto, cómo la exclusión de prereleases de SemVer forma una segunda red, cómo abrir canales next, beta, rc y canary sin perturbar a los estables, la maniobra de promoción como simple movimiento de puntero, y cómo automatizar el canal desde la rama en CI.

⏱ 17 min

Sacar la próxima versión mayor sin que estalle en producción de miles de proyectos parece imposible, y sin embargo es rutina diaria en el ecosistema. El truco vive en un mecanismo humilde y mal comprendido: el dist-tag. Es un puntero con nombre —como una rama de git, pero sobre versiones ya publicadas— que desacopla qué versiones existen de cuál recibe el usuario por defecto. Publicar es un acto irreversible; distribuir es completamente reversible. Dominar esa distinción es poder iterar en público sin cortarle el build a nadie.

🎯 Al terminar esta lección sabrás
  • Separar la publicación inmutable de la distribución mutable que gobiernan los dist-tags.
  • Entender por qué latest decide la instalación por defecto y cómo la exclusión de prereleases refuerza el flanco.
  • Abrir y gestionar canales next, beta, rc y canary con npm publish --tag y la familia npm dist-tag.
  • Automatizar la elección del canal desde la rama en CI con changesets o semantic-release.

Publicar frente a distribuir

Un registro es, en el fondo, un log de solo-añadir: cada versión que publicas es un objeto inmutable que nunca cambiará ni se sobrescribirá. Sobre ese almacén append-only, un dist-tag es una referencia móvil, exactamente como main en git no es un commit sino un puntero a uno. latest no es una versión: es el puntero que el registro considera actual.

Su privilegio es único: cuando alguien escribe npm install @acme/ui sin pedir versión, el registro resuelve lo que señala latest. Ese único puntero decide qué recibe por defecto todo el que llega sin especificar nada. Y como es móvil, moverlo cambia al instante qué código sirve una instalación limpia sin tocar una línea del paquete.

🟢

latest

El puntero por defecto: lo que recibe quien instala sin pedir versión. Debe señalar siempre la última estable.

🟡

next y beta

Canales de preestreno: la próxima mayor en preparación, o fases tempranas. Solo llegan a quien los nombra.

🔴

canary

Builds automáticos por commit, con versiones sintéticas que llevan el hash. El filo absoluto sin esperar a un release.

Por eso npm publish mueve latest a la versión recién subida salvo que le indiques lo contrario, y ahí está el peligro: si publicas una prerelease inestable y dejas que latest la señale, se la sirves de golpe a cada instalación nueva del mundo. La solución es publicar bajo otro puntero, dejando latest anclado en la última estable. Instalar una línea concreta es tan simple como nombrar su tag.

La separación se sostiene sobre dos capas de naturaleza opuesta:

  • Capa inmutable: las versiones. Una vez publicada, 3.0.0-beta.1 existe para siempre y su contenido no cambia jamás.
  • Capa mutable: los tags. latest, next o beta son punteros que se mueven sin republicar nada.
flowchart LR
V1[2.5.0 estable] --> V2[3.0.0-beta.1]
V2 --> V3[3.0.0-rc.1]
V3 --> V4[3.0.0 estable]
L[tag latest] --> V1
N[tag next] --> V3
LM[latest se mueve al promover] -.-> V4
style L fill:#a6e3a1,color:#11111b
style N fill:#fab387,color:#11111b
style LM fill:#a6e3a1,color:#11111b

La doble red: dist-tag y exclusión de prereleases

Los dist-tags no trabajan solos: se apoyan en una regla profunda de SemVer que actúa como segundo cerrojo. Una versión de prerelease —3.0.0-beta.1, 3.0.0-rc.0— lleva tras el guion un identificador que la marca como incompleta, y la especificación es tajante: los rangos normales no capturan prereleases. La consecuencia se resume en dos hechos:

  • Un ^2.4.0 jamás resolverá a 3.0.0-beta.1, aunque sea numéricamente mayor: un rango sin prerelease explícita ignora por diseño todo lo que lleve guion.
  • Solo recibe una prerelease quien la pide de forma deliberada, nombrando el tag o escribiendo un rango que la incluya, como 3.0.0-beta o el truco ^3.0.0-0.

Así, aunque por descuido publicaras una beta y latest se moviera hacia ella, quien tenga fijado un rango estable seguiría intacto: su resolución nunca contempla la prerelease. Son dos mecanismos ortogonales que se combinan: el dist-tag decide qué señala el puntero por defecto, y la regla de prereleases garantiza que los rangos existentes ni se enteren de las versiones marcadas como experimentales.

ℹ️
latest debe apuntar siempre a lo estable

La regla de oro es que latest señale la última versión estable, nunca una prerelease. Si por accidente publicaste una rc sin --tag y latest se movió hacia ella, el síntoma es que las instalaciones nuevas empiezan a recibir código no acabado. La corrección es inmediata y no destructiva: reancla latest a la última versión buena. El puntero es móvil por naturaleza, así que arreglar el error es mover una etiqueta, no republicar nada ni retirar la mala.

Abrir canales y promover

El flujo práctico combina el flag --tag al publicar con la familia npm dist-tag para inspeccionar y mover punteros después. Publicar una prerelease es versionar con sufijo y subirla bajo un tag no estable; promoverla a estable, más tarde, es solo mover latest —sin republicar el artefacto, que ya existe inmutable.

# publica una prerelease sin tocar a los usuarios estables
npm version 3.0.0-beta.1
npm publish --tag next

# inspecciona y mueve punteros despues
npm dist-tag ls @acme/ui
npm dist-tag add @acme/ui@3.0.0 latest   # promover es mover un puntero
npm dist-tag rm @acme/ui next            # retira el canal cuando sobra

# instala una linea concreta de forma deliberada
npm install @acme/ui@next

Las convenciones del ecosistema son estables y conviene reconocerlas, porque un nombre bien elegido comunica intención sin documentación:

  • latest: la última estable; el defecto de cualquier instalación.
  • next: la próxima mayor en preparación, usable pero no definitiva.
  • beta y alpha: fases tempranas, con la API aún en movimiento.
  • rc: release candidate, una beta que aspira a estable sin más cambios.
  • canary o nightly: builds automáticos por commit, para el filo absoluto.
  • legacy: una línea mayor antigua que aún recibe parches de mantenimiento.

El tag legacy merece nota aparte: mantiene viva una versión mayor anterior —parches de seguridad para quien no ha migrado— sin que latest deje de señalar la línea actual. Distribuir dos líneas en paralelo es, otra vez, cuestión de punteros, no de código.

Del lado de quien consume, elegir la línea es igual de explícito, y cada forma pide un puntero o un rango distinto:

# instalar cada linea de forma explicita
npm i @acme/ui              # el puntero latest, la version estable
npm i @acme/ui@next         # la linea de preestreno
npm i @acme/ui@3.0.0-beta.1 # una prerelease exacta y fijada
npm i @acme/ui@^3.0.0-0     # un rango que SI admite prereleases de la 3

El sufijo -0 del último rango es el truco que le dice al resolutor que, esta vez, sí considere las prereleases de esa línea mayor: es la puerta que el consumidor abre voluntariamente cuando quiere vivir en el filo de una versión concreta.

💡
Declara el canal, no lo teclees

Recordar --tag next en cada publicación es frágil: un descuido y mueves latest a una beta. Lo robusto es declararlo en publishConfig.tag dentro del package.json, o dejar que la automatización lo derive de la rama. El flag efímero del terminal es la fuente de error humano número uno del release manual; la decisión versionada en el manifiesto la elimina.

Automatizar el canal desde la rama

En proyectos serios nada de esto se hace a mano: la máquina deriva el tag de la rama, de modo que empujar a una rama concreta publica en el canal correcto sin intervención humana.

  • changesets: con changeset pre enter next entra en modo prerelease y dirige cada publicación al tag next; al cerrar la beta, pre exit vuelve a latest.
  • semantic-release: mapea ramas a canales —main a latest, beta a beta— y elige el número y el tag desde los commits, sin flags manuales.
  • canary por commit: un job publica versiones sintéticas tipo 0.0.0-canary seguidas del hash corto bajo el tag canary, para probar cada commit sin tocar latest.
# changesets: modo prerelease de la linea next
npx changeset pre enter next
npx changeset publish   # cada release sale bajo el dist-tag next
npx changeset pre exit  # al cerrar la beta, vuelve a latest

Las señales que la máquina traduce en un tag son explícitas y auditables:

  • la rama a la que empujas, que mapea a un canal fijo;
  • los mensajes de commit convencionales que marcan el nivel del cambio;
  • los changesets acumulados que declaran la intención por pull request.

El principio común es que la máquina, no la memoria del humano, elige el número y el dist-tag a partir de esas señales. Publicar deja de ser un ritual manual y se vuelve una consecuencia determinista de fusionar código, con cada canal firmado por su propia procedencia desde CI.

El dist-tag desacopla lo que existe de lo que se recibe

La idea que cristaliza este nivel es que publicar y distribuir son dos actos de naturaleza opuesta. Publicar crea una versión inmutable en un log append-only: eso es irreversible, para siempre. Distribuir es decidir a qué versión apunta cada puntero con nombre: eso es completamente reversible, un movimiento de referencia. Los dist-tags viven en esa segunda capa y no cambian el código, cambian a qué código llega quien no especifica nada. Cuando lo interiorizas, la maniobra que parecía suicida —sacar una mayor con cambios rompedores— se vuelve un baile controlado: publicas la beta bajo next, la maduras con quien la pide voluntariamente, y solo cuando confías en ella mueves latest. Mientras tanto, la exclusión de prereleases de SemVer protege el flanco pasivo, porque los rangos estables existentes son ciegos a todo lo que lleve guion. Dos mecanismos ortogonales —punteros móviles y exclusión de prereleases— se combinan para darte lo que parecía contradictorio: iterar agresivamente en público y, a la vez, no romperle el build a nadie que no haya pedido explícitamente vivir en el filo. Y cuando delegas la elección del tag a la rama en CI, esa separación entre existencia y distribución deja de depender de tu disciplina para volverse una propiedad estructural del pipeline. Esa es la maquinaria invisible que sostiene el release continuo de las librerías de las que dependen millones.

⚔️ Publica en el filo sin cortar a nadie
  1. Versiona un paquete de práctica como 1.0.0, publícalo y confirma con npm dist-tag ls que latest lo señala.
  2. Publica 2.0.0-beta.0 bajo --tag next y verifica que un ^1.0.0 en otro proyecto sigue resolviendo a la línea 1.
  3. Instala deliberadamente la beta con @next y luego con el rango ^2.0.0-0; explica la diferencia.
  4. Promueve la estable moviendo latest con npm dist-tag add y retira el tag next cuando ya no haga falta.
  5. Diseña sobre papel cómo changesets o semantic-release elegirían el canal desde la rama, sin un solo flag manual.