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
@@ -1,12 +0,0 @@
|
||||
name: TEST - verificacion de reusable workflow + composite action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
call-reusable:
|
||||
uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/_test-reusable.yaml@main
|
||||
with:
|
||||
greeting: "hola desde el caller"
|
||||
@@ -1,17 +0,0 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
greeting:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main
|
||||
with:
|
||||
source: devsecops-snippets
|
||||
- shell: bash
|
||||
run: echo "Reusable workflow input recibido -> '${{ inputs.greeting }}'"
|
||||
+6
-6
@@ -49,7 +49,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main
|
||||
with:
|
||||
source: ${{ inputs.app_dir }}
|
||||
|
||||
@@ -89,11 +89,11 @@ jobs:
|
||||
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
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-iac-step@main
|
||||
with:
|
||||
target: ${{ env.MANIFEST_FILE }}
|
||||
|
||||
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/semgrep-step@main
|
||||
with:
|
||||
target: ${{ env.APP_DIR }}
|
||||
configs: ${{ inputs.semgrep_configs }}
|
||||
@@ -118,12 +118,12 @@ jobs:
|
||||
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main
|
||||
- uses: https://gitea.cruzcloud.net/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
|
||||
- uses: https://gitea.cruzcloud.net/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 }}
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
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
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
|
||||
with:
|
||||
image_name: ${{ env.IMAGE_NAME }}
|
||||
digest: ${{ steps.push.outputs.digest }}
|
||||
+6
-6
@@ -47,7 +47,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main
|
||||
with:
|
||||
source: ${{ inputs.app_dir }}
|
||||
|
||||
@@ -87,11 +87,11 @@ jobs:
|
||||
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
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-iac-step@main
|
||||
with:
|
||||
target: ${{ env.MANIFEST_FILE }}
|
||||
|
||||
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/semgrep-step@main
|
||||
with:
|
||||
target: ${{ env.APP_DIR }}
|
||||
configs: ${{ inputs.semgrep_configs }}
|
||||
@@ -118,12 +118,12 @@ jobs:
|
||||
# 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
|
||||
- uses: https://gitea.cruzcloud.net/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
|
||||
- uses: https://gitea.cruzcloud.net/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 }}
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
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
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
|
||||
with:
|
||||
image_name: ${{ env.IMAGE_NAME }}
|
||||
digest: ${{ steps.push.outputs.digest }}
|
||||
+6
-6
@@ -53,7 +53,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main
|
||||
with:
|
||||
source: ${{ inputs.app_dir }}
|
||||
|
||||
@@ -93,11 +93,11 @@ jobs:
|
||||
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
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-iac-step@main
|
||||
with:
|
||||
target: ${{ env.APP_DIR }}
|
||||
|
||||
- uses: devops/platform-templates/devsecops-snippets/semgrep-step@main
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/semgrep-step@main
|
||||
with:
|
||||
target: ${{ env.APP_DIR }}
|
||||
configs: ${{ inputs.semgrep_configs }}
|
||||
@@ -120,12 +120,12 @@ jobs:
|
||||
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main
|
||||
- uses: https://gitea.cruzcloud.net/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
|
||||
- uses: https://gitea.cruzcloud.net/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 }}
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
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
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main
|
||||
with:
|
||||
image_name: ${{ env.IMAGE_NAME }}
|
||||
digest: ${{ steps.push.outputs.digest }}
|
||||
@@ -1 +1,205 @@
|
||||
# platform-templates
|
||||
|
||||
Patrones reutilizables para dar de alta una app nueva en el lab
|
||||
CruzCloud (ZimaOS + k3d + Gitea + Argo CD) sin escribir el pipeline y
|
||||
los manifiestos K8s desde cero cada vez. Copiás una plantilla, completás
|
||||
unos pocos valores, y tenés: pipeline de CI con el gate completo de
|
||||
DevSecOps (Gitleaks, Trivy, Semgrep, Syft, Cosign) + manifiestos K8s con
|
||||
buenas prácticas por default (resource limits, probes, non-root).
|
||||
|
||||
**Para quién es esto:** si estás dando de alta una app nueva en el lab,
|
||||
seguí el ejemplo de abajo. Si estás migrando una app existente, mirá el
|
||||
punto "Migración de apps existentes" más abajo, y andá con más cuidado
|
||||
— tocás algo que ya está en producción.
|
||||
|
||||
## Mapa del repo
|
||||
|
||||
```
|
||||
pipelines/ -- documentación de cada arquetipo de pipeline
|
||||
(el archivo real que se invoca vive en
|
||||
.gitea/workflows/, ver por qué más abajo)
|
||||
.gitea/workflows/ -- los reusable workflows reales (workflow_call)
|
||||
dockerfiles/ -- Dockerfiles genéricos, un ARG por cosa a cambiar
|
||||
k8s-manifests/base/ -- Kustomize base: Deployment+Service+Ingress
|
||||
genéricos, con securityContext/probes/resources
|
||||
ya puestos
|
||||
k8s-manifests/overlays-ejemplo/ -- el ejemplo de abajo, ya armado
|
||||
devsecops-snippets/ -- los 6 composite actions que arman cada pipeline
|
||||
```
|
||||
|
||||
## Hallazgo importante sobre Gitea Actions (leer antes de copiar nada)
|
||||
|
||||
Este repo se armó verificando en vivo, no asumiendo que Gitea Actions es
|
||||
100% igual a GitHub Actions. Dos diferencias reales, confirmadas
|
||||
corriendo pipelines de verdad contra esta instancia:
|
||||
|
||||
1. **Los reusable workflows (`workflow_call`) tienen que vivir bajo
|
||||
`.gitea/workflows/`** — no en una carpeta cualquiera. Por eso
|
||||
`pipelines/archetype-*/` solo tiene un `README.md`, el archivo
|
||||
invocable real está en `.gitea/workflows/archetype-*.yaml`.
|
||||
2. **Toda referencia cross-repo en un `uses:` necesita la URL completa**
|
||||
(`https://gitea.cruzcloud.net/owner/repo/path@ref`), no la forma
|
||||
corta de GitHub (`owner/repo/path@ref`) — esa forma corta se resuelve
|
||||
contra **github.com** por default en esta instancia (confirmado:
|
||||
intentar usarla hace que el runner intente clonar
|
||||
`github.com/devops/platform-templates`, que no existe, y falle con
|
||||
"Repository not found"). `actions/checkout@v3` sigue andando con la
|
||||
forma corta porque ESE sí existe en github.com de verdad.
|
||||
|
||||
## Ejemplo end-to-end: dar de alta "mi-app-ejemplo"
|
||||
|
||||
Pensado para seguirse sin conocer nada más del lab que "existe Gitea en
|
||||
gitea.cruzcloud.net y un cluster k3d con Argo CD sincronizando desde
|
||||
ahí". Asumimos una API Node (arquetipo `node-api`) — para Next.js o un
|
||||
sitio estático es el mismo procedimiento, cambiando el arquetipo.
|
||||
|
||||
### 1. Creá el repo de tu app en Gitea
|
||||
|
||||
Vía la UI (`https://gitea.cruzcloud.net/repo/create`) o `gh`/API si
|
||||
tenés token con el scope correcto. Nombre: `mi-app-ejemplo`.
|
||||
|
||||
### 2. Copiá el Dockerfile
|
||||
|
||||
```bash
|
||||
curl -o Dockerfile \
|
||||
https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/dockerfiles/node-api.Dockerfile
|
||||
```
|
||||
|
||||
Ajustá los `ARG` del inicio del archivo a tu caso (puerto real, comando
|
||||
de arranque) — están documentados inline, no hace falta adivinar.
|
||||
|
||||
### 3. Agregá el pipeline
|
||||
|
||||
`.gitea/workflows/build.yaml` en tu repo:
|
||||
|
||||
```yaml
|
||||
name: Build and Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'Dockerfile'
|
||||
- '.gitea/workflows/build.yaml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/archetype-node-api.yaml@main
|
||||
with:
|
||||
app_dir: .
|
||||
image_name: gitea.cruzcloud.net/devops/mi-app-ejemplo
|
||||
manifest_file: k8s/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 }}
|
||||
```
|
||||
|
||||
Los 4 secrets tienen que existir en tu repo (Settings → Actions →
|
||||
Secrets). Si tu lab ya los tiene a nivel de organización/compartidos,
|
||||
puede que no haga falta redeclararlos — depende de cómo esté armado tu
|
||||
Gitea.
|
||||
|
||||
### 4. Generá tu par de llaves de Cosign (una vez)
|
||||
|
||||
```bash
|
||||
cosign generate-key-pair
|
||||
# genera cosign.key (privada) y cosign.pub (pública)
|
||||
```
|
||||
|
||||
- `cosign.pub` → commiteala en tu repo (es pública a propósito).
|
||||
- `cosign.key` → **no la commitees**. Subila como secret
|
||||
`COSIGN_PRIVATE_KEY` (contenido del archivo) y la password que
|
||||
elegiste como secret `COSIGN_PASSWORD`.
|
||||
|
||||
### 5. Manifiestos K8s: copiá el base y armá tu overlay
|
||||
|
||||
```bash
|
||||
mkdir -p k8s/patches
|
||||
curl -o k8s/kustomization.yaml \
|
||||
https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/k8s-manifests/overlays-ejemplo/mi-app-ejemplo/kustomization.yaml
|
||||
curl -o k8s/patches/deployment.yaml \
|
||||
https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/k8s-manifests/overlays-ejemplo/mi-app-ejemplo/patches/deployment.yaml
|
||||
curl -o k8s/patches/service.yaml \
|
||||
https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/k8s-manifests/overlays-ejemplo/mi-app-ejemplo/patches/service.yaml
|
||||
curl -o k8s/patches/ingress.yaml \
|
||||
https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/k8s-manifests/overlays-ejemplo/mi-app-ejemplo/patches/ingress.yaml
|
||||
```
|
||||
|
||||
Esto ya viene armado con el patrón real (ver
|
||||
`k8s-manifests/overlays-ejemplo/mi-app-ejemplo/` en este mismo repo) —
|
||||
solo tenés que:
|
||||
|
||||
1. Cambiar `mi-app-ejemplo` por el nombre real de tu app en los 3
|
||||
`patches/*.yaml` y en el `kustomization.yaml` (nombre del
|
||||
Deployment/Service/Ingress, `host:`, `namespace:`).
|
||||
2. Ajustar imagen, puerto, y ruta del healthcheck a los reales de tu
|
||||
app.
|
||||
3. **Leer los comentarios de `patches/deployment.yaml` sobre
|
||||
`$patch: replace`/JSON6902** antes de tocar `ports` — Kustomize NO
|
||||
reemplaza esa lista con un patch de merge normal, la duplica
|
||||
(confirmado en vivo armando este repo). Si tu puerto real es
|
||||
distinto al 8080 del base, el rename+ports va en un patch JSON6902
|
||||
aparte en tu `kustomization.yaml`, exactamente como en el ejemplo.
|
||||
4. **Si tu app escucha en un puerto <1024** (80, por ejemplo): el base
|
||||
saca TODAS las capabilities del contenedor por default —
|
||||
necesitás agregar `capabilities.add: [NET_BIND_SERVICE]` en tu
|
||||
patch, o el bind falla. Ver el comentario en
|
||||
`k8s-manifests/overlays-ejemplo/mi-app-ejemplo/kustomization.yaml`
|
||||
si tu imagen corre como root en vez de un usuario propio (necesitás
|
||||
además `runAsNonRoot: false` a nivel pod).
|
||||
|
||||
### 6. Validá el resultado ANTES de commitear
|
||||
|
||||
```bash
|
||||
kubectl kustomize k8s/
|
||||
```
|
||||
|
||||
Revisá que el YAML que sale tenga sentido — nombre, imagen, puerto,
|
||||
host correctos, sin recursos duplicados. Si algo se ve raro (dos
|
||||
`ports:`, dos contenedores), revisá el punto 3 de arriba.
|
||||
|
||||
### 7. Creá la Application de Argo CD
|
||||
|
||||
Sigue el patrón de este lab: una Application de workload
|
||||
(`syncPolicy` con `prune: true, selfHeal: true`) apuntando al
|
||||
`k8s/` de tu repo. Preguntale a quien mantiene `apps-registry` cómo se
|
||||
registra — no es parte de este repo.
|
||||
|
||||
### 8. Push, mirá el pipeline correr, y listo
|
||||
|
||||
Un `git push` a `main` dispara Gitleaks → build → Trivy → Semgrep → Syft
|
||||
→ Cosign → push al registry → Argo CD sincroniza. Si algo bloquea
|
||||
(CRITICAL de Trivy, firma de Cosign inválida), el log del job te dice
|
||||
exactamente cuál.
|
||||
|
||||
## Migración de apps existentes
|
||||
|
||||
Si estás moviendo una app que **ya está corriendo en producción** hacia
|
||||
este patrón base+overlay: no es lo mismo que dar de alta una nueva.
|
||||
Antes de tocar nada:
|
||||
|
||||
1. Guardá el manifest actual desplegado (`kubectl get deploy <nombre>
|
||||
-n <ns> -o yaml`) para comparar.
|
||||
2. Armá el overlay apuntando **al mismo nombre** que ya existe (usando
|
||||
los patches JSON6902 de rename, como en el ejemplo) — no dejes que
|
||||
Kustomize invente un nombre nuevo, rompe el tracking de Argo CD y
|
||||
la resolución DNS de otros servicios que dependan del nombre viejo.
|
||||
3. **Revisá con qué usuario corre tu imagen actual.** Si no tiene un
|
||||
`USER` no-root en su Dockerfile, el `runAsNonRoot: true` del base
|
||||
rompe el pod (`CreateContainerConfigError`) — hace falta un override
|
||||
explícito hasta que la imagen se actualice aparte. Pasó exactamente
|
||||
esto migrando `docs-portal` en este mismo lab.
|
||||
4. Corré `kubectl kustomize` sobre tu overlay y compará el resultado
|
||||
contra el manifest guardado en el paso 1 antes de dejar que Argo CD
|
||||
sincronice nada.
|
||||
|
||||
## Documentación ampliada
|
||||
|
||||
`docs.cruzcloud.net` tiene una sección dedicada a este repo con un
|
||||
diagrama del flujo completo y el detalle de la migración real que se
|
||||
hizo sobre `ecommerce-frontend`, `commerce-backend` y `docs-portal`.
|
||||
@@ -0,0 +1,66 @@
|
||||
# DevSecOps snippets — composite actions
|
||||
|
||||
Cada subcarpeta es un *composite action* real de Gitea Actions
|
||||
(`action.yml`), no un fragmento de YAML para copiar/pegar — se invocan
|
||||
con `uses:` como cualquier action de GitHub.
|
||||
|
||||
**Nota sobre la estructura:** el pedido original de este repo era una
|
||||
lista plana (`gitleaks-step.yaml`, `trivy-image-step.yaml`, etc.). Se
|
||||
ajustó a `<nombre>/action.yml` porque es el único layout que Gitea (y
|
||||
GitHub) reconocen como composite action invocable — un archivo con
|
||||
nombre libre no se puede referenciar con `uses:`, tiene que ser
|
||||
literalmente `action.yml` (o `action.yaml`) en su propia carpeta.
|
||||
|
||||
## Cómo invocar uno desde tu pipeline
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main
|
||||
with:
|
||||
source: mi-app/
|
||||
```
|
||||
|
||||
**La URL absoluta (`https://gitea.cruzcloud.net/...`) no es opcional.**
|
||||
La forma corta de GitHub (`devops/platform-templates/...@main`) se
|
||||
resuelve contra **github.com** por default en esta instancia de Gitea
|
||||
(`DEFAULT_ACTIONS_URL` sin configurar = `github`), no contra el propio
|
||||
Gitea — confirmado en vivo armando este repo: con la forma corta, el
|
||||
runner intentó clonar `https://github.com/devops/platform-templates`
|
||||
(no existe) y falló con "Repository not found". Con la URL absoluta
|
||||
`https://gitea.cruzcloud.net/...` sí resuelve correctamente contra la
|
||||
instancia local. Ver
|
||||
`../pipelines/archetype-nextjs-frontend/README.md` para más contexto.
|
||||
|
||||
## Los 6 snippets
|
||||
|
||||
| Snippet | Qué hace | Bloquea por default |
|
||||
|---|---|---|
|
||||
| `gitleaks-step` | Secretos commiteados en el árbol de archivos | Sí |
|
||||
| `trivy-iac-step` | Misconfiguraciones en manifiestos K8s | No (auditoría) |
|
||||
| `trivy-image-step` | CVEs de la imagen construida | CRITICAL sí, HIGH no |
|
||||
| `semgrep-step` | SAST, ruleset configurable por `with.configs` | No (auditoría) |
|
||||
| `syft-sbom-step` | Genera y publica el SBOM de la imagen | N/A (no bloquea) |
|
||||
| `cosign-sign-verify-step` | Firma por digest + smoke test de verificación | Sí (firma inválida = falla) |
|
||||
|
||||
Cada `action.yml` tiene sus `inputs:` documentados inline — no hay
|
||||
READMEs individuales por snippet, son chicos y el propio archivo ya
|
||||
explica cada input.
|
||||
|
||||
## `cosign-sign-verify-step`: un composite action no puede leer `secrets`
|
||||
|
||||
Restricción real de Gitea/GitHub Actions, no de este repo: un composite
|
||||
action **no tiene acceso directo** al contexto `secrets` del caller. El
|
||||
workflow que lo invoca tiene que pasarlos explícitos como `with:`:
|
||||
|
||||
```yaml
|
||||
- uses: https://gitea.cruzcloud.net/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 }}
|
||||
```
|
||||
|
||||
Los 3 arquetipos de `pipelines/` ya lo hacen así — es solo relevante si
|
||||
armás tu propio pipeline a mano usando estos snippets sueltos.
|
||||
@@ -18,8 +18,7 @@ inputs:
|
||||
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.
|
||||
archivo — no es un mecanismo para silenciar sin revisar.
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
Reference in new issue
Block a user