Versionado Semántico – SemVer

Versionado Semántico – SemVer

English version

Descripción simple

Si necesitas saber todos los detalles de la especificación SemVer, por favor visita el website de la especificación, aquí solamente discutiremos en forma breve los puntos principales y algunas aplicaciones prácticas.

2.3.8-alpha3.1
^ ^ ^   ^
| | |   |
| | |   --> Prelanzamiento
| | --> Parche
| --> Menor
--> Mayor
  • Usa Parche para correción de errores, 100% compatible con la versión anterior
  • Usa Menor para agregar nueva funcionalidad sin romper compatibilidad
  • Usa Mayor para introducir cambios importantes que tienen el potencial de romper compatibilidad con versiones anteriores
  • Usa Prelanzamiento durante el desarrollo de una nueva versión

Mejores prácticas en el uso de Versionado Semántico

Única Fuente de Verdad

Es de suprema importancia en el desarrollo de software que hay una sola fuente de verdad para todo el proceso, y esto es especialmente importante con respecto a la versión. No debe haber ninguna duda sobre qué versión del código fuente fue usado para construir cada pieza de software. Escoja una fuente de verdad para la versión del paquete y asegúrese que esa versión se refleje en todo el proceso.

Sincronización automática

Una vez escogida la fuente única de la versión, busca herramientas que te permitan automatizar la mayor parte de la sincronización de la versión en las diferentes etapas del proceso de desarrollo del software (SDLC). Es inevitable que la versión esté presente en diferentes formas y lugares del proceso, por eso es necesario que, en lo posible, el proceso por medio del cual la versión se propaga a estas diferentes etapas y lugares sea automático para prevenir un desfase.

Por ejemplo, la versión aparece en un archivo meta del paquete en forma de texto, también en un tag del repositorio y en la metadescripción del paquete, y potencialmente en varios otros lugares en el camino.

Versionado intencional

No caigas en la tentación de la actualización automática de la versión. Es muy fácil para los desarrolladores crear una utilidad simple que actualiza la versión cada vez que hacemos un cambio en el código, o cada vez que compilamos la aplicación. Y, si bien es cierto, eso nos ahorra unos milisegundos, va en contra de las mejores prácticas de la versión semántica, donde cada cambio a la versión es intencional y calculado de tal manera que la versión refleje los cambios en la aplicación.

Rechaza reescribir involuntariamente la versión

Para evitar confusiones, es preferible que una versión no pueda usarse dos veces, es mejor si hay alguna forma de impedirlo por defecto. Sistemas como el administrador de paquetes de npm rechazan un segundo paquete con la misma versión, pero permiten borrar la versión actual para volverla a escribir en caso que sea absolutamente necesario usar una versión que se usó por error. Escoge herramientas que te permitan en algún punto de la canalización detectar y rechazar dos diferentes paquetes con una misma versión.

SemVer en herramientas específicas

NPM

Versionando tu paquete o aplicación

NPM es el administrador de paquetes de NodeJS y tiene soporte nativo para SemVer, es decir, no se necesita instalar una herramienta aparte para todo el manejo de Versionado Semántico en tu paquete, aunque, hay herramientas para uso avanzado en caso sea necesario. Creo que bastará mostrar un ejemplo para documentar lo fácil que es hacer versionado semántico en NPM.

# empieza a crear un nuevo paquete 
# (tendrás que contestar varias preguntas,
# como nombre de la persona que mantendrá el paquete, descripción, etc.)
npm init
# ahora verás que el folder tiene un archivo llamado package.json
# dentro podrás ver que tienen un campo version.
# ahora incrementa la versión de paquete
npm version patch
# verifica en package.json y verás que tu paquete ahora es versión 1.0.1
# si estás trabajando en una versión privada puedes incrementar la versión pre-release
npm version prerelease
# version es ahora 1.0.2-0
# digamos que continuas trabajando en esta version privada
npm version prerelease
# version es ahora 1.0.2-1
# cuando estás listo para publicar esta version
npm version patch
# version es ahora 1.0.2 (nota que ya estabas en 1.0.2, pero ha removido el prerelease)
# ahora puedes publicar esta versión
# debes usar los conceptos de SemVer, es decir 
# patch es para arreglar problemas
npm version minor
# ahora version es 1.1.0, que quiere decir que has agregado nueva funcionalidad
npm version major
# ahora version es 2.0.0, que quiere decir que has cambiado algo que podría romper
# la funcionalidad de instalaciones existentes

Versionando tus dependencias

Como viste, es facilísimo usar SemVer cuando las herramientas que usas lo soportan en forma nativa. Pero, también es necesario usar los conceptos de SemVer cuando tu aplicación o paquete depende de otros paquetes; NPM también maneja esto nativamente. Por ejemplo,

# Digamos que en tu paquete necesitas analizar un archivo swagger
# NO DEBES escribir tu propio analizador :) 
# Hay un analizador llamado swagger-parser,
# quieres instalarlo no te importa la versión
npm install swagger-parser
+ swagger-parser@10.0.3
# instalará la versión más reciente 10.0.3
# pero, la librería ha cambiado desde la última vez
# que la usaste y prefieres usar la versión anterior
# Si sabes la versión exacta, podrás instalarla así
npm install swagger-parser@2.0.0
+ swagger-parser@2.0.0
# si prefieres la version menor 2.0,
# pero no sabes cuál es la última variación 
# de esa versión, puedes usar la tilde ~
npm install swagger-parser@~2.0.0
+ swagger-parser@2.0.1
# si eres aventuraro(a) pero no quieres
# que una nueva versión rompar compatibilidad
# con tu paquete, entonces puedes
# pedir la última versión menor
# usando la careta o sombrero ^
npm install swagger-parser@^2.0.0
+ swagger-parser@2.5.0

Otro aspecto de un administrador de paquetes maduro es la creación de un lock. Es decir, si bien es cierto el desarrollador quiere cierto nivel de flexibilidad en la versión del paquete que recibe, el operador de la aplicación querrá un gran nivel de estabilidad en la aplicación que mantiene en producción. Para lograr este balance, los administradores de paquetes como NPM crean un archivo paralelo a package.json en cual guardan la versión específica de cada una de las dependencias que fueron instaladas. Esto es clave para que, al construir la versión que correrá en producción, esta contenga las mismas versiones exactas que el desarrollador usó durante el desarrollo y que fueron usadas durante la pruebas de unidad e integración. De lo contrario, los operadores no podrían tener confianza que lo que ponen en producción es idéntico a lo que fue la intención original.

NPM tiene una ventaja adicional y es que cuando usas el administrador de npm para versionar tu paquete o aplicación, automáticamente te agrega una etiqueta de git a tu repositorio, lo veremos en acción en la siguiente sección.

Git

Las etiquetas de git no son estrictamente compatibles con SemVer, es decir, uno puede asignar cualquier palabra como etiqueta (aunque hay reglas muy estrictas con referencia a qué caractéres son válidos). Entonces, aunque casi cualquier palabra es válida como una etiqueta de git, la mayor parte de los equipos de desarrollo de software usan convenciones para equiparar la etiqueta con la version. La convención más usada es prefijar la versión semántica con la letra v; así, la versión semántica 2.3.8 será etiquetada en git como ‘v2.3.8’. Nuevamente, no hay reglas estrictas, pero es importante que todo el equipo acepte una nomenclatura estándar por convención.

Todos los sistemas modernos de desarrollo de software soportan versionado semántico en el manejo de paquetes y es importante mantener la sincronización de etiquetas de git en paralelo con las del paquete mismo. Pero, debes recordar que las etiquetas de git no son reusables (aún cuando se pueden borrar y recrear, esto no es recomendable), y por lo tanto debes etiquetar cuando la versión está lista para ser promovida.

Mi recomendación es que la fuente de verdad sea la versión en el paquete y que se sincronize git manual o automáticamente. Usa SemVer estricto en el versionado del paquete y la convención actual es agregar la ‘v’ a la etiqueta del git.

# en cuanto vas a trabajar en algo nuevo, crea una rama
git checkout main
git pull
git checkout -b 25-mi-nueva-funcionalidad
# también es recomendable crear el semver temporal del paquete al mismo tiempo
# versión actual es 3.1.8
npm version prerelease
# el output será algo como v3.1.9-0
# ahora puedes trabajar en arreglar o implementar
# codificar...
# lint
# unit test
# durante la implementación se recomienda 
# agregar los archivos y hacer commit multiples veces diarias
# también es recomendable que cada vez que se empuje el código
# al repositorio remoto, se actualize la versión prerelease
# npm version prerelease
# 3.1.9-1, 3.1.9-2, etc.
npm version prerelease
git add --all
git commit -m 'agregué algo'
# la primera vez deberás indicar a qué rama remota enviar
git push -u origin 25-mi-nueva-funcionalidad
# una vez que el código ha sido implementado, validado y probado
# entonces se crea la versión apropiada (patch,minor or major)
npm version patch
# ahora la versión será 3.1.9, nótese que al crear el patch remueve el segmento de prerelease
# también es importante notar que, en el caso de npm 
# y otros administradores de paquetes modernos, 
# si usas las herrramientas de versión, 
# automáticamente se crearán las etiquetas en git
git add --all
git commit -m 'Fixes #25 - Mi nueva funcionalidad'
# El argumento --follow-tags instruye a git de enviar todo, incluyendo las etiquetas al sistema remoto
git push --follow-tags
# si tu empresa no usa algún proceso para hacer revisión de código por pares y la combinación de las ramas
# entonces las siguientes líneas se invocan localmente
git checkout main
git pull
git merge 25-mi-nueva-funcionalidad
git push
# independientemente de si se combina en el sistema remoto o local
# una vez combinado, por limpieza, debes borrar la rama local
git branch -D 25-mi-nueva-funcionalidad
# NUNCA reuses una rama ya combinada
# si estás usando el GitLab flow que yo recomiendo
# siempre empieza todo cambio de main y combínalo de regreso a main
# y si no, lee sobre el Git flow, el GitHub flow o el GitLab flow y escoge un flow
# NO INVENTES tu propio flow, te traerá muchas complicaciones tarde o temprano

Para asegurarte que las versiones semánticas del paquete no se contradigan con las etiquetas de git, puedes tomar dos caminos: automatizar las etiquetas de git cada vez que cambies la versión en el paquete, o automatizar la versión del paquete cada vez que etiquetes git. Ambos caminos tienen sus ventajas y desventajas, pero es mucho mejor cuando la tecnología ya la ha implementado por ti. Por ejemplo, en el caso anterior, podrás notar que nunca se hizo un git tag, y la razón es porque el comando npm version ... se encarga de sincronizar la versión con una etiqueta en git. Esta es una excelente evolución de las herramientas de desarrollo, anteriormente, para mantener los dos sincronizados tenías que escribir código en los ganchos de git, o de la herramienta (prerelease, postversion, etc.).

Docker

Versionando tus imágenes

Versionado con docker es un poquito más complicado, puesto que Dockerfile (el archivo meta en texto que describe a un contenedor) no tiene un campo específico de versión. Las posibilidades son usar un “label” (campo soportado por Docker) para poner estampar una versión. Pero, siendo que un campo específico de versión no es soportado nativamente por el ambiente docker, este es un caso en el que recomiendo que la única fuente de verdad con respecto a la versión resida fuera del archivo, y qué mejor que en la etiqueta de git. Sin embargo, como no hay una sincronización automática de la versión con la etiqueta, debemos asegurarnos que nuestra canalización se encargue de imponer tal restricción. Además, Docker agrega un pequeño cambio al escricto Versionado Semántico.

Según SemVer, cada artefacto diferente debe tener una versión mayor, menor y parche diferente, así, la primera versión en se publicada será 1.0.0, el siguiente parche 1.0.1 y cuando se agregue nueva funcionalidad será 1.1.0. Simple, pero Docker agrega que al publicar 1.0.0, debe ser etiquetado como 1, 1.0, y 1.0.0, cuando el primer parche cree la versión 1.0.1, debe ser ahora también etiquetada como 1 y 1.0. Esta práctica, aunque aceptada en el ambiente docker no es 100% compatible con SemVer puesto que la versión 1.0 de ayer tiene la posibilidad de ser diferente de la versión 1.0 de hoy, aún así, es la práctica aceptada y hay que usarla.

FechaEventoEtiqueta MayorEtiqueta MenorEtiqueta SemVerEtiqueta “latest”
2022-01-01Primera versión11.01.0.0latest
2022-01-02Pequeño arreglo11.01.0.1latest
2022-01-03Nueva funcionalidad11.11.1.0latest
En el caso de Docker cada imagen debe recibir múltiple etiquetas, la versión del 2022-01-03 tendrá todas “1”, “1.1”, “1.1.0” y “latest”

Para cumplir con SemVer y al mismo tiempo con las mejores prácticas de docker, el mejor lugar para generar todas estas etiquetas es la canalización. Pero, es necesario asegurarse que la empresa acuerde usar un proceso estricto y documentado para evitar confusión.

Mi recomendación es que la etiqueta interna sea “development” por defecto, pero que pueda modificarse durante la construcción de la imagen. Además, en caso que sea necesario conocer la version de la imagen dentro del contenedor, se puede ponerla en una variable del ambiente (ENV).

# En el archivo Dockerfile
ARG IMAGE_VERSION="development"
FROM alpine:latest
ENV IMAGE_VERSION=$VERSION
LABEL version="development"

# A la hora de construir
IMAGE_VERSION=$(git describe --abbrev=0)
docker build . --build-arg IMAGE_VERSION=$IMAGE_VERSION --label version="$IMAGE_VERSION" -t myregistry/myimage:$IMAGE_VERSION

Nótese que al construir la imagen, se utiliza la última etiqueta de git para construir la imágen y se la pone en tres lugares:

  • Como argumento (–build-arg), lo cual será utilizado para crear una variable de ambiente que pueda ser consumida desde dentro del contenedor
  • Como etiqueta (–label), lo cual quedará grabado como información “meta” del contenedor
  • Como etiqueta (-t), lo cual servirá para regitrarlo en el registro de imágenes correspondiente

Versionando las imágenes de las que tú dependes

Durante la construcción de tu imagen, vas a tener que usar tus propias imágenes o imágenes de terceros; es importante que uses bien tus etiquetas para asegurarte que tu aplicación la imagen usada sea la correcta. Como vimos anteriormente, docker recomienda que cada nueva versión sea etiquetada como latest, pero eso puede ser una bendición o una maldición, dependiendo de tus necesidades y circunstancias. Al usar latest, tienes la seguridad de siempre estar usando la última versión, es decir, sin esfuerzo de tu parte, tu aplicación siempre estará al día, esa es la parte buena, sin embargo, como imaginarás, también estarás recibiendo todos los nuevos errores que se han introducido en las nuevas versiones. Para aplicaciones que corran en producción, es necesario tener estabilidad y por lo tanto, no es recomendable que tu aplicación use la etiqueta latest. Pero, si has creado una canalización madura, con suficientes puertas para verificar que tu aplicación funciona al 100% antes de ser puesta en producción, entonces está bien usar latest, pero con mucho cuidado. A la larga, probablemente usar latest te traerá más dolores de cabeza que bendiciones y por eso no es muy recomendable.

Al usar imágenes base para tu imagen, asegúrate de usar etiquetas específicas en tu línea FROM para evitar sorpresas, o por lo menos que no varíen la versión mayor.

FROM nginx:1.21.5
FROM nginx:1.21
FROM nginx:1

FROM nginx
FROM nginx:latest

Cuando uses tus propias imágenes o imágenes de terceros en el despliegue de tu aplicación (compose, helm, etc.), usa etiquetas específicas.

spec:
  containers:
  - name: nginx
    image: nginx:1.21.5
  - name: myapp
    image: registry.company.com/myapp:3.4.6

Canalización (pipelining)

Es extremadamente importante que los artefactos creados durante la canalización sean estampados con la misma versión que acompaña a los archivos de texto (código fuente) que generaron el artefacto. Debe hacerse un esfuerzo muy grande que todo esto suceda en forma automática y se evite la actualizaciones manuales una vez que la versión ha sido actualizada en el código fuente. Por ejemplo, si una aplicación escrita en NodeJS va a ser publicada en un contenedor, hay una tentación muy grande de mantener dos versiones, una dentro de la aplicación y otra para el despliegue. Sin embargo, estas dos son la mismo, deberían mantenerse en paralelo y nunca deberían perder el sincronismo. Así, si hay un problema en producción, y la versión en producción es 1.3.5, yo sé exactamente qué versión del código generó ese contenedor. Veamos, hay varios lugares que deben mantenerse en sincronismo:

  • package.json – única fuente de verdad
  • contenedor – al construirse este contenedor, debe reflejar la misma versión de la aplicación instalada en él
  • despliegue – (e.g helm) Al desplegar la aplicación usando plantillas, es necesario pasar la versión del contenedor a la plantilla

La canalización debe leer la versión de la aplicación del archivo meta de la aplicación y usarla en todos los demás lugares. A veces esto es difícil porque las canalizaciones tienden a estar segregadas en múltiples pasos y no todos los pasos tienen acceso al archivo meta de la aplicación original. En esos casos, debe haber un paso inicial que extraiga la versión del archivo meta y que la pase como variable de ambiente a los demás pasos de la canalización. Por ejemplo, en GitLab CI se crea un artefacto con un archive .env para pasar la información a los demás pasos de la canalización:

setup:
  script:
    - echo "APP_VERSION="$(node -p require('./package.json').version") >> build.env
  artifacts:
    reports:
      dotenv: build.env

construir:
  dependencies:
    - setup
  script:
    - docker build . --build-arg IMAGE_VERSION=$APP_VERSION --label version="$APP_VERSION" -t myregistry/myapp:$APP_VERSION

desplegar:
  dependencies:
    - setup
  script:
    - RELEASE_NAME=myapp_$APP_VERSION
    - helm upgrade --set image=$CI_REGISTRY_IMAGE --set tag=$APP_VERSION --install $RELEASE_NAME chartmuseum/myapp