wip: contenido inicial + test de reusable workflow / composite action

This commit is contained in:
devops committed 2026-08-17 00:12:10 -05:00
1 parent e9fe7d0567
commit 262c0f271a
22 files changed
+1480

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
name: TEST - verificacion de reusable workflow + composite action
on:
push:
branches:
- main
jobs:
call-reusable:
uses: devops/platform-templates/pipelines/_test-reusable.yaml@main
with:
greeting: "hola desde el caller"
@@ -0,0 +1,68 @@
name: "Cosign — firmar y verificar por digest"
description: >-
Firma la imagen por digest (no por tag) y verifica esa firma como
smoke test, en el mismo job. --tlog-upload=false: sin publicar en el
transparency log público de Sigstore (registry privado, no tiene
sentido anunciar públicamente qué se firmó — ver
docs/devsecops/cosign.md en apps-registry para el razonamiento
completo de esta decisión).
IMPORTANTE: un composite action no puede leer `secrets` directamente
— el workflow que lo llama tiene que pasarlos como `with:` (ver
ejemplo en el README de este snippet).
inputs:
image_name:
description: "Nombre de la imagen sin tag/digest (ej. gitea.cruzcloud.net/devops/mi-app)."
required: true
digest:
description: "Digest sha256:... a firmar (el output del paso que hace docker push)."
required: true
public_key_path:
description: "Ruta al cosign.pub commiteado en el repo (para el smoke test de verificación)."
required: true
cosign_private_key:
description: "Contenido de la llave privada de Cosign (pasar como \\${{ secrets.COSIGN_PRIVATE_KEY }} desde el caller)."
required: true
cosign_password:
description: "Password de la llave privada (pasar como \\${{ secrets.COSIGN_PASSWORD }} desde el caller)."
required: true
cosign_version:
description: "Versión de Cosign a instalar."
required: false
default: "3.1.3"
runs:
using: composite
steps:
- name: Instalar Cosign
shell: bash
run: |
set -euo pipefail
curl -sSfL \
"https://github.com/sigstore/cosign/releases/download/v${{ inputs.cosign_version }}/cosign-linux-amd64" \
-o /tmp/cosign
chmod +x /tmp/cosign
/tmp/cosign version
- name: Firmar imagen (Cosign) — por digest
shell: bash
env:
COSIGN_PRIVATE_KEY: ${{ inputs.cosign_private_key }}
COSIGN_PASSWORD: ${{ inputs.cosign_password }}
run: |
set -euo pipefail
/tmp/cosign sign \
--key env://COSIGN_PRIVATE_KEY \
--use-signing-config=false \
--tlog-upload=false \
--yes \
"${{ inputs.image_name }}@${{ inputs.digest }}"
- name: Verificar firma (smoke test)
shell: bash
run: |
set -euo pipefail
/tmp/cosign verify \
--key "${{ inputs.public_key_path }}" \
--insecure-ignore-tlog=true \
"${{ inputs.image_name }}@${{ inputs.digest }}"
@@ -0,0 +1,52 @@
name: "Gitleaks — escaneo de secretos"
description: >-
Escanea el árbol de archivos (no el historial de git) buscando
secretos commiteados. Bloquea (exit 1) si encuentra algo. Extraído
literal de los 3 pipelines reales del lab (frontend, commerce-backend,
docs-portal) — idéntico en los tres salvo el "source".
inputs:
source:
description: "Directorio a escanear (ej. workloads/mi-app)."
required: true
version:
description: "Versión de Gitleaks a instalar."
required: false
default: "8.21.2"
runs:
using: composite
steps:
- name: Instalar Gitleaks
shell: bash
run: |
set -euo pipefail
curl -sSfL \
"https://github.com/gitleaks/gitleaks/releases/download/v${{ inputs.version }}/gitleaks_${{ inputs.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, no el log de
# commits. Un scan histórico completo se corre aparte, manual, no
# en cada push/PR (ver README de este snippet).
- name: Escanear secretos en el árbol de archivos
shell: bash
run: |
set -euo pipefail
/tmp/gitleaks detect \
--source="${{ inputs.source }}" \
--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
@@ -0,0 +1,82 @@
name: "Semgrep — SAST"
description: >-
Escaneo estático de código, modo auditoría (no bloquea) por default —
mismo criterio que los 3 pipelines reales: primera vuelta para
revisar hallazgos en conjunto antes de decidir qué reglas deberían
bloquear. Corre nativo en un venv, no como contenedor aparte — el
runner de Gitea ya ejecuta cada job dentro de un contenedor propio
(sibling containers hablando con el Docker del host, no
Docker-in-Docker), así que un "docker run -v" desde acá monta rutas
que no existen en el host real y falla con "read-only file system".
inputs:
target:
description: "Directorio a escanear."
required: true
configs:
description: >-
Rulesets de Semgrep separados por coma, ej.
"p/typescript,p/react,p/nextjs,p/security-audit". Elegí el
combo según tu stack real — no copies el mismo set para todo
(ver README de este snippet, tabla de qué usa cada arquetipo).
required: true
semgrep_version:
description: "Versión de Semgrep a instalar."
required: false
default: "1.173.0"
fail_on_findings:
description: "'true' para bloquear el pipeline si hay hallazgos. Default 'false' (auditoría)."
required: false
default: "false"
runs:
using: composite
steps:
- name: Instalar Semgrep
shell: bash
run: |
set -euo pipefail
python3 -m venv /tmp/semgrep-venv
/tmp/semgrep-venv/bin/pip install --quiet "semgrep==${{ inputs.semgrep_version }}"
/tmp/semgrep-venv/bin/semgrep --version
- name: Escaneo SAST (Semgrep)
shell: bash
run: |
set -euo pipefail
CONFIG_ARGS=()
IFS=',' read -ra CONFIGS <<< "${{ inputs.configs }}"
for cfg in "${CONFIGS[@]}"; do
CONFIG_ARGS+=(--config="${cfg}")
done
ERROR_FLAG=()
if [ "${{ inputs.fail_on_findings }}" = "true" ]; then
ERROR_FLAG=(--error)
fi
/tmp/semgrep-venv/bin/semgrep scan \
"${CONFIG_ARGS[@]}" \
"${ERROR_FLAG[@]}" \
--json \
--output=semgrep-report.json \
"${{ inputs.target }}"
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
@@ -0,0 +1,50 @@
name: "Syft — SBOM"
description: >-
Genera el SBOM (CycloneDX + SPDX) de la imagen que ya pasó el gate de
CRITICAL de Trivy — describe exactamente qué paquetes (y en qué
versión) quedaron adentro. CycloneDX es el formato más usado por
herramientas de consulta/alertas de CVEs nuevas.
inputs:
image_ref:
description: "Referencia completa imagen:tag a describir."
required: true
artifact_suffix:
description: "Sufijo para el nombre del artifact publicado (ej. la versión)."
required: true
syft_version:
description: "Versión de Syft a instalar."
required: false
default: "1.51.0"
runs:
using: composite
steps:
- name: Instalar Syft
shell: bash
run: |
set -euo pipefail
curl -sSfL \
"https://github.com/anchore/syft/releases/download/v${{ inputs.syft_version }}/syft_${{ inputs.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 "${{ inputs.image_ref }}" \
-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-${{ inputs.artifact_suffix }}
path: |
sbom.cdx.json
sbom.spdx.json
if-no-files-found: ignore
@@ -0,0 +1,47 @@
name: "Trivy IaC — escaneo de manifiestos Kubernetes"
description: >-
Escanea manifiestos Kubernetes buscando misconfiguraciones (root,
falta de resource limits, falta de probes, etc). Informativo por
default (no bloquea) — mismo criterio que los 3 pipelines reales del
lab mientras se revisan los hallazgos antes de decidir qué debería
bloquear.
inputs:
target:
description: "Archivo o directorio de manifiestos a escanear."
required: true
trivy_version:
description: "Versión de Trivy a instalar (compartida con trivy-image-step si corrés ambos en el mismo job)."
required: false
default: "0.74.0"
fail_on_findings:
description: "'true' para bloquear el pipeline si hay hallazgos. Default 'false' (auditoría)."
required: false
default: "false"
runs:
using: composite
steps:
- name: Instalar Trivy
shell: bash
run: |
set -euo pipefail
curl -sSfL \
"https://github.com/aquasecurity/trivy/releases/download/v${{ inputs.trivy_version }}/trivy_${{ inputs.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: Escanear manifiestos Kubernetes (Trivy IaC)
shell: bash
run: |
set -euo pipefail
EXIT_CODE=0
if [ "${{ inputs.fail_on_findings }}" = "true" ]; then
EXIT_CODE=1
fi
/tmp/trivy config \
--severity CRITICAL,HIGH,MEDIUM \
--exit-code "${EXIT_CODE}" \
"${{ inputs.target }}"
@@ -0,0 +1,65 @@
name: "Trivy imagen — CVE de la imagen construida"
description: >-
Escanea la imagen Docker ya construida y cargada localmente (load:
true en el build, no hace falta push todavía). CRITICAL bloquea el
pipeline; HIGH es informativo. --timeout 15m0s siempre — el default
de Trivy (5m) no alcanza bajo carga real del host (visto en vivo en
commerce-backend, "context deadline exceeded" a los 4m52s).
inputs:
image_ref:
description: "Referencia completa imagen:tag a escanear (ej. \\${{ env.IMAGE_NAME }}:\\${{ steps.vars.outputs.VERSION }})."
required: true
trivy_version:
description: "Versión de Trivy a instalar."
required: false
default: "0.74.0"
ignorefile:
description: >-
Ruta a un .trivyignore con excepciones documentadas (opcional).
Cada excepción debe tener justificación y fecha en el propio
archivo — no es un mecanismo para silenciar sin revisar. Ver
devsecops-snippets/trivy-image-step/README.md.
required: false
default: ""
runs:
using: composite
steps:
- name: Instalar Trivy
shell: bash
run: |
set -euo pipefail
curl -sSfL \
"https://github.com/aquasecurity/trivy/releases/download/v${{ inputs.trivy_version }}/trivy_${{ inputs.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: Escanear imagen (Trivy) — CRITICAL bloquea
shell: bash
run: |
set -euo pipefail
IGNOREFILE_ARGS=()
if [ -n "${{ inputs.ignorefile }}" ]; then
IGNOREFILE_ARGS=(--ignorefile "${{ inputs.ignorefile }}")
fi
/tmp/trivy image \
--severity CRITICAL \
--exit-code 1 \
--ignore-unfixed \
--timeout 15m0s \
"${IGNOREFILE_ARGS[@]}" \
"${{ inputs.image_ref }}"
- name: Escanear imagen (Trivy) — HIGH informativo
shell: bash
run: |
set -euo pipefail
/tmp/trivy image \
--severity HIGH \
--exit-code 0 \
--ignore-unfixed \
--timeout 15m0s \
"${{ inputs.image_ref }}"
+111
View File
@@ -0,0 +1,111 @@
# Basado en el Dockerfile real de ecommerce-frontend (Next.js, output
# "standalone"). Tu next.config.* necesita "output: 'standalone'" para
# que este Dockerfile funcione -- sin eso, .next/standalone no existe
# y el build de la imagen falla en el "test -f" del stage builder.
#
# ARGs a completar por tu app:
# NODE_IMAGE -- versión exacta de Node, fijá una concreta.
# APP_PORT -- puerto de escucha (Next.js con HOSTNAME/PORT del
# runtime standalone). Default 3000; el frontend real
# del lab lo corre en 80 vía este mismo ARG.
# syntax=docker/dockerfile:1.7
ARG NODE_IMAGE=node:24.18.0-bookworm-slim
ARG APP_PORT=3000
# ============================================================
# 1. Dependencias
# ============================================================
FROM ${NODE_IMAGE} AS deps
WORKDIR /app
ENV NODE_ENV=development \
NEXT_TELEMETRY_DISABLED=1 \
NPM_CONFIG_AUDIT=false \
NPM_CONFIG_FUND=false
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm,sharing=private \
npm ci \
--include=prod \
--include=dev \
--no-audit \
--no-fund \
--foreground-scripts
# ============================================================
# 2. Compilación Next.js
# ============================================================
FROM ${NODE_IMAGE} AS builder
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
NODE_OPTIONS=--max-old-space-size=2048
COPY --from=deps /app/node_modules ./node_modules
COPY package.json package-lock.json ./
COPY . .
RUN --mount=type=cache,target=/app/.next/cache,sharing=private \
npm run build
# Falla temprano y con un mensaje claro si next.config.* no tiene
# "output: 'standalone'" -- mejor que un COPY silencioso más abajo
# fallando con "no existe el archivo".
RUN test -f /app/.next/standalone/server.js \
&& test -d /app/.next/static \
|| { echo "ERROR: falta 'output: standalone' en next.config.*"; exit 1; }
# ============================================================
# 3. Runtime de producción
# ============================================================
FROM ${NODE_IMAGE} AS runner
ARG APP_PORT
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
HOSTNAME=0.0.0.0 \
PORT=${APP_PORT}
# setcap: solo hace falta si APP_PORT < 1024 (un puerto "privilegiado"
# que normalmente requiere root para bindear). Si tu APP_PORT es 3000
# o cualquier valor >= 1024, podés borrar el setcap y libcap2-bin.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates libcap2-bin \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --gid 1001 nodejs \
&& useradd --uid 1001 --gid 1001 --create-home --shell /usr/sbin/nologin nextjs \
&& setcap 'cap_net_bind_service=+ep' /usr/local/bin/node
# El runtime solo ejecuta "node server.js" -- nunca npm/npx/corepack.
# Sacarlos reduce lo que Trivy escanea al set real de producción.
RUN rm -rf \
/usr/local/lib/node_modules/npm \
/usr/local/lib/node_modules/corepack \
/usr/local/bin/npm \
/usr/local/bin/npx \
/usr/local/bin/corepack
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE ${APP_PORT}
# CAMBIAR: /api/health tiene que existir en tu app (una route handler
# de Next.js que devuelva 200). Si no la tenés todavía, es la primera
# ruta a agregar -- este healthcheck y el readinessProbe/livenessProbe
# de k8s-manifests/base/deployment.yaml asumen que existe.
HEALTHCHECK --interval=30s --timeout=8s --start-period=30s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/api/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
CMD ["node", "server.js"]
+94
View File
@@ -0,0 +1,94 @@
# Basado en el Dockerfile real de commerce-backend (Medusa v2 / Express).
# Multi-stage: build con devDependencies, runtime solo con lo necesario
# para correr -- sin compilador, sin devDependencies, sin npm/npx.
#
# ARGs a completar por tu app (todos tienen un default razonable):
# NODE_IMAGE -- versión exacta de Node. Fijala a una versión concreta
# (no "node:22-slim"), así un rebuild no te cambia la
# versión de Node por sorpresa un día cualquiera.
# APP_PORT -- puerto en el que tu app escucha.
# START_CMD -- cómo arrancar tu app en runtime. Por default invoca
# "node dist/index.js" -- CAMBIALO al entrypoint real de
# tu build. No uses "npm start"/"npx algo": el runtime
# de esta imagen NO tiene npm (ver más abajo, por qué).
# syntax=docker/dockerfile:1.7
ARG NODE_IMAGE=node:22.18.0-bookworm-slim
ARG APP_PORT=3000
#############################################################
# Builder
#############################################################
FROM ${NODE_IMAGE} AS builder
WORKDIR /app
ENV CI=true \
NODE_ENV=development \
NPM_CONFIG_UPDATE_NOTIFIER=false
COPY package.json package-lock.json* .npmrc* ./
# --legacy-peer-deps: sacalo si tu proyecto no lo necesita. Medusa v2 sí
# lo necesita hoy (conflictos de peer deps conocidos del ecosistema).
RUN --mount=type=cache,id=node-api-npm-cache,target=/root/.npm \
npm ci \
--legacy-peer-deps \
--prefer-offline \
--no-audit \
--no-fund
# CAMBIAR: copiá acá los archivos/carpetas fuente reales de tu app
# (tsconfig, src/, config específico). Estos son solo un ejemplo.
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
RUN npm prune --omit=dev \
&& npm cache clean --force
#############################################################
# Runtime
#############################################################
FROM ${NODE_IMAGE}
ARG APP_PORT
ENV NODE_ENV=production \
PORT=${APP_PORT} \
NPM_CONFIG_UPDATE_NOTIFIER=false
# El CLI global de npm que trae la imagen base node:*-slim arrastra su
# propia copia de "tar" con historial de CVEs — no hace falta en
# runtime (nunca corrés npm/npx acá, solo "node algo.js"). Borrarlo
# reduce lo que Trivy tiene que escanear al set real que corre en
# producción.
#
# OJO si tu app usa un init container que corre "npx <algo>" contra
# esta misma imagen (ej. migraciones): eso se rompe si borrás npm acá.
# Cambiá ese comando a invocar el binario directo con "node", como el
# CMD de abajo — no vuelvas a agregar npm solo para eso.
RUN rm -rf /usr/local/lib/node_modules/npm
WORKDIR /app
# CAMBIAR: qué carpetas copiar del builder depende de tu build real
# (dist/, .output/, node_modules). Este ejemplo asume un build típico
# de tsc/esbuild a dist/ + node_modules de producción.
COPY --from=builder --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
# La imagen base node:*-bookworm-slim ya trae un usuario "node" (uid
# 1000) sin privilegios -- no hace falta crear uno nuevo, a diferencia
# de nextjs.Dockerfile.
USER node
EXPOSE ${APP_PORT}
HEALTHCHECK --interval=30s --timeout=8s --start-period=20s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
# CAMBIAR: el entrypoint real. Nunca "npm start"/"npx algo" -- ver nota
# de arriba sobre por qué npm no existe en esta imagen.
CMD ["node", "dist/index.js"]
+66
View File
@@ -0,0 +1,66 @@
# Basado en el Dockerfile real de docs-portal (sitio MkDocs Material).
# Mejora respecto al original: nginx corre como usuario NO-root acá
# (nginxinc/nginx-unprivileged) -- el docs-portal real del lab todavía
# corre nginx:1.27-alpine estándar (como root), un gap real encontrado
# al armar este template, pendiente de aplicar al repo real por
# separado.
#
# Este template asume MkDocs -- es el único generador de sitio estático
# que existe hoy en el lab. Si tu app usa otro generador (Hugo, Astro,
# un build de Vite sin backend), el STAGE 1 es lo único que cambia:
# reemplazalo por el build real de tu herramienta, el STAGE 2
# (nginx no-root) sirve igual para cualquier sitio 100% estático.
#
# ARGs:
# PYTHON_IMAGE -- versión del builder (solo aplica al ejemplo MkDocs).
# NGINX_IMAGE -- versión de nginx no-root.
# APP_PORT -- OJO: esto solo cambia el EXPOSE/healthcheck de este
# Dockerfile, NO el puerto real en el que nginx
# escucha -- eso lo define el nginx.conf que trae la
# propia imagen nginx-unprivileged, fijo en 8080. Si
# cambiás este ARG a otro valor, tenés que agregar un
# nginx.conf propio (COPY) que escuche en ese mismo
# puerto, o quedan desalineados. Dejalo en 8080
# mientras no necesites tocar nginx.conf.
# syntax=docker/dockerfile:1.7
ARG PYTHON_IMAGE=python:3.12-slim
ARG NGINX_IMAGE=nginxinc/nginx-unprivileged:1.27-alpine
ARG APP_PORT=8080
# --- Stage 1: build del sitio estático (ejemplo: MkDocs Material) ---
FROM ${PYTHON_IMAGE} AS build
WORKDIR /site
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY mkdocs.yml .
COPY docs/ docs/
# --strict: cualquier link roto o warning de nav rompe el build, para
# no publicar nunca un sitio con referencias muertas. Si tu generador
# no tiene un equivalente a --strict, considerá agregar un paso de
# validación de links aparte (ej. linkinator) antes de este build.
RUN mkdocs build --strict
# --- Stage 2: sirve el sitio estático con nginx, sin root ---
FROM ${NGINX_IMAGE}
ARG APP_PORT
# ARG no alcanza para el HEALTHCHECK -- ese CMD corre en runtime, no en
# build time, y ahí un ARG ya no existe. Hace falta ENV.
ENV APP_PORT=${APP_PORT}
# CAMBIAR "/site/site" al directorio real de salida de tu build si no
# usás MkDocs (ej. "dist/", "public/", "build/", ".output/public/").
COPY --from=build /site/site /usr/share/nginx/html
EXPOSE ${APP_PORT}
# nginx-unprivileged trae wget (busybox) -- alcanza para un healthcheck
# simple sin instalar nada más.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -q -O /dev/null "http://127.0.0.1:${APP_PORT}/" || exit 1
+73
View File
@@ -0,0 +1,73 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
labels:
app: app
spec:
replicas: 1
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
imagePullSecrets:
- name: gitea-registry-secret
# Nivel Pod: nada corre como root a menos que el overlay lo
# sobreescriba explícitamente (y debería justificarlo si lo hace).
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
# CAMBIAR: imagen real de tu app. El overlay la sobreescribe.
image: gitea.cruzcloud.net/devops/CAMBIAR-imagen:v0.0.0
ports:
- name: http
containerPort: 8080
# Nivel contenedor: complementa el runAsNonRoot de arriba.
# readOnlyRootFilesystem queda en false por default porque
# muchos runtimes (Next.js standalone, cachés de npm/pip)
# escriben en disco al arrancar -- si tu app NO escribe nada
# en runtime, ponelo en true y listo (más seguro).
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
# CAMBIAR: casi seguro tu app expone un endpoint de salud
# distinto a "/" (ver README de tu arquetipo de pipeline —
# los 3 arquetipos ya documentan cuál usa cada stack).
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
# Límites conservadores para una app chica/media. Subilos en
# tu overlay si tu app realmente los necesita — pero no los
# borres: sin esto, un solo pod con un bug de memoria puede
# arrastrar al nodo entero a swap (visto en vivo con
# gitea-runner sin límites, ver docs-portal del lab).
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 512Mi
+23
View File
@@ -0,0 +1,23 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
ingressClassName: traefik
rules:
# CAMBIAR: el host público real. Sigue el patrón *.cruzcloud.net
# ya usado por shop/commerce/docs — apunta a NPM -> serverlb ->
# Traefik -> este Service (ver reference_infra_topology en la
# memoria del lab si necesitás repasar la ruta completa).
- host: CAMBIAR-app.cruzcloud.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-svc
port:
number: 80
+7
View File
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: app-svc
spec:
selector:
app: app
ports:
- name: http
port: 80
targetPort: http
@@ -0,0 +1,57 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: mi-app-ejemplo
resources:
- ../../base
patches:
# Contenido real de tu app (imagen, puerto, probes, etc.) — el
# "name" del contenedor se queda en "app" a propósito, ver el
# comentario en patches/deployment.yaml.
- target:
kind: Deployment
name: app-deployment
path: patches/deployment.yaml
- target:
kind: Service
name: app-svc
path: patches/service.yaml
- target:
kind: Ingress
name: app-ingress
path: patches/ingress.yaml
# Renombrar los objetos en sí (Kustomize no lo hace con un patch de
# merge normal -- cambiar metadata.name ahí simplemente no aplica.
# Hace falta un patch JSON6902 explícito, uno por recurso).
- target:
kind: Deployment
name: app-deployment
patch: |-
- op: replace
path: /metadata/name
value: mi-app-ejemplo-deployment
# El puerto real de la app: reemplazo la lista de "ports" entera
# por JSON6902 -- un patch de merge normal (ver patches/deployment.yaml)
# NO reemplaza esta lista, la duplica (confirmado en vivo).
- op: replace
path: /spec/template/spec/containers/0/ports
value:
- name: http
containerPort: 3000
- target:
kind: Service
name: app-svc
patch: |-
- op: replace
path: /metadata/name
value: mi-app-ejemplo-svc
- target:
kind: Ingress
name: app-ingress
patch: |-
- op: replace
path: /metadata/name
value: mi-app-ejemplo-ingress
@@ -0,0 +1,37 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
labels:
app: mi-app-ejemplo
spec:
replicas: 1
selector:
matchLabels:
app: mi-app-ejemplo
template:
metadata:
labels:
app: mi-app-ejemplo
spec:
containers:
# OJO: el "name" del contenedor se queda en "app", igual que en
# el base -- es la clave que usa Kustomize para fusionar este
# patch con el contenedor del base en vez de agregar uno
# nuevo al lado. No lo cambies.
- name: app
image: gitea.cruzcloud.net/devops/mi-app-ejemplo:v1.0.1
# El puerto real NO se cambia acá -- $patch: replace no
# funciona para esto en esta versión de Kustomize (probado en
# vivo, se probó y se descartó). Se reemplaza con un patch
# JSON6902 aparte, ver kustomization.yaml. Si no tocás el
# puerto (8080 te sirve tal cual), podés borrar este comentario
# y el patch JSON6902 correspondiente, y listo.
readinessProbe:
httpGet:
path: /api/health
port: http
livenessProbe:
httpGet:
path: /api/health
port: http
@@ -0,0 +1,16 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
spec:
rules:
- host: mi-app-ejemplo.cruzcloud.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mi-app-ejemplo-svc
port:
number: 80
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: app-svc
spec:
selector:
app: mi-app-ejemplo
ports:
- name: http
port: 80
targetPort: 3000
+17
View File
@@ -0,0 +1,17 @@
on:
workflow_call:
inputs:
greeting:
type: string
required: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
with:
source: devsecops-snippets
- shell: bash
run: echo "Reusable workflow input recibido -> '${{ inputs.greeting }}'"
@@ -0,0 +1,193 @@
# Arquetipo de pipeline: Next.js standalone.
# Extraído del pipeline real de ecommerce-frontend (apps-registry),
# generalizando lo específico de esa app (rutas hardcodeadas, checks
# de negocio del catálogo) en inputs.
#
# Cómo usarlo desde tu repo: ver pipelines/archetype-nextjs-frontend/README.md.
on:
workflow_call:
inputs:
app_dir:
description: "Directorio de la app dentro de tu repo (ej. app/)."
type: string
required: true
image_name:
description: "Nombre completo de la imagen sin tag (ej. gitea.cruzcloud.net/devops/mi-app)."
type: string
required: true
manifest_file:
description: "Manifiesto K8s cuyo tag de imagen se actualiza tras el push (CD)."
type: string
required: true
semgrep_configs:
description: "Rulesets de Semgrep, separados por coma."
type: string
required: false
default: "p/typescript,p/react,p/nextjs,p/security-audit"
trivy_ignorefile:
description: "Ruta a un .trivyignore, si tu app tiene excepciones documentadas."
type: string
required: false
default: ""
secrets:
REGISTRY_USER:
required: true
REGISTRY_PASSWORD:
required: true
COSIGN_PRIVATE_KEY:
required: true
COSIGN_PASSWORD:
required: true
jobs:
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
with:
source: ${{ inputs.app_dir }}
build:
name: Construir y Subir Imagen
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APP_DIR: ${{ inputs.app_dir }}
MANIFEST_FILE: ${{ inputs.manifest_file }}
IMAGE_NAME: ${{ inputs.image_name }}
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
persist-credentials: true
- 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; }
- uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main
with:
target: ${{ env.MANIFEST_FILE }}
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
with:
target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }}
- 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 localmente para poder
# escanearla con Trivy antes de subirla.
- name: Construir Imagen
uses: docker/build-push-action@v4
with:
context: ${{ env.APP_DIR }}
push: false
load: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
- uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }}
- uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ steps.vars.outputs.VERSION }}
- 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"
- uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
with:
image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }}
public_key_path: ${{ env.APP_DIR }}/cosign.pub
cosign_private_key: ${{ secrets.COSIGN_PRIVATE_KEY }}
cosign_password: ${{ secrets.COSIGN_PASSWORD }}
- 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)"
if [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; then
echo "promote=true" >> "$GITHUB_OUTPUT"
else
echo "promote=false" >> "$GITHUB_OUTPUT"
echo "Hay un commit más reciente; no se actualizará el manifiesto."
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 "gitea-actions"
git config user.email "[email protected]"
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(gitops): deploy ${VERSION} [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"
@@ -0,0 +1,193 @@
# Arquetipo de pipeline: API Node/Express (o similar, sin SSR).
# Extraído del pipeline real de commerce-backend/Medusa (apps-registry).
#
# Cómo usarlo desde tu repo: ver pipelines/archetype-node-api/README.md.
on:
workflow_call:
inputs:
app_dir:
description: "Directorio de la app dentro de tu repo."
type: string
required: true
image_name:
description: "Nombre completo de la imagen sin tag."
type: string
required: true
manifest_file:
description: "Manifiesto K8s cuyo tag de imagen se actualiza tras el push (CD)."
type: string
required: true
semgrep_configs:
description: "Rulesets de Semgrep, separados por coma."
type: string
required: false
default: "p/typescript,p/security-audit,p/owasp-top-ten"
trivy_ignorefile:
description: "Ruta a un .trivyignore, si tu app tiene excepciones documentadas."
type: string
required: false
default: ""
secrets:
REGISTRY_USER:
required: true
REGISTRY_PASSWORD:
required: true
COSIGN_PRIVATE_KEY:
required: true
COSIGN_PASSWORD:
required: true
jobs:
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
with:
source: ${{ inputs.app_dir }}
build:
name: Construir y Subir Imagen
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
APP_DIR: ${{ inputs.app_dir }}
MANIFEST_FILE: ${{ inputs.manifest_file }}
IMAGE_NAME: ${{ inputs.image_name }}
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
persist-credentials: true
- name: Definir versión
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; }
- uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main
with:
target: ${{ env.MANIFEST_FILE }}
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
with:
target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }}
- 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 Imagen
uses: docker/build-push-action@v4
with:
context: ${{ env.APP_DIR }}
push: false
load: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
# --timeout 15m0s ya viene por default en trivy-image-step —
# commerce-backend (Medusa v2) es exactamente el caso real que
# justificó ese default (node_modules completo del framework +
# admin-sdk + cli, mucho más que escanear que una API chica).
- uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }}
- uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ steps.vars.outputs.VERSION }}
- 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"
- uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
with:
image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }}
public_key_path: ${{ env.APP_DIR }}/cosign.pub
cosign_private_key: ${{ secrets.COSIGN_PRIVATE_KEY }}
cosign_password: ${{ secrets.COSIGN_PASSWORD }}
- 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)"
if [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; then
echo "promote=true" >> "$GITHUB_OUTPUT"
else
echo "promote=false" >> "$GITHUB_OUTPUT"
echo "Hay un commit más reciente; no se actualizará el manifiesto."
fi
- name: Actualizar manifiesto
if: steps.promotion.outputs.promote == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
git config user.name "gitea-actions"
git config user.email "[email protected]"
git fetch origin main
git checkout -B main origin/main
sed -i -E "s|(${IMAGE_NAME}:)v[0-9]+\.[0-9]+\.[0-9]+|\1${VERSION}|g" "${MANIFEST_FILE}"
git add "${MANIFEST_FILE}"
if git diff --cached --quiet; then
echo "El manifiesto ya apunta a ${VERSION}."
exit 0
fi
git commit -m "chore(gitops): deploy ${VERSION} [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"
@@ -0,0 +1,195 @@
# Arquetipo de pipeline: sitio estático servido por nginx (MkDocs,
# Hugo, Astro, HTML plano). Extraído del pipeline real de docs-portal
# (apps-registry).
#
# Diferencia clave con los otros 2 arquetipos: gitleaks y Trivy IaC
# escanean todo APP_DIR (no un único manifest_file) — un sitio estático
# suele tener varios manifiestos K8s separados (deployment/service/
# ingress) en vez de uno solo.
#
# Cómo usarlo desde tu repo: ver pipelines/archetype-static-site/README.md.
on:
workflow_call:
inputs:
app_dir:
description: "Directorio de la app dentro de tu repo."
type: string
required: true
image_name:
description: "Nombre completo de la imagen sin tag."
type: string
required: true
manifest_file:
description: "Manifiesto K8s (Deployment) cuyo tag de imagen se actualiza tras el push (CD)."
type: string
required: true
semgrep_configs:
description: "Rulesets de Semgrep, separados por coma."
type: string
required: false
default: "p/python,p/security-audit"
trivy_ignorefile:
description: "Ruta a un .trivyignore, si tu app tiene excepciones documentadas."
type: string
required: false
default: ""
secrets:
REGISTRY_USER:
required: true
REGISTRY_PASSWORD:
required: true
COSIGN_PRIVATE_KEY:
required: true
COSIGN_PASSWORD:
required: true
jobs:
gitleaks:
name: Escaneo de Secretos (Gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
with:
source: ${{ inputs.app_dir }}
build:
name: Construir y publicar
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 20
env:
APP_DIR: ${{ inputs.app_dir }}
MANIFEST_FILE: ${{ inputs.manifest_file }}
IMAGE_NAME: ${{ inputs.image_name }}
steps:
- name: Checkout del código
uses: actions/checkout@v3
with:
fetch-depth: 1
persist-credentials: true
- name: Definir versión
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; }
- uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main
with:
target: ${{ env.APP_DIR }}
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
with:
target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }}
- 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 Imagen
uses: docker/build-push-action@v4
with:
context: ${{ env.APP_DIR }}
push: false
load: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest
- uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }}
- uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main
with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ steps.vars.outputs.VERSION }}
- 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"
- uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
with:
image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }}
public_key_path: ${{ env.APP_DIR }}/cosign.pub
cosign_private_key: ${{ secrets.COSIGN_PRIVATE_KEY }}
cosign_password: ${{ secrets.COSIGN_PASSWORD }}
- 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)"
if [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; then
echo "promote=true" >> "$GITHUB_OUTPUT"
else
echo "promote=false" >> "$GITHUB_OUTPUT"
echo "Hay un commit más reciente; no se actualizará el manifiesto."
fi
- name: Actualizar manifiesto GitOps
if: steps.promotion.outputs.promote == 'true'
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.vars.outputs.VERSION }}"
git config user.name "gitea-actions"
git config user.email "[email protected]"
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 "El manifiesto ya apunta a ${VERSION}."
exit 0
fi
git commit -m "chore(gitops): deploy ${VERSION} [skip ci]"
git push origin HEAD:main
- name: Resumen del pipeline
if: always()
shell: bash
run: |
echo "========================================"
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
echo "Commit: ${{ github.sha }}"
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
echo "========================================"