Release por tag
Al final de este módulo vas a haber publicado una versión en producción, demostrado que un push a una rama no puede llegar ahí, y revertido en menos de un minuto.
Por qué un tag y no una rama
Sección titulada «Por qué un tag y no una rama»Una rama es un puntero que se mueve. Un tag es una etiqueta inmutable sobre un commit concreto.
Eso importa cuando alguien pregunta qué hay en producción. Con despliegue por rama la
respuesta es «lo que estaba en main cuando corrió el pipeline», que ya no se puede
saber con certeza. Con despliegue por tag la respuesta es v1.4.2, y ese tag apunta al
mismo commit para siempre.
Y hay una segunda razón, la que convierte esto en una decisión de seguridad y no de estilo: se puede exigir en IAM. Eso es lo que vas a comprobar en el paso 9.4.
Paso 9.1 · Registrar las variables de producción
Sección titulada «Paso 9.1 · Registrar las variables de producción»Objetivo: que el entorno prod de GitHub sepa a dónde desplegar.
Junta los datos:
cd taskflow-p01-inframake bootstrap-output # deploy_role_prod_arnmake output ENV=prod # bucket_name, distribution_id, site_urlY regístralos:
cd ../taskflow-p01-appmake shellgh variable set AWS_REGION --env prod --body "us-east-1"gh variable set AWS_DEPLOY_ROLE --env prod --body "arn:aws:iam::123456789012:role/taskflow-p01-deploy-prod"gh variable set AWS_S3_BUCKET --env prod --body "taskflow-p01-prod-123456789012"gh variable set AWS_CLOUDFRONT_ID --env prod --body "E0987654321XYZ"gh variable set SITE_URL --env prod --body "https://p01.workshop.v0x.site"
gh variable list --env prodQué deberías ver: cinco variables listadas. Verifica que el bucket sea el de prod y no el de dev: es el error más fácil de cometer aquí.
Paso 9.2 · Publicar tu primera versión
Sección titulada «Paso 9.2 · Publicar tu primera versión»Objetivo: un tag que dispare producción.
git switch maingit pullgit tag -a v1.0.0 -m "Primera versión en producción"git push origin v1.0.0Y observa la ejecución:
gh run watchQué deberías ver: el workflow arranca y se detiene esperando aprobación, porque
activaste Required reviewers en el entorno prod. Apruébalo desde la interfaz de
GitHub y el despliegue continúa.
Paso 9.3 · Verificar que la versión viajó dentro del artefacto
Sección titulada «Paso 9.3 · Verificar que la versión viajó dentro del artefacto»Objetivo: confirmar que el sitio sabe qué versión es.
Abre tu subdominio y ve a /acerca.
Qué deberías ver: v1.0.0 y el commit corto. Si dice dev y local, estás viendo
un build local o el despliegue no llegó.
Eso funciona porque el workflow le pasa el tag a Vite:
env: APP_VERSION: ${{ github.ref_name }} APP_COMMIT: ${{ github.sha }}Y hay una comprobación en el workflow que vale su peso en oro:
if ! grep -rq "$VERSION" dist/assets/*.js; then echo "::error::La versión $VERSION no aparece en el bundle." exit 1fiSi alguien rompe la inyección de versión, el pipeline falla ahí en lugar de publicar un artefacto que miente sobre lo que es.
Paso 9.4 · Comprobar que hoy producción está abierta
Sección titulada «Paso 9.4 · Comprobar que hoy producción está abierta»Objetivo: ver el problema antes de arreglarlo.
Tu rol de producción todavía no exige que el despliegue venga de un tag: esa condición está comentada en el bootstrap. Vamos a comprobar qué significa eso.
cd taskflow-p01-appmake shellgh workflow run release.yml --ref mainexitgh run watchQué deberías ver: el workflow corre desde main, sin ningún tag, pide aprobación,
y despliega a producción. Con APP_VERSION valiendo main.
Eso es exactamente lo que no queremos. Y fíjate en que el archivo del workflow no tiene nada de malo: solo permite la ejecución manual, que es una funcionalidad razonable.
Paso 9.5 · Poner la cerradura
Sección titulada «Paso 9.5 · Poner la cerradura»Objetivo: que AWS rechace lo que acabas de hacer.
Abre el bootstrap y busca el marcador PASO 9.5:
cd ../taskflow-p01-infra # condition { # test = "StringLike" # variable = "${local.oidc_host}:ref" # values = [var.tag_pattern] # }
condition { test = "StringLike" variable = "${local.oidc_host}:ref" values = [var.tag_pattern] }
condition { test = "StringEquals" variable = "${local.oidc_host}:ref_type" values = ["tag"] }Descomenta las dos condiciones y aplica:
make bootstrap-applyQué deberías ver: Terraform modifica la política de confianza del rol de producción. Un solo recurso cambiado.
Y ahora repite el intento
Sección titulada «Y ahora repite el intento»cd ../taskflow-p01-appmake shellgh workflow run release.yml --ref mainexitgh run watchQué deberías ver: el workflow arranca, GitHub emite el token, y el paso
Autenticar contra AWS falla con un error de STS: no confía en ese ref.
No fue el YAML el que lo impidió. Fue IAM.
Comprueba además que el tag sí sigue funcionando:
git tag -a v1.0.1 -m "Verificar que el tag sigue desplegando"git push origin v1.0.1Debe desplegar con normalidad, tras la aprobación.
Ver el workflow de release completo
name: Release a producción
# Producción se despliega SOLO desde un tag de versión.## No es una convención amable: el rol de AWS tiene una condición sobre el claim# `ref` del token OIDC que exige refs/tags/v*. Si alguien cambiara este# disparador por una rama, STS rechazaría la asunción del rol y el despliegue# fallaría. La política de seguridad no depende de este archivo.## Nota: este workflow ya viene completo, con las cabeceras de caché y la# invalidación que tú agregas a deploy-dev.yml en el módulo 8. La idea es que# aprendas el patrón en desarrollo y producción lo tenga bien desde el inicio.on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" - "v[0-9]+.[0-9]+.[0-9]+-*"
# Permitir la ejecución manual es deliberado, y sirve para demostrar el punto: # este workflow te DEJA intentar desplegar producción desde una rama. Lo que lo # impide es la condición del rol de IAM, no este archivo. workflow_dispatch:
permissions: id-token: write contents: write # necesario para publicar el release en GitHub
concurrency: group: release-prod cancel-in-progress: false # nunca cancelar un despliegue a producción a medias
jobs: publicar: runs-on: ubuntu-latest
# Este entorno puede tener revisores obligatorios configurados en GitHub. # Con eso, el job espera aprobación humana antes de tocar producción. environment: name: prod url: ${{ vars.SITE_URL }}
steps: - uses: actions/checkout@v7 with: fetch-depth: 0 # para poder generar notas del release
- uses: actions/setup-node@v7 with: node-version: 24 cache: npm
- name: Instalar dependencias run: npm ci
- name: Verificar tipos run: npm run typecheck
- name: Compilar con la versión del tag env: # github.ref_name es el tag, por ejemplo v1.2.3. Esto es lo que hace # que la app pueda decir qué versión está sirviendo. APP_VERSION: ${{ github.ref_name }} APP_COMMIT: ${{ github.sha }} run: npm run build
- name: Verificar que la versión quedó incrustada env: VERSION: ${{ github.ref_name }} run: | # Una comprobación barata que evita publicar un artefacto sin versionar. if ! grep -rq "$VERSION" dist/assets/*.js; then echo "::error::La versión $VERSION no aparece en el bundle." exit 1 fi echo "Versión $VERSION presente en el bundle."
- name: Autenticar contra AWS uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ vars.AWS_DEPLOY_ROLE }} aws-region: ${{ vars.AWS_REGION }} role-session-name: gha-release-${{ github.ref_name }}
- name: Subir assets env: BUCKET: ${{ vars.AWS_S3_BUCKET }} run: | aws s3 sync dist/ "s3://${BUCKET}/" \ --exclude "index.html" \ --cache-control "public,max-age=31536000,immutable" \ --only-show-errors
- name: Subir index.html env: BUCKET: ${{ vars.AWS_S3_BUCKET }} run: | # El index.html se sube al final: es el interruptor que activa la nueva # versión. Hasta este momento, los usuarios siguen viendo la anterior. aws s3 cp dist/index.html "s3://${BUCKET}/index.html" \ --cache-control "public,max-age=0,must-revalidate" \ --content-type "text/html; charset=utf-8" \ --metadata "version=${{ github.ref_name }},commit=${{ github.sha }}" \ --only-show-errors
- name: Invalidar la caché de CloudFront env: DISTRIBUTION: ${{ vars.AWS_CLOUDFRONT_ID }} run: | ID=$(aws cloudfront create-invalidation \ --distribution-id "${DISTRIBUTION}" \ --paths "/" "/index.html" \ --query 'Invalidation.Id' \ --output text) echo "Invalidación creada: $ID"
# Esperar a que termine da una señal clara de "ya está publicado". aws cloudfront wait invalidation-completed \ --distribution-id "${DISTRIBUTION}" \ --id "$ID"
- name: Publicar el release en GitHub # Solo cuando viene de un tag: una ejecución manual desde una rama no # tiene un tag que publicar. if: startsWith(github.ref, 'refs/tags/') env: GH_TOKEN: ${{ github.token }} VERSION: ${{ github.ref_name }} SITE_URL: ${{ vars.SITE_URL }} run: | gh release create "$VERSION" \ --title "$VERSION" \ --generate-notes \ --notes-start-tag "$(git describe --tags --abbrev=0 "$VERSION^" 2>/dev/null || echo '')" \ --verify-tag || gh release create "$VERSION" --title "$VERSION" --generate-notes
{ echo "### Publicado en producción" echo "" echo "- Versión: \`$VERSION\`" echo "- Commit: \`${GITHUB_SHA:0:7}\`" echo "- Sitio: ${SITE_URL}" echo "" echo "Para revertir, vuelve a ejecutar este workflow desde el tag anterior." echo "Los assets de los releases previos siguen en S3, así que la" echo "reversión solo reemplaza el index.html." } >> "$GITHUB_STEP_SUMMARY"Las tres barreras, en orden de solidez:
El disparador usa un patrón estricto, no v*, así que un tag como version-final
no dispara nada. El segundo patrón admite prerelanzamientos del tipo v1.0.0-rc.1.
La aprobación humana viene del entorno prod con revisores obligatorios. El tag ya
existe y el build ya se hizo, pero nada llega a producción sin que alguien lo apruebe.
La condición de IAM es la única que no vive en un archivo que cualquiera puede editar, y es la que acabas de comprobar.
Paso 9.6 · Revertir
Sección titulada «Paso 9.6 · Revertir»Objetivo: volver a una versión anterior en menos de un minuto.
Supongamos que v1.0.1 tiene un error grave y quieres volver a v1.0.0.
-
Abre la ejecución del workflow del tag
v1.0.0en GitHub:Ventana de terminal gh run list --workflow release.yml -
Vuelve a ejecutarla con Re-run all jobs.
-
Apruébala.
Qué deberías ver: en menos de un minuto, /acerca vuelve a decir v1.0.0.
Por qué es tan rápido
Sección titulada «Por qué es tan rápido»Porque los assets de v1.0.0 nunca se borraron. El sync sin --delete los dejó
en el bucket, y como llevan hash en el nombre, los de v1.0.1 se sumaron en lugar de
reemplazarlos.
La reversión no recompila nada, no baja artefactos de ningún lado, y no depende de que
el build de hace tres semanas siga siendo reproducible. Solo reemplaza el index.html,
que es un archivo de un kilobyte, e invalida dos rutas.
Qué versión subir
Sección titulada «Qué versión subir»Versionado semántico, MAYOR.MENOR.PARCHE:
| Cambias | Subes | Ejemplo |
|---|---|---|
| Corriges un error sin cambiar comportamiento | parche | v1.0.1 |
| Agregas funcionalidad compatible | menor | v1.1.0 |
| Rompes compatibilidad | mayor | v2.0.0 |
Para una SPA, romper compatibilidad suele significar un cambio que obliga a los usuarios a algo, como perder datos guardados en el navegador o requerir una versión nueva de la API.
Resumen de comandos
Sección titulada «Resumen de comandos»# variables de prodgh variable set AWS_REGION --env prod --body "us-east-1"# ... las otras cuatro
# publicargit tag -a v1.0.0 -m "Primera versión en producción"git push origin v1.0.0gh run watch
# demostrar que producción está abiertagh workflow run release.yml --ref main
# poner la cerradura (tras descomentar el PASO 9.5)make bootstrap-apply
# y comprobar que ahora rechazagh workflow run release.yml --ref main
# revertirgh run list --workflow release.yml # y Re-run all jobs del tag anteriorComprobación
Sección titulada «Comprobación»/acercamuestra el tag que publicaste, nodev.- Un push a
maindespliega a desarrollo, y no toca producción. - Un tag
v*.*.*despliega a producción, tras aprobación. - Una ejecución manual desde
mainfalla al autenticar contra AWS. - Volver a ejecutar el workflow de un tag anterior revierte producción.
Si los cinco se cumplen, tienes un flujo de despliegue que muchos equipos en producción no tienen. Cerremos.