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:

  1. Clonar el repositorio de trabajo.

  2. Generar una nueva versión.

  3. 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.