El runner de Gitea ejecuta cada job dentro de un contenedor propio que
habla con el daemon Docker del host (sibling containers) -- un
`docker run -v "\${{ github.workspace }}:/src"` desde ahí intenta
montar una ruta que solo existe DENTRO del contenedor del job, no en
el host real: "mkdir /workspace: read-only file system". Confirmado en
el run real (job 128, task_id 128) tras el fix del anchor.
Se valida localmente contra docker.gitea.com/runner-images:ubuntu-latest
(la misma imagen del runner real) que python3/pip3 están disponibles.
pip3 install directo falla por paquetes de apt sin metadata compatible
(PyJWT, "RECORD file not found") -- se usa un venv aislado en su lugar,
validado end-to-end con los dos steps extraídos del YAML real.
541 lines
19 KiB
YAML
541 lines
19 KiB
YAML
name: Build and Push Frontend
|
|
|
|
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: 20
|
|
|
|
env:
|
|
APP_DIR: workloads/ecommerce
|
|
MANIFEST_FILE: workloads/ecommerce/frontend.yaml
|
|
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
|
|
}
|
|
|
|
grep -q 'currentCollection' "${NAVBAR_FILE}" || {
|
|
echo "ERROR: navbar.tsx no calcula currentCollection."
|
|
exit 1
|
|
}
|
|
|
|
grep -q 'collection=Marvel' "${NAVBAR_FILE}" || {
|
|
echo "ERROR: falta collection=Marvel en navbar.tsx."
|
|
exit 1
|
|
}
|
|
|
|
grep -q 'category=Mochilas' "${NAVBAR_FILE}" || {
|
|
echo "ERROR: falta category=Mochilas en navbar.tsx."
|
|
exit 1
|
|
}
|
|
|
|
grep -q 'aria-current={active ? "page" : undefined}' "${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}:${section}`}' "${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
|
|
|
|
# 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 \
|
|
"${MANIFEST_FILE}"
|
|
|
|
# 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.
|
|
- name: Escanear imagen (Trivy) — CRITICAL bloquea
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
/tmp/trivy image \
|
|
--severity CRITICAL \
|
|
--exit-code 1 \
|
|
--ignore-unfixed \
|
|
"${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 \
|
|
"${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
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
|
|
docker push "${IMAGE_NAME}:latest"
|
|
|
|
- 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 solo la versión (no :latest, que es un tag mutable y firmarlo
|
|
# pierde sentido en cuanto se vuelve a mover). --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.vars.outputs.VERSION }}"
|
|
|
|
# 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.vars.outputs.VERSION }}"
|
|
|
|
# Evita que una ejecución antigua actualice frontend.yaml después de que
|
|
# ya exista un commit más reciente en main.
|
|
- 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 [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; 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}"
|
|
|
|
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
|
|
|
|
- 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 "========================================" |