Saltar a contenido

Pipeline de despliegue con GitHub Actions

Análisis, decisiones y plan por fases para automatizar el despliegue del CMS: imagen construida en CI, publicada en GHCR y desplegada por un runner self-hosted, reemplazando el procedimiento manual del runbook —«no hay pipeline de build ni de deploy»—. Es el documento hermano del análisis equivalente del storefront y reutiliza su arquitectura donde aplica; aquí se documenta lo que es distinto en el CMS.

Alcance. Los hallazgos del Diagnóstico están medidos contra dependencias-npm el 27/08/2026 (Payload 3.88.0, Next 15.4.11, ADR-0004), no inferidos. La fase 0 se ejecutó ese mismo día y su resultado quedó en el ADR-0005.

Respuesta corta

Las decisiones que fijan la forma del pipeline están tomadas. Donde este documento no dice lo contrario, aplica el estándar CI/CD de Compulandia (Guía de configuración CI/CD con runners self-hosted, versión 26/08/2026): secretos en GCP Secret Manager, staging automático por push a la rama staging (el Environment staging solo acepta esa rama), producción por tag v*, GHCR autenticado con el token del job.

Pregunta Decisión Por qué
¿Cómo llegan los cambios de esquema a producción? Solo payload migrate con migraciones commiteadas; el modo push jamás sale de dev El push de drizzle pide confirmación interactiva ante warnings (pushDevSchema): sin TTY se cuelga — verificado el 26/08/2026 con el test de integración. Además puede destruir datos sin dejar registro
¿El deploy incluye migraciones? Sí, aplicadas por el propio arranque del contenedor (ADR-0006) La imagen standalone no incluye el CLI payload migrate. prodMigrations (con la compuerta PAYLOAD_MIGRATE_ON_INIT=true, que solo definen los servidores) las aplica al inicializar, antes de servir tráfico de Payload
¿Dónde vive la base de datos? Fuera del compose, en una VM dedicada de PostgreSQL El pipeline nunca gestiona la base, solo se conecta. El control de acceso por red de esa VM (solo los hosts del CMS) reemplaza al usuario sin DDL que el ADR-0006 vuelve inviable
¿Imagen única o por entorno? Una imagen por entorno NEXT_PUBLIC_SERVER_URL se inlinea en el build, y las páginas SSG del frontend heredado se prerenderizan con el contenido de la base usada al construir
¿Dónde se construye la imagen? En CI (ubuntu-latest) + GHCR El build ya no necesita la base — resuelto en la fase 0, ADR-0005
¿Cómo se publica producción? Por tag v* sobre main, con doble guard Estándar CI/CD: el tag es el acto deliberado y versionado — reemplaza la aprobación manual por revisores. El rollback se lee «volvé a la v1.2.4», no un hash
¿Dónde viven variables y secretos? GCP Secret Manager: un secret por entorno con el .env completo Estándar CI/CD: versionado, valores anteriores legibles, y una sola fuente para build y runtime. El .env del host pasa a ser generado por el deploy

Resuelto: el build ya no necesita PostgreSQL

Medido el 27/08/2026: next build fallaba con la base inalcanzable porque las páginas del frontend heredado consultan contenido al compilar. Se protegieron los tres generateStaticParams (devuelven lista vacía sin base; las páginas se generan bajo demanda) y la portada y el listado de posts pasaron a renderizado bajo demanda. Con eso el build pasa completo sin base, y DATABASE_URI y PAYLOAD_SECRET dejaron de ser argumentos de build. Alternativas consideradas y descartadas: en el ADR-0005.

Diagnóstico

Hallazgo Evidencia Impacto en el pipeline
~~El build requiere la base alcanzable~~ Resuelto 27/08/2026 (ADR-0005) El job de build puede correr en ubuntu-latest
~~Los build-args llevan secretos~~ Resuelto 27/08/2026: DATABASE_URI y PAYLOAD_SECRET ya no son ARG; solo runtime docker history queda limpio
Las migraciones no se aplican solas CMD = node server.js; runbook, Deploy El job de deploy necesita el paso migrate explícito, y ya hay una pendiente (payload_kv)
No hay endpoint de salud propio Solo existen las rutas del template; /admin inicializa Payload → depende de la base El runner no puede distinguir «CMS caído» de «base caída»
No hay registry ni tags El host construye con docker compose build Rollback = recompilar (y re-migrar): minutos, no segundos
Sin healthcheck en compose docker-compose.yml, servicio payload up -d reporta éxito aunque el proceso muera al arrancar
El host de deploy no está en el repo Runbook, Deploy No hay dónde instalar el runner todavía
CORS del storefront hardcodeado Array cors en src/payload.config.ts Un dominio nuevo de storefront exige build nuevo, no un cambio de config

Lo que ya está resuelto (y en el storefront costó una fase entera): la imagen usa node:22-alpine, output: standalone y usuario no-root; existe .dockerignore y excluye .env*; el artefacto corre Payload 3.88 + Next 15.4.11 sin vulnerabilidades críticas (ADR-0004); el test de integración está en verde tras mapear el valor huérfano custom (commit 2a9e455).

Requerimientos

R1 · El artefacto

ID Requerimiento Estado
R1.1 Base node:22-alpine, standalone, non-root, .dockerignore listo
R1.2 Ningún secreto como ARG: BuildKit secrets o build sin base listo
R1.3 El build no depende de la base (ADR-0005) listo
R1.4 Endpoint /api/health sin tocar la base, con SHA del commit listo
R1.5 healthcheck en docker-compose.yml listo

R2 · El pipeline

ID Requerimiento Estado
R2.1 Workflow build + push a GHCR: tag staging-<sha> para staging, v<semver> para producción; login con el GITHUB_TOKEN del job (nunca PAT) verificado 27/08/2026: imágenes staging-* publicadas en GHCR
R2.2 rama staging → staging automático (develop es integración, no despliega); producción por tag v* verificado en ambos carriles (staging 27/08, producción v1.0.0 28/08)
R2.3 Job de deploy: pull → up -d → smoke en dos pasos: /api/health (la app vive) y /admin (fuerza el init de Payload → aplica y verifica migraciones, ADR-0006) verificado en ambos entornos
R2.4 Doble guard de producción: deployment tag policy v* en el Environment + git merge-base --is-ancestor contra main en el job (los tags no pertenecen a ramas: hacen falta los dos) verificado — la regla rechazó un deploy desde develop el 27/08 y aceptó v1.0.0
R2.5 workflow_dispatch con versión para rollback/redeploy pendiente
R2.6 concurrency por entorno, sin cancelar el deploy en curso listo

R3 · Entorno y secretos

ID Requerimiento Estado
R3.1 Secrets cms-staging-env / cms-prod-env en GCP Secret Manager, cada uno con el .env completo (DATABASE_URI, PAYLOAD_SECRET, CRON_SECRET, PREVIEW_SECRET, credenciales Cloudflare, NEXT_PUBLIC_SERVER_URL) listo (staging v3 con secretos reales; prod operativo)
R3.2 Workload Identity Federation configurado (pool, provider con attribute-condition por organización, cuenta de servicio atada al repo) y variables GCP_* en el repositorio listo — a nivel de organización
R3.3 El job de deploy escribe el .env del host desde el secret: editarlo por SSH deja de tener efecto verificado en ambos hosts (/opt/payload-cms)
R3.4 El build extrae del secret solo las variables públicas (NEXT_PUBLIC_* — aquí una sola: NEXT_PUBLIC_SERVER_URL) como build-args verificado
R3.5 Acceso a la base restringido por red (única barrera restante: con el ADR-0006 la credencial de la app tiene DDL). Producción (GCP): ya configurado. Staging (VM local, alcanzable desde toda la red): decidir si se restringe en la VM o se acepta el riesgo — el accidente npm run dev apuntado a esa base es el mismo que corrompió la base de dev staging: a decidir
R3.6 Environments staging/production en GitHub; production con la tag policy v* (disponible en plan Team para repos privados) listo — y staging con regla de rama

La protección contra un next dev apuntado a otra base es la red

El modo push se activa por cómo corre Payload (dev), no contra qué base: un next dev con la URI equivocada en el .env intenta alterar el esquema de esa base. Como el ADR-0006 exige que la credencial de la app tenga DDL (quien migra es la app), la salvaguarda no puede ser el usuario de base — es la red. En producción (GCP) ya está restringida. En staging (VM local alcanzable desde toda la red) es una decisión abierta (R3.5); el accidente ahí es real: es el mismo que corrompió la base de dev y rompió el test de integración.

R4 · Rollback

ID Requerimiento Estado
R4.1 Rollback por tag: elegir tag anterior → pull → up -d las imágenes por versión ya existen en GHCR; falta el botón (workflow_dispatch, fase 4)
R4.2 Política de reversión de migraciones documentada por release pendiente

El rollback del CMS tiene un paso más que el del storefront

Volver el código atrás no revierte el esquema. La regla del runbook se mantiene: si la versión que se baja aplicó una migración, decidir entre down o esquema adelantado compatible. El pipeline lo hace visible: el job de deploy registra qué migraciones aplicó.

Plan de implementación

Fase 0 · Responder si el build puede funcionar sin base — hecha (27/08/2026)

El build pasa sin base: guardas en los 3 generateStaticParams, portada y listado de posts en renderizado bajo demanda, y DATABASE_URI/PAYLOAD_SECRET fuera de los argumentos de build. Decisión y consecuencias en el ADR-0005. El carril de build queda: ubuntu-latest + GHCR.

Fase 1 · Señal de salud — hecha (27/08/2026)

/api/health responde 200 sin importar nada de Payload, devolviendo el SHA del commit (build-arg GIT_SHA, lo pasa el workflow) y la versión; healthcheck agregado al servicio en compose. Criterio verificado: con Postgres caído a propósito, /api/health respondió 200 y /admin 500 — dos señales distintas. Con la base real, la API REST de Payload convive con la ruta nueva.

Fase 2 · Construir y publicar — hecha y verificada (27/08/2026)

Workflow imagen.yml según el estándar: push a la rama staging → :staging-<sha>, tag v* → :v<semver>; OIDC hacia GCP, extrae solo las NEXT_PUBLIC_* del secret (cms-staging-env / cms-prod-env) como build-args (más GIT_SHA para /api/health), GHCR con el GITHUB_TOKEN, cache de capas. Con las variables GCP_* cargadas a nivel de organización, el carril quedó operativo desde el primer push (imágenes staging-* en GHCR, build en ~3m30). El disparador inicial fue develop; el 27/08/2026 se corrigió a la rama staging, que es la que el Environment acepta. workflow_dispatch para redeploy/rollback se suma en la fase 4.

Fase 3a · Deploy a staging — hecha y verificada (27/08/2026)

Job deploy-staging según la guía: escribe el .env del host desde el secret, compose pull + up -d con el tag del build, y smoke en dos pasos — /api/health verifica que la app vive y que responde el SHA desplegado, /admin fuerza la inicialización de Payload que aplica las migraciones. Corre en el runner de la organización con label staging (VM de staging de esta red) y se activa con la variable RUNNER_STAGING del repo.

Primer despliegue automático: 27/08/2026. La base mi-cms-payload-staging, creada vacía esa mañana, terminó con las 77 tablas y las 3 migraciones registradas — aplicadas por el arranque del contenedor, sin intervención humana. https://stg-cms.compulandia.com.py/api/health responde el SHA exacto del commit desplegado.

Fase 3b · Deploy a producción — hecha y verificada (28/08/2026)

v1.0.0 desplegado por tag: la regla del Environment y el guard de main funcionaron (rechazaron un intento desde develop), el runner de la VM de e-commerce (label ecommerce-srv-production) operó desde /opt/payload-cms y el arranque migró la base de producción. Los tres incidentes del camino (permiso del secret, permisos de /opt, red hacia la base) fueron diagnosticados por el propio smoke sin afectar lo que corría.

Plan original de la fase (referencia)

  • Instalar el runner del host de staging (label staging, servicio systemd, solo tráfico saliente hacia GitHub, GHCR y *.googleapis.com — pasos en la guía de la wiki) y cargar la variable RUNNER_STAGING.
  • Job deploy-prod: igual al de staging, más el doble guard (tag policy v* en el Environment y merge-base --is-ancestor contra main). Necesita: secret cms-prod-env, host con runner production, URI de la base de producción en GCP.
  • Reescribir la sección Deploy del runbook cuando el primer despliegue automático funcione.

Fase 4 · Endurecer

  • concurrency, retención de imágenes en GHCR, workflow_dispatch de rollback.
  • Evaluar sacar el array cors a configuración de runtime.

Definiciones pendientes

  1. ¿Hosts de staging y producción del CMS? ¿Los mismos del storefront? Sin esto no hay dónde instalar runners.
  2. ¿URI de la base de producción (GCP)? La restricción de acceso por red ya está configurada allí. Staging usa su propia VM local de PostgreSQL; queda a decisión del equipo si se restringe por red (R3.5).
  3. ¿URL pública del CMS en producción? Es NEXT_PUBLIC_SERVER_URL (se hornea en el build) y condiciona el array cors del storefront.
  4. ¿Proyecto de GCP para Secret Manager? Cuál se usa, y quién crea el Workload Identity Pool y la cuenta de servicio (R3.1–R3.2). Es un setup único por organización que comparten todos los repos.
  5. ¿El sitio Next del frontend heredado se mantiene o se apaga? Si nadie lo consume, el sitemap/SEO local dejan de importar.

Ya no es una definición pendiente quién aprueba los deploys: con el estándar por tag, el tag es la aprobación. Los revisores requeridos del Environment quedan como opción para cuando haya separación real de roles.