Flujos de lanzamiento y publicación para Node.js
Tanto la entrega continua como el despliegue continuo requieren el uso de flujos de trabajo para generar un nuevo lanzamiento y publicarlo. Recordemos que la entrega continua y el despliegue continuo suelen utilizar una estrategia de ramificación troncal que permite publicar sin problemas cuando es necesario, ya que son estables y los requisitos de publicación se cumplen. La diferencia es que la entrega continua utiliza un despliegue automatizado, pero disparado manualmente; en cambio, el despliegue continuo realiza la publicación automáticamente sin intervención manual.
Al finalizar, sabrá:
-
Qué son los flujos de lanzamiento y de publicación.
-
Cuáles son los eventos más utilizados para disparar la ejecución de estos flujos de trabajo.
-
Cómo versionar paquetes de NPM con npm y Lerna.
-
Cómo realizar la publicación en NPM con npm y Lerna.
-
Para qué utilizar el comando npm pkg.
Nota. Esta lección asume que se publicarán los paquetes desde un monorepo. Cualquier modificación para un repositorio de un único paquete no le resultará difícil implementarla.
Introducción
Cuando necesitemos publicar los paquetes del monorepo, por ejemplo, tras la finalización del sprint o como consecuencia de un hotfix, la manera más sencilla de hacerlo es mediante el uso de npm. En primer lugar, hay que actualizar la versión de los paquetes del monorepo, esto lo haremos con npm version. Una vez fijada la versión de los paquetes, pasaremos a publicarlos en NPM mediante npm publish.
El flujo de lanzamiento (release workflow) es aquel que se encarga de generar una nueva versión del software; mientras que el flujo de publicación (publish workflow) es el encargado de publicar una versión generada por el de lanzamiento en NPM.
Flujo de lanzamiento
El objeto del flujo de lanzamiento (release workflow) es generar un nuevo lanzamiento o versión, aunque, recordemos, algunas organizaciones suelen utilizar este flujo también para realizar su publicación. En nuestro caso, los separaremos, pero cada organización es un mundo y podría hacer ambas tareas con el mismo flujo.
Su proceso es muy sencillo:
-
Clonar el repositorio de trabajo.
-
Generar una nueva versión.
-
Generar y publicar el lanzamiento de GitHub.
Eventos de lanzamiento
Por lo general, los eventos que dispararán el flujo de lanzamiento suelen ser los manuales, si estamos ante entrega continua; o bien los de push o pull_request cuando estamos ante despliegue continuo. Ejemplo:
# entrega continua
on:
workflow_dispatch:
inputs:
release:
description: Version type to publish
required: true
type: choice
options: [major, minor, patch]
# despliegue continuo
on:
push:
branches:
- release/v*
tags:
- v*
Versionado con npm
El versionado (versioning) es la operación por la que producimos una nueva versión del software. Usa tanto etiquetas de Git como comandos de npm.
Comando npm version
El comando npm version se utiliza para cambiar la versión de los paquetes a publicar, generalmente, incrementando el valor de alguna de las secciones de la versión. Cuando trabajamos con un monorepo, lo que hace es recorrer los distintos paquetes para incrementar su versión según le hayamos indicado.
Su sintaxis básica es como sigue:
# incrementa el número de versión de los paquetes del monorepo
npm version -ws tipoCambio
# fija la versión indicada en los paquetes del monorepo
npm version -ws númeroDeVersión
# fija la versión usando la última etiqueta de versión
# indicada en Git
npm version -ws from-git
Los tipos de cambio pueden ser: major, minor, patch, premajor, preminor, prepatch y prerelease. Atendiendo a la actualización en el número de versión, indicaremos un tipo de cambio u otro. Por ejemplo, si vamos a publicar una nueva versión que resuelve un bug, usaremos npm version patch; en cambio, si vamos a publicar una versión que añade nueva funcionalidad, usaremos npm version minor. Lo que hace npm version es incrementar el número correspondiente. Con npm version patch, se incrementa el tercer número de la versión; npm version minor, el segundo; y npm version major, el primero. En estos dos últimos casos, los siguientes se pondrán a cero. Así pues, si tenemos 0.0.1 e indicamos npm version minor, la versión se pondrá a 0.1.0.
En ocasiones, puede ser necesario fijar una determinada versión. En estos casos, podemos indicar la versión en cuestión como, por ejemplo, 1.2.3; o bien, si la versión se ha fijado como última etiqueta de Git, mediante from-git.
No olvide usar la opción --workspaces
para que se aplique a todos los paquetes del monorepo.
También puede usar la opción --workspace
si sólo quiere que la actualización se lleve a cabo sobre un determinado paquete.
El comando npm version actualiza los archivos package.json y, si se encuentran presentes, también package-lock.json y npm-shrinkwrap.json. Recordemos que el archivo package-lock.json (package-lock.json file) lo genera automáticamente npm y representa una instalación concreta del paquete, listando todas las dependencias, tanto directas como indirectas, con sus correspondientes versiones instaladas. La idea es garantizar que cuando se instale, se instale con esas mismas versiones para así garantizar que funcionará ya que las pruebas ejecutadas durante la integración continua se realizaron también con esas mismas versiones y todo fue bien. Por seguridad y buenas prácticas, debe generarse siempre y debe formar parte de los archivos subidos a GitHub. Cuando se publica un paquete que dispone de este archivo, se subirá también al registro y, así, garantizará que las instalaciones realizadas usarán las versiones concretas en él indicadas, aunque haya otras compatibles según lo indicado en el archivo package.json.
npm version está muy integrado con Git.
Cada vez que fijamos una nueva versión y usamos la opción --include-workspace-root
para que actualice también la versión del archivo de metadatos del monorepo, automáticamente se realizan varias operaciones en el repositorio:
-
Se realiza una confirmación (commit), indicando como mensaje la versión que se ha fijado como, por ejemplo, 3.2.1.
-
Se registra una etiqueta cuyo identificador será la versión que acabamos de fijar, prefijada con la letra v como, por ejemplo, v3.2.1.
En ocasiones, sobre todo si deseamos agrupar todos los cambios realizados durante la publicación de una nueva versión en un registro como NPM, podemos desear que la confirmación no se realice.
Si este es el caso, hay que usar la opción --no-commit-hooks
.
También es posible indicar que no se genere la etiqueta, en este caso, usaremos la opción --no-git-tag-version
.
Comando npm pkg
En ocasiones, es posible que necesitemos información sobre los metadatos del monorepo o de un paquete para su uso en el flujo de trabajo. Esto se puede hacer fácilmente con el comando npm pkg, con el cual podemos consultar, fijar o suprimir metadatos de un archivo package.json.
Comando npm pkg get
El comando npm pkg get consulta un archivo de metadatos y devuelve información contenida en él. Su sintaxis básica es:
# del archivo de metadatos del directorio actual
npm pkg get campo
# de un paquete del monorepo
npm pkg get -w paquete campo
Por ejemplo, supongamos que estamos usando un monorepo y deseamos extraer el número de versión de su archivo de metadatos para usarlo durante la publicación en el registro de NPM. Si estamos ubicados en el directorio raíz del monorepo, podremos obtener esto como sigue:
$ npm pkg get version
"0.2.0"
Si está trabajando con Bash, recuerde que puede almacenar esta versión de la siguiente forma:
# obtenemos versión del monorepo
$ version=$(npm pkg get version)
# quitamos las comillas dobles
$ version=${version:1:-1}
# mostramos versión
$ echo $version
0.2.0
Podemos usar los operadores de indexación y punto para acceder a propiedades anidadas o indexadas. Ejemplo:
npm pkg get contributors[0].email
Comando npm pkg set
Mediante el comando npm pkg set podemos fijar el valor de una propiedad en un package.json. Sintaxis básica:
# del archivo de metadatos del directorio actual
npm pkg set campo=valor
# de un paquete del monorepo
npm pkg set -w paquete campo=valor
Comando npm pkg delete
Si lo que necesitamos es suprimir una propiedad del archivo de metadatos, usaremos npm pkg delete:
# del archivo de metadatos del directorio actual
npm pkg delete campo
# de un paquete del monorepo
npm pkg delete -w paquete campo
Flujo de publicación
En nuestro caso, el flujo de publicación (publish workflow) no hace más que publicar el lanzamiento en NPM. Si lo desea, puede añadir su funcionalidad al flujo de lanzamiento. Es una cuestión de gustos y probablemente de forma de trabajo de la organización.
Eventos de publicación
Para llevar a cabo una publicación, se suele utilizar el evento release como disparador de este flujo de trabajo. Tras la generación del lanzamiento, será el momento de llevar a cabo su publicación en NPM.
Comando npm publish
Ahora, vamos a ver cómo publicar nuestros paquetes de Node.js en NPM. Hacerlo en otros administradores de paquetes es muy similar.
La publicación se puede realizar con npm publish. Es importante que tenga en cuenta que cuando utilizamos la acción actions/setup-node se crea automáticamente un archivo .npmrc similar a:
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
registry=https://registry.npmjs.org/
always-auth=true
Observe que el token utilizado en los accesos al registro de NPM se espera en la variable de entorno NODE_AUTH_TOKEN. Así pues, no tenemos más que adjuntar su valor en el paso de publicación. He aquí un ejemplo ilustrativo:
- name: Publish packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish -ws
Los secretos deben encontrarse a buen recaudo, no los mantenga escritos en el código ni en ningún sitio al que cualquiera que tenga acceso al repositorio pueda verlo. De ahí, que hayamos ubicado el token en el almacenamiento de secretos proporcionado por GitHub.
Lerna
npm no es perfecto y presenta algunas deficiencias. Una importante es el comando npm version. Cada vez que ejecutamos el comando, actualizamos las versiones de todos los paquetes del monorepo, independientemente de si han sufrido cambios desde la última publicación. Por otra parte, no actualiza las interdependencias. Una interdependencia (interdependency) se produce cuando un paquete del monorepo depende de otro del mismo monorepo. Por ejemplo, supongamos que el monorepo tiene tres paquetes A, B y C, con B dependiendo de A. Lo ideal es que si actualizamos la versión de A, actualicemos también la que tiene B de A, en su package.json. npm version no lo hace y es por eso que algunos, entre los que se encuentra quien escribe, consideramos esto como una carencia importante.
Por suerte, podemos usar Lerna, otra herramienta de monorepo, que resuelve algunos de las carencias de npm.
Importante. Lerna es muy potente y dispone de comandos muy similares a los que hemos visto con los espacios de trabajo de npm. Aquí, no vamos a presentar más que aquellos que resuelven las carencias de los de npm. La razón es que muy probablemente npm mejorará en un futuro no muy lejano y al final estas carencias desaparecerán, no siendo, entonces, necesario el uso de Lerna.
Instalación de Lerna
Lerna se instala mediante su paquete homónimo. Se suele instalar a nivel local en el monorepo, aunque la opción global está disponible también. Personalmente, prefiero la instalación local como dependencia de desarrollo del monorepo.
No hay que instalar Lerna en cada paquete, es decir, no debe de aparecer como dependencia de desarrollo en cada paquete del monorepo. Hay que añadirlo a las dependencias de desarrollo del monorepo, por ejemplo, mediante:
npm i -D lerna
Archivo lerna.json
El monorepo debe contener un archivo lerna.json que contiene la configuración de Lerna para el monorepo. Cuando usamos también los espacios de trabajo de npm, como en nuestro caso, este archivo puede ser tan simple como:
{
"useWorkspaces": true,
"version": "0.0.0"
}
La propiedad useWorkspaces indica si debe integrarse con los espacios de trabajo de npm, en nuestro caso, indicaremos siempre true. Mientras que version, establece la versión actual del monorepo. Lerna usa la versión de su configuración y no la indicada en el archivo de metadatos del monorepo, por lo que podemos omitir esta propiedad del package.json del monorepo.
Comando lerna version
El comando lerna version es muy parecido a npm version y se encarga de hacer lo mismo, pero añade algunos pasos extras como la actualización de las versiones de las interdependencias, razón por la que necesitamos esta herramienta. Además, actualizará la versión de su archivo de configuración, es decir, la propiedad version del archivo lerna.json. Si usamos lerna version, ya no usaremos npm version.
El comando lerna version solicita confirmación al usuario;
esto es de vital importancia si está usando una herramienta de automatización como GitHub Actions.
Para evitar esta solicitud de confirmación, usaremos la opción --yes
.
Lerna también realiza commits y fija etiquetas de manera similar a npm version.
Para indicarle que no lo haga, podemos usar las mismas opciones que con npm version: --no-commit-hooks
y --no-git-tag-version
.
Adicionalmente, hace un push implícito, algo que no hace npm version.
Si deseamos que no lo haga, indicaremos la opción --no-push
.
Con todo esto, he aquí un ejemplo para incrementar el número menor de los paquetes actualizados desde la última publicación y de las interdependencias cuando sea necesario:
lerna version minor --yes --no-commit-hooks --no-git-tag-version --no-push
El comando lerna version dispone de una colección muy rica de opciones.
En ocasiones, atendiendo al proceso de desarrollo que utilice su organización, alguna de ellas puede serle útil.
Le recomiendo que eche un vistazo a las siguientes:
--amend
,
--conventional-commits
,
--conventional-graduate
,
--conventional-prerelease
,
--create-release
,
--preid
y
--tag-version-prefix
.
Comando lerna publish
El comando lerna publish recorre los paquetes del monorepo y realiza una publicación de aquellos que han sufrido cambios desde la última publicación. Debemos ejecutarlo después de haber fijado las versiones de los paquetes; si no están actualizadas, lerna publish hará un lerna version previo. Se recomienda actualizar las versiones antes.
Su sintaxis más básica, que consiste en publicar los paquetes que han sufrido cambios desde la última publicación, es como sigue:
lerna publish from-package --yes
La opción --yes
se utiliza para que no pida confirmación de la publicación.
Se utiliza principalmente en procesos automatizados como, por ejemplo, en GitHub Actions.
Eche un vistazo a las opciones del comando porque, en algunos proyectos, alguna de ellas puede resultarle útil.
Cuando publique paquetes con ámbito (scoped packages) usando Lerna, no olvide que si son públicos deben tener la propiedad publishConfig.access del archivo package.json a public.
Ejemplo de flujo de lanzamiento y publicación
Tal y como le indiqué anteriormente, es posible que las organizaciones incluyan las tareas de lanzamiento y publicación en el mismo flujo. Aquí vamos a presentar uno de estos casos. Separarlo en dos no le resultará nada difícil. Lo importante es que sepa qué debe hacer y qué eventos debe utilizar para disparar el flujo.
Es muy útil definir un flujo reutilizable, el cual invocar desde el flujo de lanzamiento del monorepo. De esta forma, si el repositorio no añade nada excepcional, podrá hacer modificaciones en el flujo reutilizable sin necesidad de actualizar los flujos de los distintos monorepos del proyecto. Por ejemplo, el proyecto Akromio dispone de varios flujos reutilizables para los monorepos de Node.js. Entre ellos, se encuentra nodejs-release.yaml, el cual puede consultar en https://github.com/akromio/.github/blob/master/.github/workflows/nodejs-release.yaml:
name: Release workflow (Node.js)
on:
workflow_call:
inputs:
release:
description: Version type to publish (major, minor, patch...)
required: true
type: string
secrets:
NPM_TOKEN:
description: Token to use for authN on NPM.
required: true
env:
version: ${{ inputs.release }}
jobs:
publish:
name: Publish packages
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: [18.x]
os: [ubuntu-latest]
steps:
- name: Clone repo
uses: actions/checkout@v3
- name: Set Git metadata
run: |
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
- name: Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: npm ci -ws --include-workspace-root
- name: Set version to publish
run: npm exec lerna -- version --yes --no-push ${{ env.version }}
- name: Publish packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm exec lerna -- publish from-package --yes
- name: Push changes
run: |
git push --force
git push origin --tags
Y en cada monorepo del proyecto, podemos ver un flujo de lanzamiento como el siguiente:
name: Publish packages on NPM
on:
workflow_dispatch:
inputs:
release:
description: Version type to publish
required: true
type: choice
options: [major, minor, patch]
jobs:
publish:
uses: akromio/.github/.github/workflows/nodejs-release.yaml@master
with:
release: ${{ inputs.release }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Por seguridad y buenas prácticas, cuando se instalan las dependencias antes de su publicación, se recomienda el uso del comando npm ci en vez de npm i. Así, se garantiza que se publica lo probado en la integración continua.