docs: READMEs completos, estructura final de arquetipos

Mueve los reusable workflows a .gitea/workflows/ (restriccion real de
Gitea: workflow_call no puede vivir en una ruta arbitraria, confirmado
en vivo). Corrige todos los 'uses:' cross-repo a URL absoluta
(https://gitea.cruzcloud.net/...) -- la forma corta de GitHub se
resuelve contra github.com en esta instancia, confirmado en vivo con un
test real (reusable workflow + composite action ejecutando gitleaks de
punta a punta). Limpia los archivos de prueba.
This commit is contained in:
devops committed 2026-08-17 00:23:24 -05:00
1 parent 018d424a1c
commit a3c022c024
11 files changed
+514 -49

No files matched your search

@@ -0,0 +1,94 @@
# Arquetipo: Next.js frontend (standalone)
Cuándo usar este arquetipo: tu app es un frontend Next.js que compila a
`output: 'standalone'` y sirve HTML renderizado en servidor (SSR/RSC),
no un sitio 100% estático. Si es 100% estático, usá
`archetype-static-site` en cambio — es más liviano.
Qué asume de tu repo:
- Un `Dockerfile` en `app_dir` basado en (o compatible con)
`dockerfiles/nextjs.Dockerfile` — específicamente, que
`next.config.*` tiene `output: 'standalone'`.
- Un endpoint `/api/health` que devuelva 200 — lo usan tanto el
`HEALTHCHECK` del Dockerfile como las probes de
`k8s-manifests/base/deployment.yaml`.
- `cosign.pub` commiteado en `app_dir` (par de la llave privada que
firma en el pipeline).
- Un manifiesto K8s (`manifest_file`) con una línea `image: <tu-imagen>:vX.Y.Z`
que el pipeline actualiza automáticamente tras cada build exitoso
(promoción GitOps).
## Dónde vive el archivo real
**Importante, no es donde dice el nombre de esta carpeta:** el reusable
workflow en sí (`workflow_call`) tiene que vivir bajo `.gitea/workflows/`
por una restricción real de Gitea Actions — no se puede invocar un
`workflow_call` desde una ruta arbitraria como `pipelines/algo/x.yaml`
(confirmado en vivo, error real del servidor: `"uses:" path "..." must
be under a configured workflow directory`). El archivo real es:
```
.gitea/workflows/archetype-nextjs-frontend.yaml
```
Esta carpeta (`pipelines/archetype-nextjs-frontend/`) es solo
documentación — el README que estás leyendo.
## Cómo usarlo desde tu repo
En `.gitea/workflows/build.yaml` de tu propio repo:
```yaml
name: Build and Push
on:
push:
branches: [main]
paths:
- 'app/**'
- '.gitea/workflows/build.yaml'
jobs:
build:
uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/archetype-nextjs-frontend.yaml@main
with:
app_dir: app
image_name: gitea.cruzcloud.net/devops/mi-app
manifest_file: workloads/mi-app/deployment.yaml
# semgrep_configs y trivy_ignorefile son opcionales, tienen default
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
```
**OJO con el `uses:`** — tiene que ser la URL completa
`https://gitea.cruzcloud.net/...`, no la forma corta
`devops/platform-templates/...` (esa se resuelve contra github.com por
default en esta instancia, no contra Gitea local — confirmado en vivo,
es la razón real por la que este repo usa URLs absolutas en todos los
`uses:` internos también, no solo acá).
**`@main` en vez de un tag/commit fijo:** significa que tu pipeline
siempre usa la ÚLTIMA versión del arquetipo. Cómodo para no tener que
actualizar cada repo consumidor a mano, pero un cambio incompatible acá
te rompe el pipeline sin aviso. Si preferís control explícito, fijá un
tag (`@v1.0.0`) o un commit SHA en vez de `@main` — a costa de tener que
actualizarlo vos cuando quieras el cambio nuevo.
## Semgrep: por qué este ruleset
`p/typescript,p/react,p/nextjs,p/security-audit` — cubre el lenguaje, el
framework de UI, Next.js específicamente (rutas, SSR), y reglas
generales de seguridad. Si tu app usa algo más (GraphQL, un ORM
específico), agregá el ruleset correspondiente al `semgrep_configs` del
`with:`.
## Modo auditoría por default
Igual que los 3 pipelines reales de los que sale este arquetipo:
Semgrep y Trivy IaC corren pero NO bloquean por default (primera vuelta
para revisar hallazgos antes de decidir qué debería bloquear). Trivy
imagen (CRITICAL) sí bloquea siempre, no es opcional.
@@ -1,193 +0,0 @@
# 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 "========================================"
+66
View File
@@ -0,0 +1,66 @@
# Arquetipo: API Node/Express (sin SSR)
Cuándo usar este arquetipo: una API backend en Node.js/TypeScript (Express,
Fastify, Medusa, NestJS, etc.) que expone endpoints HTTP, sin código de
UI propio. Si tu app renderiza páginas del lado del servidor, usá
`archetype-nextjs-frontend` en cambio.
Qué asume de tu repo:
- Un `Dockerfile` en `app_dir` basado en (o compatible con)
`dockerfiles/node-api.Dockerfile`.
- Un endpoint `/health` (o el que definas en tu overlay de
`k8s-manifests`) que devuelva 200.
- `cosign.pub` commiteado en `app_dir`.
- Un manifiesto K8s (`manifest_file`) con la imagen a actualizar tras
cada build.
## Dónde vive el archivo real
Igual que los otros 2 arquetipos: el `workflow_call` tiene que estar
bajo `.gitea/workflows/` (restricción real de Gitea, ver
`pipelines/archetype-nextjs-frontend/README.md` para el detalle
completo del hallazgo). Archivo real:
```
.gitea/workflows/archetype-node-api.yaml
```
## Cómo usarlo desde tu repo
```yaml
jobs:
build:
uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/archetype-node-api.yaml@main
with:
app_dir: api
image_name: gitea.cruzcloud.net/devops/mi-api
manifest_file: workloads/mi-api/deployment.yaml
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
```
Mismas advertencias que el arquetipo de Next.js sobre `uses:` con URL
absoluta y sobre fijar `@main` vs un tag/commit — no las repito acá, ver
ese README.
## Semgrep: por qué este ruleset
`p/typescript,p/security-audit,p/owasp-top-ten` — sin `p/react`/`p/nextjs`
(no hay código de UI), con OWASP Top 10 sumado porque una API expuesta
públicamente es exactamente el tipo de superficie que ese ruleset cubre
(inyección, auth rota, etc.).
## Sobre `npm`/`npx` en runtime
`dockerfiles/node-api.Dockerfile` borra `npm` de la imagen de runtime a
propósito (reduce superficie de CVEs — ver comentario en el propio
Dockerfile). Si tu app tiene un init container o un job que corre
`npx algo`, va a fallar contra esta imagen. Invocá el binario/CLI
directo con `node`, como hace el `CMD` del propio Dockerfile — no
agregues npm de vuelta solo para eso (esto rompió un pipeline real del
lab una vez, ver `docs-portal` → playbooks → contención de recursos y
el propio commit histórico de `commerce-backend` sobre este tema).
@@ -1,193 +0,0 @@
# 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 "========================================"
+65
View File
@@ -0,0 +1,65 @@
# Arquetipo: sitio estático (nginx)
Cuándo usar este arquetipo: un sitio generado a HTML estático (MkDocs,
Hugo, Astro, un build de Vite sin backend) servido por nginx, sin
runtime de aplicación. Si tu app necesita ejecutar código en cada
request, no es este arquetipo.
Qué asume de tu repo:
- Un `Dockerfile` en `app_dir` basado en (o compatible con)
`dockerfiles/static-nginx.Dockerfile`.
- `cosign.pub` commiteado en `app_dir`.
- Un manifiesto K8s (`manifest_file`, el Deployment) con la imagen a
actualizar tras cada build.
Diferencia real con los otros 2 arquetipos: Gitleaks y Trivy IaC
escanean todo `app_dir` (no un único archivo) — un sitio estático suele
tener varios manifiestos K8s por separado (deployment/service/ingress)
en vez de uno solo con todo adentro.
## Dónde vive el archivo real
Misma restricción de Gitea que los otros 2 arquetipos (ver
`pipelines/archetype-nextjs-frontend/README.md` para el detalle
completo). Archivo real:
```
.gitea/workflows/archetype-static-site.yaml
```
## Cómo usarlo desde tu repo
```yaml
jobs:
build:
uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/archetype-static-site.yaml@main
with:
app_dir: sitio
image_name: gitea.cruzcloud.net/devops/mi-sitio
manifest_file: workloads/mi-sitio/deployment.yaml
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
```
Mismas advertencias sobre `uses:` con URL absoluta y `@main` vs
tag/commit — ver el README del arquetipo de Next.js.
## Semgrep: por qué este ruleset
`p/python,p/security-audit` — asume MkDocs (Python). Si tu generador es
Hugo/Astro (Go/JS), este ruleset no aplica realmente — ajustalo al
lenguaje real de tu build (o quitá el step si tu sitio no tiene ningún
código ejecutable propio, solo contenido).
## docs-portal real todavía no usa nginx no-root
`dockerfiles/static-nginx.Dockerfile` corre nginx sin privilegios
(`nginxinc/nginx-unprivileged`) — mejora encontrada al armar este
template. El `docs-portal` real del lab **todavía** corre
`nginx:1.27-alpine` estándar (root), es un gap real, pendiente de
aplicar por separado (no forma parte de la migración de manifiestos de
esta tarea — cambiar la imagen es un cambio de app aparte).
@@ -1,195 +0,0 @@
# 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 "========================================"