La Entrega Continua (Continuous Delivery, CD) concluye el ciclo de ingeniería transformando el código fuente verificado en un artefacto inmutable listo para producción (Immutable Artifact). En plataformas cloud distribuidas y clústeres de Kubernetes, el formato estándar de empaquetado es la imagen de contenedor bajo la especificación OCI (Open Container Initiative).
La automatización de lanzamientos a nivel empresarial se apoya en cinco fundamentos de ingeniería:
semantic-release analiza el historial de Git conforme a la convención Conventional Commits mediante el árbol de sintaxis abstracta (AST) de los mensajes:
fix(...): incrementa la versión PATCH (corrección de errores sin alterar la API);feat(...): incrementa la versión MINOR (incorporación de funcionalidad retrocompatible);feat(...)! o el pie BREAKING CHANGE:: incrementa la versión MAJOR (cambios que rompen contratos de interfaz o arquitectura).verifyConditions: verifica permisos del token GITHUB_TOKEN y credenciales de acceso a registros;analyzeCommits: clasifica los cambios producidos desde la etiqueta (Git tag) previa;generateNotes: genera un registro automatizado de cambios en Markdown (CHANGELOG.md);prepare: actualiza la versión en package.json y genera un commit en el repositorio con la marca [skip ci];publish: crea una publicación oficial en GitHub (Release) con artefactos binarios y una etiqueta Git inmutable (vX.Y.Z).type=gha): La compilación de contenedores en runners efímeros carece por defecto de caché local en el demonio de Docker. El uso de cache-from=type=gha y cache-to=type=gha,mode=max descarga y almacena las capas intermedias en la GitHub Actions Cache API, reduciendo los tiempos de reconstrucción de 5 minutos a 20 segundos.ghcr.io, autenticado mediante GITHUB_TOKEN) del registro público o comercial (Docker Hub docker.io). Las imágenes se publican en paralelo con etiquetado determinista e inmutable: Git SHA (sha-a1b2c3d), versión SemVer exacta (1.4.2), etiqueta mayor flotante (v1) y latest.La interacción entre el motor de análisis de commits, el compilador Buildx y los registros de imágenes distribuidos:
[ Merge del PR en la rama de release: main ]
|
v
+-----------------------------+
| GitHub Actions CD Job |
| (VM Efímera: ubuntu-latest) |
+-----------------------------+
|
v
+-------------------------------------------------------------------+
| 1. Ciclo de Vida de Semantic Release |
| AST Parse Commits: [feat, fix, perf, BREAKING CHANGE] |
| ├── Cálculo de versión: 1.3.4 -> 1.4.0 (Minor Bump) |
| ├── Generación del artefacto CHANGELOG.md |
| ├── Creación de Git Tag: v1.4.0 |
| └── Exportación a variables: NEW_VERSION=1.4.0 |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 2. Inicialización de Docker Buildx y QEMU |
| Plataformas: linux/amd64 + linux/arm64 |
| Restauración de caché de capas: actions/cache (type=gha) |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 3. Autenticación en Registros de Contenedores |
| ├── ghcr.io (GITHUB_TOKEN) |
| └── docker.io (DOCKERHUB_USERNAME / DOCKERHUB_TOKEN) |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 4. Compilación Multi-Arquitectura y Envío (Push) |
| Etiquetas: |
| ├── ghcr.io/org/app:1.4.0 |
| ├── ghcr.io/org/app:sha-7f9e2b1 |
| ├── ghcr.io/org/app:latest |
| └── docker.io/company/app:1.4.0 |
| Publicación de OCI Image Manifest List (Fat Manifest) |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 5. Publicación de GitHub Release y Webhook a Orquestador GitOps |
| (ArgoCD / FluxCD / Sincronización en Kubernetes) |
+-------------------------------------------------------------------+output: 'standalone'). La imagen se etiqueta con el SHA del commit y se envía al GHCR privado. Posteriormente, el operador GitOps ArgoCD detecta la actualización y concilia el despliegue en K8s.cosign en Docker Hub.La entrega continua se articula mediante la integración de .releaserc.json, un Dockerfile multi-etapa optimizado y un workflow de GitHub Actions.
El archivo .releaserc.json declara los plugins y las reglas de incremento de versiones:
{
"branches": [
"main",
{ "name": "beta", "prerelease": true },
{ "name": "next", "prerelease": true }
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "refactor", "release": "patch" },
{ "type": "chore", "release": false },
{ "breaking": true, "release": "major" }
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
]
}El texto [skip ci] en el mensaje de commit de @semantic-release/git es imprescindible: impide que GitHub Actions interprete la actualización del changelog como un nuevo push y entre en un bucle infinito de ejecución.
Para construir un contenedor de tamaño mínimo que opere bajo un usuario sin privilegios administrativos (non-root):
# -------------------------------------------------------------
# Etapa 1: Imagen base y gestor de paquetes
# -------------------------------------------------------------
FROM node:22-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare [email protected] --activate
# -------------------------------------------------------------
# Etapa 2: Instalación de dependencias
# -------------------------------------------------------------
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prefer-offline
# -------------------------------------------------------------
# Etapa 3: Compilación de la aplicación
# -------------------------------------------------------------
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# Compilación de Next.js en modo output: 'standalone'
RUN pnpm build
# -------------------------------------------------------------
# Etapa 4: Contenedor mínimo de ejecución (Runner)
# -------------------------------------------------------------
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Seguridad: ejecución bajo usuario sin privilegios
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copia de archivos estáticos públicos
COPY --from=builder /app/public ./public
# Copia del bundle autónomo y fragmentos compilados
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]Ejecución de semantic-release, cálculo del incremento de versión, redacción de CHANGELOG.md y creación del tag vX.Y.Z.
Aprovisionamiento de emuladores QEMU e inicio de una instancia BuildKit con soporte multi-arquitectura.
Inicio de sesión simultáneo en GitHub Container Registry (ghcr.io) y Docker Hub mediante tokens seguros.
Construcción paralela para linux/amd64 y linux/arm64 reutilizando capas mediante la caché de GitHub Actions (type=gha).
Envío del índice de manifiestos a todos los registros con etiquetas inmutables de SemVer, Git SHA y latest.
A continuación se muestra el workflow .github/workflows/release-and-publish.yml que ejecuta el versionado automático, generación de notas de lanzamiento y publicación de imágenes multiplataforma.
name: Continuous Delivery & Release
on:
push:
branches:
- main
concurrency:
group: cd-release-production
cancel-in-progress: false
permissions:
contents: write
packages: write
issues: write
pull-requests: write
env:
REGISTRY_GHCR: ghcr.io
IMAGE_NAME_GHCR: ${{ github.repository }}
REGISTRY_DOCKERHUB: docker.io
IMAGE_NAME_DOCKERHUB: company/nextjs-storefront
jobs:
# -------------------------------------------------------------
# Fase 1: Versionado semántico y creación de la release
# -------------------------------------------------------------
release:
name: Semantic Release & Versioning
runs-on: ubuntu-latest
outputs:
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
new_release_version: ${{ steps.semantic.outputs.new_release_version }}
new_release_git_tag: ${{ steps.semantic.outputs.new_release_git_tag }}
steps:
- name: Checkout del código con historial completo
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configurar Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Instalar dependencias de Semantic Release
run: |
npm install -g \
semantic-release \
@semantic-release/commit-analyzer \
@semantic-release/release-notes-generator \
@semantic-release/changelog \
@semantic-release/git \
@semantic-release/github \
conventional-changelog-conventionalcommits
- name: Ejecutar Semantic Release
id: semantic
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
# -------------------------------------------------------------
# Fase 2: Compilación y publicación del contenedor Multi-Arch
# -------------------------------------------------------------
publish-container:
name: Build & Push Multi-Arch Container
needs: [release]
if: needs.release.outputs.new_release_published == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout del repositorio
uses: actions/checkout@v4
- name: Configurar emuladores QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Inicializar Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Iniciar sesión en GitHub Container Registry (GHCR)
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY_GHCR }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Iniciar sesión en Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY_DOCKERHUB }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extraer metadatos y generar etiquetas Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME_GHCR }}
${{ env.REGISTRY_DOCKERHUB }}/${{ env.IMAGE_NAME_DOCKERHUB }}
tags: |
type=raw,value=${{ needs.release.outputs.new_release_version }}
type=raw,value=latest
type=sha,format=short,prefix=sha-
- name: Compilar y publicar imagen OCI multi-arquitectura
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxEl plugin @semantic-release/git hace commit del archivo CHANGELOG.md y de package.json en la rama main. Si la plantilla de mensaje de commit no incluye instrucciones explícitas para omitir el CI, GitHub Actions interpreta este cambio como un nuevo push externo y relanza el workflow, generando un bucle infinito que agota los minutos de cómputo del plan.
// INCORRECTO: Mensaje de commit sin instrucción de omisión de CI
{
"plugins": [
["@semantic-release/git", { "message": "release: update changelog" }]
]
}
// CORRECTO: Inclusión de [skip ci] en el mensaje de commit
{
"plugins": [
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}Desplegar imágenes usando únicamente la etiqueta mutable :latest sin fijar la versión SemVer (1.4.2) o el commit SHA (sha-7a1b8c). Ante un incidente crítico en producción, el orquestador no puede ejecutar un rollback determinista debido a que la etiqueta :latest ya fue sobreescrita por la compilación defectuosa.
# INCORRECTO: Uso exclusivo de la etiqueta mutable latest
tags: my-org/app:latest
# CORRECTO: Combinación de etiquetas inmutables junto con etiquetas flotantes
tags: |
type=raw,value=${{ needs.release.outputs.new_release_version }}
type=sha,format=short,prefix=sha-
type=raw,value=latestCompilar para linux/arm64 en runners estándar x86 ejecuta la instrucción RUN pnpm build mediante emulación de instrucciones de CPU a través de QEMU. La emulación ralentiza la compilación de JavaScript y Rust entre 5 y 10 veces (el proceso toma 25 minutos en lugar de 2). La solución eficiente radica en emplear runners ARM nativos en GitHub Actions (runs-on: ubuntu-24.04-arm) o compilar en el host antes de copiar el bundle al contenedor.
Declarar variables mediante ARG NPM_TOKEN e invocar RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc. Aunque el archivo .npmrc se elimine en una instrucción posterior, el valor del token queda expuesto de forma permanente en los metadatos de las capas intermedias, accesible mediante docker history.
# INCORRECTO: El secreto queda registrado en los metadatos de capa
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
# CORRECTO: Montaje de secreto efímero con BuildKit sin persistencia en capas
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc pnpm install --frozen-lockfileImplementar commitlint y Husky para hacer cumplir la convención Conventional Commits a todo el equipo.
Configurar output: standalone en Next.js, multi-stage builds, ejecución non-root y separación de capas.
Definir .releaserc.json para el cálculo automático de SemVer, actualización de CHANGELOG.md y creación de tags Git.
Habilitar emuladores QEMU y compilar para linux/amd64 y linux/arm64 aprovechando la caché GHA.
Autenticar en GHCR y Docker Hub, emitir OCI Image Manifest Lists e integrar con pipelines GitOps.