Tutorial: configurar GitHub Actions para tests de integración con PostgreSQL en contenedores efímeros y rollback automático de migrations — guía práctica
Deploy & DevOps · Por Kevin Marcondes · Publicado el · 9 min de lectura
Este paso a paso muestra cómo preparar un workflow que levanta PostgreSQL como servicio efímero, aplica migrations (up), ejecuta tests de integración y realiza rollback (down) automáticamente, incluso cuando hay fallos. Tema: tutorial para configurar GitHub Actions con PostgreSQL en contenedores efímeros y rollback automático de migrations. Al final tendrás un pipeline reproducible y limpio.
1) Requisitos previos y arquitectura del pipeline
Necesitas tests de integración y scripts de migración con up/down. El workflow crea un PostgreSQL efímero por job, aplica las migrations, ejecuta los tests y deshace los cambios al final. Nunca apuntes a una base compartida en el CI.
Arquitectura: runner de GitHub Actions + servicio PostgreSQL + scripts de migrate/rollback. Define DATABASE_URL y valida la conexión antes de la suite para fallar rápido.
- Repositorio con directorio de migrations (.up.sql y .down.sql).
- Scripts para aplicar y deshacer migrations (bash, make o la herramienta de tu stack).
- Test runner (pytest, npm test, go test, etc.).
- Acceso a un runner Ubuntu en GitHub Actions y psql instalado.
- DATABASE_URL configurada para el servicio local.
# Variáveis de ambiente e verificação rápida
export DATABASE_URL="postgres://postgres:postgres@localhost:5432/app_test"
pg_isready -h localhost -p 5432 -U postgres
psql "$DATABASE_URL" -c "SELECT 1;"
# Exemplos de execução (substitua pelo seu runner)
# Node: npm run test:integration
# Python: pytest -m integration
# Go: go test ./... -tags=integration
./scripts/run-tests.sh
2) ¿Cómo funcionan la base efímera y el rollback automático?
El servicio PostgreSQL se descarta al finalizar el job, garantizando aislamiento entre ejecuciones. Aun así, el rollback (down) es útil para validar reversiones y entrenar al equipo en escenarios reales.
Puedes combinar estrategias complementarias de aislamiento, rollback y sandbox transaccional según tu stack y la cobertura que desees.
| Estrategia | Cuándo usar | Ventajas | Contras |
|---|---|---|---|
| Base efímera por job | Siempre que sea posible | Aislamiento total y limpio | No valida los scripts de rollback |
| Rollback de migrations (down) al final | Validar reversiones y entrenar al equipo | Garantiza que el down funciona | Requiere mantener scripts down fiables |
| Transacción por test (BEGIN/ROLLBACK) | Frameworks que soportan sandbox | Ejecución rápida y repetible | No cubre cambios complejos de esquema |
3) Organizar migraciones y scripts de up/down
Estandariza una estructura predecible y crea dos scripts: migrate.sh (up) y rollback.sh (down). Los scripts deben fallar en el primer error para no ocultar problemas.
Si necesitas criterios y prácticas adicionales sobre reversiones, consulta también la guía de organización de migraciones en PostgreSQL en migraciones y rollbacks bien estructurados.
# Estrutura sugerida
# migrations/
# 001_create_users.up.sql
# 001_create_users.down.sql
# 002_add_email_index.up.sql
# 002_add_email_index.down.sql
# scripts/
# migrate.sh
# rollback.sh
# scripts/migrate.sh
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
: "${DATABASE_URL:?Defina DATABASE_URL}"
for f in migrations/*.up.sql; do
echo "[UP] Aplicando $f"
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -1 -f "$f"
done
echo "Migrations UP concluídas."
# scripts/rollback.sh
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
: "${DATABASE_URL:?Defina DATABASE_URL}"
# Reverte na ordem inversa
for f in $(ls migrations/*.down.sql | sort -r); do
echo "[DOWN] Revertendo $f"
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -1 -f "$f"
done
echo "Rollback concluído."
4) Definir el workflow de GitHub Actions con el servicio PostgreSQL
Crea un workflow que levante el servicio postgres, instale el cliente psql, aplique migrations, ejecute tests y siempre intente el rollback al final. Usa GitHub Secrets en lugar de contraseñas en claro.
Para reforzar la seguridad de pipelines y secrets, consulta el checklist de seguridad CI/CD. Si además realizas despliegues con Actions, revisa el paso a paso del pipeline de despliegue automático.
name: ci-integration-postgres
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
ports:
- 5432:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_test
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=5s
--health-timeout=5s
--health-retries=20
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
steps:
- uses: actions/checkout@v4
- name: Instalar cliente PostgreSQL
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
- name: Aguardar Postgres e validar conexão
run: |
for i in {1..30}; do
if pg_isready -h localhost -p 5432 -U postgres; then
break
fi
sleep 1
done
psql "$DATABASE_URL" -c "SELECT version();"
- name: Rodar migrations (UP)
run: bash scripts/migrate.sh
- name: Executar testes de integração
run: |
./scripts/run-tests.sh
- name: Rollback de migrations (DOWN) — sempre
if: always()
run: bash scripts/rollback.sh
5) ¿Cómo garantizar el rollback incluso cuando los tests fallan?
Usa if: always() en el paso de rollback. Así, el down se ejecuta independientemente del resultado de los tests. Las fallas en el rollback deben romper el job para señalar migrations no reversibles.
Si prefieres, crea un job de limpieza separado con needs e if: always() para ejecutar acciones adicionales tras la suite.
- Paso de rollback con if: always().
- Errores en el rollback detienen el pipeline para corrección inmediata.
- Job cleanup opcional para tareas extra después de los tests.
# Exemplo de job de limpeza opcional
jobs:
test:
runs-on: ubuntu-latest
# ... passos do job de testes ...
cleanup:
runs-on: ubuntu-latest
needs: [test]
if: always()
steps:
- uses: actions/checkout@v4
- name: (Opcional) operações de limpeza adicionais
run: |
echo "Nada a fazer: o service Postgres é efêmero."
# Em casos especiais, notifique ou arquive artefatos.
6) Paralelismo y bases aisladas por job: cómo configurar?
Para paralelizar, genera un nombre de base único por job y expórtalo en DATABASE_URL, evitando colisiones entre ejecuciones simultáneas. La matrix permite probar múltiples versiones de PostgreSQL.
Si usas runners self‑hosted y necesitas escalar concurrencia, revisa la guía de auto‑escalado de runners.
- Nombre de base con matrix.pg, github.run_id y github.run_attempt.
- Creación idempotente vía CREATE DATABASE o createdb.
- El rollback sigue siendo válido con múltiples bases aisladas.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
pg: [14, 15, 16]
services:
postgres:
image: postgres:${{ matrix.pg }}
ports: ["5432:5432"]
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=5s --health-timeout=5s --health-retries=20
env:
DB_NAME: app_test_${{ matrix.pg }}_${{ github.run_id }}_${{ github.run_attempt }}
DATABASE_URL: postgres://postgres:postgres@localhost:5432/${{ env.DB_NAME }}
steps:
- uses: actions/checkout@v4
- name: Instalar cliente PostgreSQL
run: sudo apt-get update && sudo apt-get install -y postgresql-client
- name: Criar banco isolado
run: |
psql "postgres://postgres:postgres@localhost:5432/postgres" \
-v ON_ERROR_STOP=1 -c "CREATE DATABASE \"${DB_NAME}\";" || true
- name: Rodar migrations e testes
run: |
bash scripts/migrate.sh
./scripts/run-tests.sh
- name: Rollback (sempre)
if: always()
run: bash scripts/rollback.sh
7) Seeds de datos en el CI sin ensuciar el entorno
Los seeds insertan los datos mínimos necesarios para los tests. Aplícalos después de las migrations y, si no hay un rollback específico, confía en la base efímera para la limpieza.
Prefiere seeds idempotentes (UPSERT/ON CONFLICT) para evitar duplicados e intermitencias en reejecuciones.
- Seeds después de las migrations; antes de los tests.
- IDs fijos y UPSERTs evitan duplicados.
- El rollback puede eliminar registros de seed explícitamente.
# scripts/seed.sh (exemplo)
#!/usr/bin/env bash
set -euo pipefail
: "${DATABASE_URL:?}"
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL'
INSERT INTO users(id, name, email)
VALUES (1, 'Test User', '[email protected]')
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, email=EXCLUDED.email;
SQL
echo "Seeds aplicados."
8) Verificación final y resolución de problemas comunes
Confirma que el esquema se aplicó, que los tests usaron la base correcta y que el rollback limpió los cambios. En fallos, céntrate en conexión, autenticación y el orden de las migrations.
Si el error persiste, aísla un caso mínimo con una migration y un test para reducir variables y acelerar el diagnóstico.
- Connection refused: espera la salud del servicio e instala psql.
- Autenticación: usa secrets; usuario con privilegios adecuados.
- Timeouts: aumenta health‑retries/interval; evita consultas pesadas en el setup.
- Locks: aplica migrations en transacción (-1) y serializa cambios complejos.
- psql no encontrado: instala postgresql-client en el job.
- Sin down: usa la base efímera y escribe scripts de rollback cuanto antes.
# Checks rápidos
psql "$DATABASE_URL" -c "\\dt public.*" # Tabelas no schema público
psql "$DATABASE_URL" -c "SELECT count(*) FROM public.users;" # Tabela-chave
psql "$DATABASE_URL" -c "\\dt public.users" || true # Após rollback, debe fallar/estar vacía
Preguntas frecuentes
¿Necesito el rollback si la base del CI ya es efímera?
Sí, si quieres validar los scripts down. La base efímera garantiza limpieza; el rollback asegura que tus migrations son reversibles y evita bloqueos en escenarios reales de reversión.
¿Puedo usar docker-compose en lugar de services en GitHub Actions?
Puedes, pero el servicio de GitHub Actions es más sencillo para un único PostgreSQL. Docker Compose aporta flexibilidad cuando hay múltiples servicios y redes personalizadas en el pipeline.
¿Cómo aislar builds paralelos para no confligir datos?
Usa nombres de base únicos por job (run_id, run_attempt, matrix). Crea la base antes de las migrations y exporta DATABASE_URL con el nombre diferenciado para cada ejecución.
¿Dónde pongo la contraseña del PostgreSQL en el CI?
Colócala en GitHub Secrets y referencia el secreto en el workflow. En ejemplos didácticos se usan valores por defecto, pero en proyectos reales usa secrets para minimizar exposición y facilitar auditoría.
¿Y si mis migrations no tienen scripts down?
Usa la base efímera para garantizar limpieza entre jobs y prioriza escribir los scripts down. Sin down no podrás entrenar ni validar rollbacks, lo que aumenta el riesgo en producción.
¿Cómo semear datos de test sin ensuciar el entorno?
Aplica seeds idempotentes tras las migrations y, si hace falta, elimínalos en el rollback. Con base efímera, el contenedor se descarta al finalizar el job, asegurando limpieza automática.
¿Quieres acelerar tu pipeline de tests con PostgreSQL?
Si necesitas adaptar el ejemplo a tu stack (Node, Python, Go, PHP) y garantizar rollbacks fiables, hablemos sobre tu escenario.
Hablar sobre tu pipeline CIVer también
- Tutorial: configurar mTLS entre Traefik y servicios en Docker Compose usando smallstep CA y certificados automáticos — paso a paso
- Tutorial: integrar Sentry en el pipeline de deploy con GitHub Actions para subir source maps automáticamente y generar issues (PHP y JavaScript) — paso a paso
- Configurar retención y compresión de logs de contenedores Docker con journald + logrotate + rsyslog en VPS con poco disco
- Configurar BuildKit garbage collection y systemd timers para limpieza automática de imágenes y volúmenes Docker en VPS: tutorial práctico