Objetivo del tema: /git-flow publish = producción actualizada, sin entrar nunca al servidor. Y en cada push, GitHub compila y pasa todos los tests. develop no se despliega en ningún sitio: es local.
Lo que ya sabéis: qué es GitHub Actions y un primer workflow con VPN y SSH, en Despliegue 5; cómo funciona nginx-proxy con VIRTUAL_HOST, en Despliegue 4.c. Lo nuevo aquí es sustituir la VPN y el SSH por un runner dentro del servidor.
Un VPS compartido por toda la clase, con tres cosas montadas por el profesor:
nginx-proxy-network, escuchando en el 80. Cada aplicación se publica sola por su VIRTUAL_HOST: <alumno>.proyectosdaw.cip.fpmislata.com, que ya está en compose.yaml desde el tema 4.docker compose directamente: ni SSH, ni VPN, ni registro de imágenes. El profesor lo registra con el token que da GitHub en Settings → Actions → Runners → New self-hosted runner; es la única vez que alguien entra al servidor.docker ps cada uno ve los contenedores de los demás. Es un servidor de clase y se asume. Lo que separa a cada alumno es el nombre de proyecto de Compose (el del repositorio) y su subdominio.
En el servidor no hay nada nuevo que aprender: la aplicación se opera con el mismo script del tema 4, app.sh, con sus cuatro subcomandos. Lo único que cambia es una variable de entorno que fija el workflow:
COMPOSE_FILE=compose.yaml
COMPOSE_FILE=compose.yaml Compose carga solo producción: el compose.override.yaml de desarrollo no se aplica, así que ni ng serve, ni puertos publicados, ni datos de prueba.app.sh start hace up -d –build –wait: construye la imagen con el código del commit, sustituye los contenedores que han cambiado y espera a los healthcheck. Nunca hace down -v: el volumen db-data se queda y un despliegue nunca borra la base de datos; Flyway aplica las migraciones nuevas al arrancar.app.sh, así que lo mismo que hace GitHub se puede lanzar a mano desde el servidor si un día hace falta.
La contraseña de la base de datos de producción no está en ningún fichero: está en Settings → Secrets and variables → Actions del repositorio como DB_PASSWORD. El workflow la pasa como variable de entorno a app.sh y Compose la lee en ${DB_PASSWORD}. En desarrollo esa misma variable viene del .env del worktree.
Un solo fichero para CI y CD:
name: CI/CD
on:
push:
branches: [develop, main]
workflow_dispatch:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: 25 }
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Levantar la aplicación
env: { DB_PASSWORD: ci }
run: |
docker network create nginx-proxy-network
docker compose up -d --build --wait
- name: Tests del backend (unitarios y ArchUnit)
working-directory: backend
run: mvn -B test
- name: Tests de API y E2E
working-directory: frontend
run: |
npm ci
npx playwright install --with-deps chromium
npm run test:api
npm run test:e2e
deploy:
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Desplegar
env:
COMPOSE_FILE: compose.yaml
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: ./app.sh start
- name: Smoke test
run: curl -fsS http://<alumno>.proyectosdaw.cip.fpmislata.com/api/clientes > /dev/null
Lo que hay que entender:
ci corre en un runner de GitHub (ubuntu-latest), en cada push a develop y a main. Levanta la aplicación con el mismo compose.yaml + compose.override.yaml del portátil (sin .env: valen los puertos por defecto y el perfil datos-prueba), y pasa los tests con los mismos comandos que en el worktree: mvn test, npm run test:api y npm run test:e2e. –wait espera a los healthcheck.deploy corre en el runner del servidor (self-hosted), solo en main y solo si ci ha pasado (needs). Hace checkout del commit y ./app.sh start con COMPOSE_FILE=compose.yaml: la imagen se construye en el servidor con ese código.workflow_dispatch permite relanzarlo a mano desde la pestaña Actions (siempre sobre main).
Y dos workflows pequeños de operación, status.yml y logs.yml, solo con workflow_dispatch y un paso ./app.sh status o ./app.sh logs (con el mismo COMPOSE_FILE) en self-hosted. Son la forma de ver qué pasa en producción sin acceso al servidor: se lanzan desde Actions y la salida queda en el log del workflow.
El YAML lo escribe el agente a partir de lo de arriba. Y no hay skill para cuando CI sale en rojo, porque no debería salir en rojo: pasa los mismos comandos que /git-flow finish acaba de pasar en el worktree. Si sale, es que algo distingue el runner del portátil (un fichero que no está en git, una variable, un puerto) o que ha fallado el despliegue o el smoke test. Un workflow rojo tiene un log largo y la causa está en tres líneas: se abre el paso fallido en Actions, se copia su log y se le pregunta al agente en una conversación nueva. Desde la terminal, gh run view –log-failed da solo los pasos que han fallado, que es lo que hay que pegarle (tema 5: no pegarle logs enteros).
app.sh funciona como lo va a ejecutar el servidor, sin .env y solo con producción: COMPOSE_FILE=compose.yaml ./app.sh start desde el repositorio principal, ./app.sh status y luego ./app.sh stop.DB_PASSWORD en Settings → Secrets and variables → Actions..github/workflows/ci-cd.yml solo con el job ci. Finaliza el issue y mira en Actions cómo se ejecuta en el push a develop. Si sale en rojo, arréglalo hasta que esté en verde.deploy al workflow con el needs: ci, el if sobre main y el smoke test contra tu URL./git-flow publish. Sigue la ejecución en Actions y comprueba que la aplicación responde en http://<alumno>.proyectosdaw.cip.fpmislata.com.status.yml y logs.yml con workflow_dispatch sobre self-hosted.app.sh status en el log.deploy sale en rojo. Copia el log del paso fallido, pregúntaselo al agente y comprueba que señala la URL. Arréglala y vuelve a publicar.
Fuera de este tema, por ser de un sistema real y no de un proyecto de clase: entornos de dev y pre, copias de seguridad, copiar datos de producción, tareas programadas y HTTPS (el proxy sirve HTTP).