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:
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 "========================================"
|
||||
@@ -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 "========================================"
|
||||
@@ -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 "========================================"
|
||||
Reference in new issue
Block a user