name: Build and Push Frontend on: push: branches: - main paths: &frontend_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: *frontend_paths 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: Login en Gitea Registry uses: docker/login-action@v2 with: registry: gitea.cruzcloud.net username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_PASSWORD }} logout: true - name: Construir y Subir Imagen uses: docker/build-push-action@v4 with: context: workloads/ecommerce/ push: true tags: | gitea.cruzcloud.net/devops/ecommerce-frontend:${{ steps.vars.outputs.VERSION }} gitea.cruzcloud.net/devops/ecommerce-frontend:latest # 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 "cristiancruz0529@gmail.com" # 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 "========================================"