Files
apps-registry/.gitea/workflows/build.yaml
T
devopsandClaude Sonnet 5 fed5dfc1c1 ci(ecommerce): automatizar promocion de Staging a Produccion
Agrega el step "Promover a Produccion (CD)" al mismo job que ya
despliega a Staging en build.yaml -- misma logica de
scripts/promote-to-production.sh (sed/perl sobre el manifest de
Produccion) pero sin el read -rp interactivo, gateado por la misma
verificacion de promocion segura (merge-base --is-ancestor) que ya usa
el step de Staging.

Decision explicita del usuario 2026-09-20: cero gate humano entre
Staging y Produccion -- la promocion manual deja de ser el camino
normal, scripts/promote-to-production.sh queda solo como herramienta
de excepcion/rollback.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
2026-09-20 17:27:37 +00:00

665 lines
26 KiB
YAML

name: Build and Push Frontend
# Retrigger real (take 4): los 3 intentos anteriores fueron commits vacíos
# (--allow-empty) que no tocaban ningún path filtrado -- por diseño, nunca
# iban a disparar build.yaml. Este comentario sí cuenta como cambio real
# en .gitea/workflows/build.yaml, que está en la lista de paths.
on:
# NOTA: sin anchors/aliases de YAML (&x / *x) a propósito -- el parser
# de workflows de Gitea Actions no los resuelve en el bloque "on:" y
# descarta el archivo completo con "unknown on type" (visto en vivo el
# 2026-08-15). Las dos listas de paths quedan duplicadas literalmente.
push:
branches:
- main
paths:
# Código y configuración real del frontend.
# Los cambios exclusivos de GitOps (frontend.yaml, ingress, patches, etc.)
# no vuelven a construir la imagen.
- 'workloads/ecommerce/Dockerfile'
- 'workloads/ecommerce/.dockerignore'
- 'workloads/ecommerce/.npmrc'
- 'workloads/ecommerce/package.json'
- 'workloads/ecommerce/package-lock.json'
- 'workloads/ecommerce/next.config.*'
- 'workloads/ecommerce/tsconfig.json'
- 'workloads/ecommerce/tailwind.config.*'
- 'workloads/ecommerce/postcss.config.*'
- 'workloads/ecommerce/eslint.config.*'
- 'workloads/ecommerce/*.js'
- 'workloads/ecommerce/*.mjs'
- 'workloads/ecommerce/*.ts'
- 'workloads/ecommerce/*.tsx'
- 'workloads/ecommerce/app/**'
- 'workloads/ecommerce/src/**'
- 'workloads/ecommerce/components/**'
- 'workloads/ecommerce/lib/**'
- 'workloads/ecommerce/public/**'
- 'workloads/ecommerce/styles/**'
- '.gitea/workflows/build.yaml'
pull_request:
branches:
- main
paths:
- 'workloads/ecommerce/Dockerfile'
- 'workloads/ecommerce/.dockerignore'
- 'workloads/ecommerce/.npmrc'
- 'workloads/ecommerce/package.json'
- 'workloads/ecommerce/package-lock.json'
- 'workloads/ecommerce/next.config.*'
- 'workloads/ecommerce/tsconfig.json'
- 'workloads/ecommerce/tailwind.config.*'
- 'workloads/ecommerce/postcss.config.*'
- 'workloads/ecommerce/eslint.config.*'
- 'workloads/ecommerce/*.js'
- 'workloads/ecommerce/*.mjs'
- 'workloads/ecommerce/*.ts'
- 'workloads/ecommerce/*.tsx'
- 'workloads/ecommerce/app/**'
- 'workloads/ecommerce/src/**'
- 'workloads/ecommerce/components/**'
- 'workloads/ecommerce/lib/**'
- 'workloads/ecommerce/public/**'
- 'workloads/ecommerce/styles/**'
- '.gitea/workflows/build.yaml'
permissions:
contents: write
packages: write
jobs:
# Corre en push y en pull_request, siempre antes que build. Si encuentra
# un secreto commiteado, el step termina con exit code distinto de 0 y,
# por el "needs" del job build, la imagen nunca se construye ni se sube.
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Instalar Gitleaks
shell: bash
run: |
set -euo pipefail
GITLEAKS_VERSION="8.21.2"
curl -sSfL \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
-o /tmp/gitleaks.tar.gz
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
chmod +x /tmp/gitleaks
/tmp/gitleaks version
# --no-git: escanea el árbol de archivos del checkout (fetch-depth: 1,
# sin historial), no el log de commits. El scan histórico completo del
# repo se corre aparte, manualmente, no en cada push/PR.
- name: Escanear secretos en el árbol de archivos
shell: bash
run: |
set -euo pipefail
/tmp/gitleaks detect \
--source=workloads/ecommerce \
--no-git \
--redact \
--report-format=json \
--report-path=gitleaks-report.json \
--exit-code=1
- name: Publicar reporte de Gitleaks
if: always()
uses: actions/upload-artifact@v3
with:
name: gitleaks-report
path: gitleaks-report.json
if-no-files-found: ignore
build:
name: Construir y Subir Imagen
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 30
env:
APP_DIR: workloads/ecommerce
# Cambio 2026-09-14: todo push a main promueve a STAGING
# automaticamente (workloads/ecommerce-staging), no a Produccion
# directo -- decision del usuario para dejar de probar cambios en
# vivo contra Prod.
# Cambio 2026-09-20: la promocion de Staging a Produccion (antes
# scripts/promote-to-production.sh, manual con confirmacion
# interactiva) ahora corre sola en este mismo job -- ver el step
# "Promover a Produccion (CD)" mas abajo, justo despues del de
# Staging. Decision explicita del usuario: cero gate humano entre
# Staging y Produccion. El script manual queda solo como
# herramienta de rollback/excepcion, ya no es el camino normal.
MANIFEST_FILE: workloads/ecommerce-staging/frontend/patches/deployment.yaml
PROD_MANIFEST_FILE: workloads/ecommerce/frontend/patches/deployment.yaml
KUSTOMIZE_DIR: workloads/ecommerce-staging/frontend
IMAGE_NAME: gitea.cruzcloud.net/devops/ecommerce-frontend
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
persist-credentials: true
# Conservamos el esquema estable v1.0.X basado en el número del pipeline.
- name: Definir Versión Semántica
id: vars
shell: bash
run: |
set -euo pipefail
echo "VERSION=v1.0.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
- name: Validar secretos del Registry
shell: bash
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -euo pipefail
test -n "${REGISTRY_USER}" || {
echo "ERROR: REGISTRY_USER no está configurado."
exit 1
}
test -n "${REGISTRY_PASSWORD}" || {
echo "ERROR: REGISTRY_PASSWORD no está configurado."
exit 1
}
echo "OK: secretos del Registry disponibles."
# Evita que archivos de ejemplo entren en la compilación de Next.js.
# Se eliminan únicamente del workspace temporal del runner.
- name: Preparar contexto de producción
shell: bash
run: |
set -euo pipefail
test -f "${APP_DIR}/Dockerfile"
test -f "${APP_DIR}/package.json"
test -f "${APP_DIR}/package-lock.json"
test -f "${MANIFEST_FILE}"
rm -rf "${APP_DIR}/integration"
find "${APP_DIR}" -type f \( -name '*.example.ts' -o -name '*.example.tsx' \) -delete
echo "Contexto listo:"
du -sh "${APP_DIR}"
# Valida los tres componentes reales que controlan la navegación.
# No busca CategoryMenu.tsx porque ese archivo era código sin uso.
- name: Validar navegación real del catálogo
shell: bash
run: |
set -euo pipefail
NAVBAR_FILE="${APP_DIR}/components/layout/navbar.tsx"
CATALOG_FILE="${APP_DIR}/components/catalog/catalog-client.tsx"
PAGE_FILE="${APP_DIR}/app/catalog/page.tsx"
for FILE in "${NAVBAR_FILE}" "${CATALOG_FILE}" "${PAGE_FILE}"; do
if [ ! -f "${FILE}" ]; then
echo "ERROR: no existe ${FILE}"
exit 1
fi
done
echo "Validando Navbar real: ${NAVBAR_FILE}"
grep -q 'useSearchParams' "${NAVBAR_FILE}" || {
echo "ERROR: navbar.tsx no usa useSearchParams."
exit 1
}
grep -q 'currentCategory' "${NAVBAR_FILE}" || {
echo "ERROR: navbar.tsx no calcula currentCategory."
exit 1
}
# Desde Etapa 2 sub-fase 2 (2026-08-25) el nav es data-driven
# contra el arbol real de categorias de Medusa, no un array
# hardcodeado -- esta version anterior del check no detectaba
# que "category=Mochilas"/"collection=Marvel" eran strings
# hardcodeadas que ya no correspondian a ningun dato real (rotas
# en silencio desde que se elimino legacyProductMeta, PR #29).
# "currentCollection" tampoco existe mas: las colecciones ya no
# se usan para resaltar el item activo del menu de categorias.
grep -q 'buildCategoryTree' "${NAVBAR_FILE}" || {
echo "ERROR: navbar.tsx ya no arma el arbol de categorias desde datos reales."
exit 1
}
grep -q 'encodeURIComponent(root.name)' "${NAVBAR_FILE}" || {
echo "ERROR: navbar.tsx ya no linkea por el nombre real de categoria."
exit 1
}
grep -q 'aria-current={isActive(' "${NAVBAR_FILE}" || {
echo "ERROR: navbar.tsx no expone correctamente el menú activo."
exit 1
}
echo "Validando sincronización del catálogo: ${CATALOG_FILE}"
grep -q 'useEffect' "${CATALOG_FILE}" || {
echo "ERROR: catalog-client.tsx no usa useEffect."
exit 1
}
grep -q 'setCategory(initialCategory' "${CATALOG_FILE}" || {
echo "ERROR: catalog-client.tsx no sincroniza initialCategory."
exit 1
}
grep -q 'setCollection(initialCollection' "${CATALOG_FILE}" || {
echo "ERROR: catalog-client.tsx no sincroniza initialCollection."
exit 1
}
grep -q 'setQuery(initialQuery' "${CATALOG_FILE}" || {
echo "ERROR: catalog-client.tsx no sincroniza initialQuery."
exit 1
}
echo "Validando remontaje por parámetros: ${PAGE_FILE}"
grep -q 'key={`${category}:${collection}:${query}`}' "${PAGE_FILE}" || {
echo "ERROR: app/catalog/page.tsx no remonta CatalogClient al cambiar la URL."
exit 1
}
echo "OK: navegación real del catálogo validada."
- name: Instalar Trivy
shell: bash
run: |
set -euo pipefail
TRIVY_VERSION="0.74.0"
curl -sSfL \
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \
-o /tmp/trivy.tar.gz
tar -xzf /tmp/trivy.tar.gz -C /tmp trivy
chmod +x /tmp/trivy
/tmp/trivy version
- name: Instalar kubectl
shell: bash
run: |
set -euo pipefail
KUBECTL_VERSION="v1.31.2"
curl -sSfL \
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" \
-o /tmp/kubectl
chmod +x /tmp/kubectl
/tmp/kubectl version --client
# ${MANIFEST_FILE} es solo el patch de Kustomize del Deployment, no el
# manifiesto que de verdad corre en el clúster -- escanearlo directo
# generaba falsos positivos en todo lo que hereda del base remoto de
# platform-templates (runAsNonRoot, seccompProfile), porque ese patch
# nunca repite lo que ya trae el base (visto en vivo 2026-08-24:
# comparando el patch aislado contra el output real de
# "kubectl kustomize", 2 de 5 hallazgos desaparecían). Renderizamos el
# kustomization completo (${KUSTOMIZE_DIR}, que sí trae el base vía
# git remoto) y escaneamos eso.
- name: Renderizar manifiesto Kustomize
shell: bash
run: |
set -euo pipefail
/tmp/kubectl kustomize "${KUSTOMIZE_DIR}" > /tmp/frontend-rendered.yaml
# Escanea el manifiesto Kubernetes real del frontend (no la imagen):
# contenedores como root, falta de resource limits, falta de
# readiness/liveness probes, etc. Informativo por ahora — no bloquea
# el pipeline mientras revisamos juntos qué hallazgos son reales.
- name: Escanear manifiestos Kubernetes (Trivy IaC)
shell: bash
run: |
set -euo pipefail
/tmp/trivy config \
--severity CRITICAL,HIGH,MEDIUM \
--exit-code 0 \
/tmp/frontend-rendered.yaml
# Instalado directo con pip (no como container aparte): el runner de
# Gitea ejecuta cada job ya dentro de un contenedor propio que habla
# con el daemon Docker del host (sibling containers) -- un
# `docker run -v "${{ github.workspace }}:/src"` desde acá intenta
# montar una ruta que solo existe DENTRO del contenedor del job, no
# en el host real, y falla con "read-only file system". Evitamos por
# completo el problema corriendo Semgrep nativo, igual que
# gitleaks/trivy/syft/cosign.
# venv en vez de "pip3 install --break-system-packages": la imagen del
# runner ya trae paquetes de Python instalados por apt (ej. PyJWT)
# sin metadata compatible con pip, y pip aborta al intentar
# reemplazarlos ("RECORD file not found"). Un venv aislado evita
# tocar los paquetes del sistema por completo.
- name: Instalar Semgrep
shell: bash
run: |
set -euo pipefail
python3 -m venv /tmp/semgrep-venv
/tmp/semgrep-venv/bin/pip install --quiet "semgrep==1.173.0"
/tmp/semgrep-venv/bin/semgrep --version
# Modo auditoría: sin --error a propósito, Semgrep siempre termina
# con exit 0 aunque reporte hallazgos. Es la primera vuelta — se
# revisan los resultados en conjunto antes de decidir qué reglas
# deberían pasar a bloquear el pipeline más adelante.
- name: Escaneo SAST (Semgrep) — modo auditoría, no bloquea
shell: bash
run: |
set -euo pipefail
/tmp/semgrep-venv/bin/semgrep scan \
--config=p/typescript \
--config=p/react \
--config=p/nextjs \
--config=p/security-audit \
--json \
--output=semgrep-report.json \
"${APP_DIR}"
echo "=== Resumen Semgrep ==="
/tmp/semgrep-venv/bin/python -c "
import json
data = json.load(open('semgrep-report.json'))
results = data.get('results', [])
print(f'Hallazgos: {len(results)}')
for r in results:
print(f\" [{r['extra']['severity']}] {r['check_id']} - {r['path']}:{r['start']['line']}\")
"
- name: Publicar reporte de Semgrep
if: always()
uses: actions/upload-artifact@v3
with:
name: semgrep-report
path: semgrep-report.json
if-no-files-found: ignore
- name: Login en Gitea Registry
uses: docker/login-action@v2
with:
registry: gitea.cruzcloud.net
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
logout: true
# push: false — la imagen se queda cargada en el daemon local (load:
# true) para poder escanearla con Trivy antes de subirla al registry.
- name: Construir Imagen
uses: docker/build-push-action@v4
with:
context: workloads/ecommerce/
push: false
load: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
# CRITICAL bloquea el pipeline: no se sube una imagen con una CVE
# crítica conocida y con fix disponible.
# --timeout 15m0s: el default de Trivy (5m) dejó de alcanzar también
# para el frontend (visto en vivo: "context deadline exceeded" a los
# ~4m45s, en un run justo después de terminar el docker build, con
# el host bajo la misma inestabilidad de nodo vista ese día). Mismo
# fix ya aplicado en build-medusa.yaml el 2026-08-16 -- el supuesto
# de que el frontend era más liviano y no lo necesitaba ya no vale.
- name: Escanear imagen (Trivy) — CRITICAL bloquea
shell: bash
run: |
set -euo pipefail
/tmp/trivy image \
--severity CRITICAL \
--exit-code 1 \
--ignore-unfixed \
--timeout 15m0s \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
# HIGH solo informa por ahora — no bloquea mientras aprendemos a leer
# los reportes y decidimos, con calma, qué reglas deben bloquear.
- name: Escanear imagen (Trivy) — HIGH informativo
shell: bash
run: |
set -euo pipefail
/tmp/trivy image \
--severity HIGH \
--exit-code 0 \
--ignore-unfixed \
--timeout 15m0s \
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
# SBOM de la imagen que ya pasó el gate de CRITICAL — describe
# exactamente qué paquetes (y en qué versión) quedaron adentro.
# CycloneDX: formato más usado por herramientas de consulta/alertas
# de CVEs (ej. cruzar el SBOM contra un aviso nuevo tipo log4shell).
- name: Instalar Syft
shell: bash
run: |
set -euo pipefail
SYFT_VERSION="1.51.0"
curl -sSfL \
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_linux_amd64.tar.gz" \
-o /tmp/syft.tar.gz
tar -xzf /tmp/syft.tar.gz -C /tmp syft
chmod +x /tmp/syft
/tmp/syft version
- name: Generar SBOM (Syft)
shell: bash
run: |
set -euo pipefail
/tmp/syft "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" \
-o cyclonedx-json=sbom.cdx.json \
-o spdx-json=sbom.spdx.json
- name: Publicar SBOM
if: always()
uses: actions/upload-artifact@v3
with:
name: sbom-${{ steps.vars.outputs.VERSION }}
path: |
sbom.cdx.json
sbom.spdx.json
if-no-files-found: ignore
# Login ya se hizo arriba; recién acá se sube, después de que la
# imagen pasó el gate de CRITICAL.
- name: Subir Imagen al Registry
id: push
shell: bash
run: |
set -euo pipefail
PUSH_OUTPUT="$(docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}")"
echo "${PUSH_OUTPUT}"
docker push "${IMAGE_NAME}:latest"
DIGEST="$(echo "${PUSH_OUTPUT}" | grep -oE 'sha256:[a-f0-9]{64}' | tail -1)"
test -n "${DIGEST}" || {
echo "ERROR: no se pudo extraer el digest del push."
exit 1
}
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
- name: Instalar Cosign
shell: bash
run: |
set -euo pipefail
COSIGN_VERSION="3.1.3"
curl -sSfL \
"https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" \
-o /tmp/cosign
chmod +x /tmp/cosign
/tmp/cosign version
# Firma por digest, no por tag: :VERSION y :latest son punteros que
# en teoría podrían volver a moverse, pero el digest identifica el
# contenido exacto de la imagen que Trivy ya escaneó unos pasos
# atrás. --use-signing-config=false --tlog-upload=false: firma solo
# con el par de llaves propio, sin publicar metadata en el
# transparency log público de Sigstore — este registry es privado,
# no tiene sentido anunciar públicamente qué se firmó y cuándo.
- name: Firmar Imagen (Cosign)
shell: bash
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
set -euo pipefail
/tmp/cosign sign \
--key env://COSIGN_PRIVATE_KEY \
--use-signing-config=false \
--tlog-upload=false \
--yes \
"${IMAGE_NAME}@${{ steps.push.outputs.digest }}"
# Smoke test: confirma en el propio pipeline que la firma que se
# acaba de crear valida contra la llave pública commiteada en el
# repo (workloads/ecommerce/cosign.pub). Es la misma verificación
# que, más adelante, podría correr un admission controller en el
# cluster antes de dejar desplegar la imagen (ver docs/devsecops/cosign.md).
- name: Verificar Firma (smoke test)
shell: bash
run: |
set -euo pipefail
/tmp/cosign verify \
--key "${APP_DIR}/cosign.pub" \
--insecure-ignore-tlog=true \
"${IMAGE_NAME}@${{ steps.push.outputs.digest }}"
# Evita que una ejecución antigua actualice frontend.yaml después de que
# ya exista un commit más reciente en main.
#
# merge-base --is-ancestor, no comparación exacta de SHA: cuando 2+
# pipelines comparten el mismo commit disparador y el runner los
# corre en serie (capacity: 1), el primero en terminar ya dejó su
# propio commit de GitOps en main -- con comparación exacta, los
# demás ven "main ya no es igual a mi commit" y se saltan la
# promoción sin necesidad (visto en vivo 2026-08-17, ver
# docs/playbooks/migracion-platform-templates-2026-08.md). Con
# is-ancestor, mientras el commit que disparó ESTE pipeline siga
# siendo ancestro de main, promociona igual -- solo bloquea si de
# verdad hubo un rebase/force-push que lo descartó.
- name: Verificar promoción segura
id: promotion
shell: bash
run: |
set -euo pipefail
git fetch origin main
CURRENT_SHA="${{ github.sha }}"
REMOTE_SHA="$(git rev-parse origin/main)"
echo "Commit del pipeline: ${CURRENT_SHA}"
echo "Último commit remoto: ${REMOTE_SHA}"
if git merge-base --is-ancestor "${CURRENT_SHA}" origin/main; then
echo "promote=true" >> "$GITHUB_OUTPUT"
echo "La imagen puede promocionarse."
else
echo "promote=false" >> "$GITHUB_OUTPUT"
echo "La imagen fue publicada, pero no se actualizará el manifiesto."
echo "Existe un commit más reciente y su pipeline debe realizar la promoción."
fi
- name: Actualizar Manifiesto GitOps (CD)
if: steps.promotion.outputs.promote == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
git config user.name "Cristian Felipe"
git config user.email "[email protected]"
# Sincronizamos el workspace temporal con main.
# No se fuerza ni se sobrescribe la rama remota.
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(image: ${IMAGE_NAME}:).*|\1${VERSION}|g" "${MANIFEST_FILE}"
# El footer (workloads/ecommerce/components/layout/footer.tsx) lee
# process.env.APP_VERSION para mostrar la versión real desplegada
# -- este bump la mantiene igual al tag de la imagen de arriba.
# -0777 (slurp): el nombre y el valor están en líneas distintas del
# YAML, un sed de una sola línea no puede verlos juntos.
perl -0777 -pi -e "s/(- name: APP_VERSION\n\s*value: ).*/\${1}${VERSION}/" "${MANIFEST_FILE}"
git add "${MANIFEST_FILE}"
if git diff --cached --quiet; then
echo "No hay cambios en el manifiesto."
exit 0
fi
git commit -m "chore: release ${VERSION} [skip ci]"
# Push normal: si main cambió durante el job, se detiene para evitar
# sobrescribir commits ajenos o promocionar una versión antigua.
git push origin HEAD:main
# Automatiza lo que antes era scripts/promote-to-production.sh (paso
# manual con confirmación interactiva, read -rp). Decisión del
# usuario 2026-09-20: cero pasos manuales entre Staging y
# Producción — la misma imagen que acaba de promocionar a Staging
# arriba se promueve sola a Producción, sin gate humano en el
# medio. Mismo sed/perl que el script manual (mismo MANIFEST, otro
# archivo), corriendo como un segundo commit separado justo
# después del de Staging — no se pisan en un solo commit porque
# Staging y Producción son releases conceptualmente distintos
# (permite revertir uno sin tocar el otro).
- name: Promover a Producción (CD)
if: steps.promotion.outputs.promote == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
git config user.name "Cristian Felipe"
git config user.email "[email protected]"
# Re-fetch: el step de Staging de arriba ya pusheó un commit a
# main en este mismo job -- sin este refresh, este checkout
# local queda un commit atrás y el push de abajo se rechaza.
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(image: ${IMAGE_NAME}:).*|\1${VERSION}|g" "${PROD_MANIFEST_FILE}"
perl -0777 -pi -e "s/(- name: APP_VERSION\n\s*value: ).*/\${1}${VERSION}/" "${PROD_MANIFEST_FILE}"
git add "${PROD_MANIFEST_FILE}"
if git diff --cached --quiet; then
echo "Producción ya está en ${VERSION}."
exit 0
fi
git commit -m "chore: promote ${VERSION} to production [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "ARI Shopping Frontend"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"