CI/CD Pipeline
Todo despliegue sale de GitHub Actions — nunca builds manuales en el servidor. El flujo de ramas
es feature → PR a developer → deploy a dev → PR developer → main
→ deploy a producción. El monorepo zentto-web tiene 21 workflows; el resto de
repos del ecosistema replica el mismo patrón.
graph LR
F[feature branch] --> PRD[PR a developer]
PRD --> DEV[Deploy DEV
apidev/appdev.zentto.net] DEV --> PRM[PR developer → main] PRM --> PROD[Deploy PROD
api/app.zentto.net] style F fill:#6C63FF,color:#fff style DEV fill:#f59e0b,color:#fff style PROD fill:#10b981,color:#fff
apidev/appdev.zentto.net] DEV --> PRM[PR developer → main] PRM --> PROD[Deploy PROD
api/app.zentto.net] style F fill:#6C63FF,color:#fff style DEV fill:#f59e0b,color:#fff style PROD fill:#10b981,color:#fff
Modelo de runners (dual)
| Qué | Runner | Por qué |
|---|---|---|
| Deploys, tests, builds Docker, migraciones | self-hosted [self-hosted, zentto-ci] — 6 runners dellxeon-ci-* en el LXC dev del centro de datos dellxeon | Costo cero de minutos GitHub; acceso directo al ambiente dev |
Escaneo de seguridad (reusable zentto-erp/.github/security.yml) | hosted ubuntu-latest | En el runner propio clogaba la cola de deploys; en hosted consume minutos → debe ser eficiente (diff-aware en PR) |
| Publish de paquetes npm, agente Claude | hosted ubuntu-22.04/latest | Aislamiento de credenciales npm |
El escaneo de seguridad es advisory: reporta al Security tab pero no bloquea
merges ni deploys (no es required check). Antes de pushear se corre en local
(gitleaks + trivy, ~8 s) para llegar verde al primer intento.
Workflows principales de zentto-web
| Workflow | Trigger | Qué hace |
|---|---|---|
deploy-api.yml | PR a main + dispatch | PROD API: type-check (incluye audit:openapi) → schema-tests (SP Contract Tests contra PG de servicio) → build+push ghcr → migrate-pg (goose sobre la BD principal y luego las BDs dedicadas de tenant, no bloqueante) → deploy SSH |
deploy-dev-api.yml | push a developer | DEV API → apidev.zentto.net, imagen :dev, BD zentto_dev |
deploy-frontend.yml / deploy-dev-frontend.yml | PR a main / push a developer | Frontend prod/dev (buildx con cache local + push + SSH) |
deploy-panel.yml / deploy-dev-panel.yml | paths apps/panel/** | Deploy del panel (app con ciclo propio) |
test.yml | push + PR | type-check + audit OpenAPI + SP contracts + smoke tests |
migrations-guard.yml | PR | Detecta versiones goose duplicadas entre PRs |
db-governance.yml | PR + push main | Reporte de gobernanza de BD |
audit-api-shapes.yml | PR | Consistencia de shapes API (warning, no bloquea) |
security.yml | push/PR + cron semanal | Consume el reusable de la org (gitleaks + trivy + semgrep), fail-on: high, advisory |
hotfix.yml | dispatch | Reinicia contenedores sin rebuild + re-aplica nginx |
publish-*.yml | push main por paths | Publican paquetes npm (design-tokens, erp-ux, landing-kit, vertical-layout, platform-client) |
Dev vs Prod
- Dev: push a
developerdespliega al ambiente dev, que vive en el LXC 101 de dellxeon (secret de orgSSH_HOST_DEV). Hostnamesapidev/appdev.zentto.netpor túnel Cloudflare. - Prod: merge del PR
developer → maindespliega a Hetzner (secretSSH_HOST). - ⚠️ Un hostname dev nuevo necesita CNAME explícito al túnel: sin él resuelve al wildcard → Hetzner (dev apagado) → 502 fantasma con deploy verde.
Migraciones en el deploy
- El job
migrate-pgejecutagoose -allow-missing upsobre la BD principal (las migraciones viajan dentro de la imagen de la API). - Después,
migrate-tenant-dbs.shrecorresys.TenantDatabasey migra cada BD dedicada (no bloqueante: un tenant roto se reporta pero no aborta el deploy). -allow-missingtolera migraciones out-of-order de PRs concurrentes.
Secrets relevantes
| Secret | Uso |
|---|---|
SSH_HOST / SSH_HOST_DEV | Producción (Hetzner) / dev (LXC dellxeon, secret de organización) |
SSH_USER, SSH_PRIVATE_KEY | Conexión de deploy |
GITHUB_TOKEN | ghcr.io (packages: write) |
NPM_TOKEN | Instalar/publicar los @zentto/* privados |
AUTH_SECRET, PG_* | Runtime frontend / migraciones |
Reglas de eficiencia (obligatorias en todo workflow)
paths:para no disparar deploys irrelevantes;concurrencyconcancel-in-progress: truepara matar runs viejos.timeout-minutesen todo job (deploys ≤ 30, scans ≤ 20, lint ≤ 10) — un job colgado ocupa el runner hasta 6 h.- Cache de buildx local en el runner propio
(
type=local,dest=/opt/buildx-cache—type=ghafalla en self-hosted). - En condiciones
ifde jobs: solovarsen modo opt-in (vars.X == 'true'), nuncasecrets(no están disponibles ahí) ni opt-out (!= 'false'). - Service containers de PostgreSQL con puerto efímero
(
ports: ['5432']+job.services.postgres.ports['5432']): el runner comparte máquina con un PG local en 5432 — un mapeo fijo colisiona siempre. - Acciones de terceros con SSH a prod pinneadas por SHA (supply-chain).
Diagnóstico de fallos típicos
| Síntoma | Causa probable |
|---|---|
| El workflow falla en 1-3 segundos | YAML inválido o budget de Actions agotado — no es el código |
| Deploy verde pero el cambio no está | El workflow no reconstruyó la imagen (paths/condición) — verificar que el job de build corrió de verdad |
| Re-run no toma un fix del reusable | Re-run no re-resuelve workflows reusables: hacer un push nuevo |
| "failed to reserve cache" | Se usó type=gha en el runner propio — usar cache local |
| Migración goose "saltada" | Número menor mergeado después de uno mayor — -allow-missing la aplica; verificar orden en developer |
IMPORTANTE: nunca commit directo a
main ni developer;
nunca docker build/run manual en el servidor. Si Actions está caído y hay una
emergencia, existe un procedimiento de deploy manual documentado — pero jamás admin-merge para
"saltarse" el CI.