10 — Pipeline CI/CD con GitHub Actions
← Anterior: 09 — Workload Identity Federation (Autenticación sin claves en GitHub Actions)
✅ Gratuito para la mayoría de usos. GitHub Actions otorga 2.000 minutos gratuitos/mes para repositorios privados y minutos ilimitados para repositorios públicos. Cada ejecución de despliegue (pruebas + build + push + deploy) tarda aproximadamente 5–10 minutos, dando ~200–400 despliegues gratuitos/mes en un repo privado. Después del nivel gratuito, los minutos adicionales cuestan $0.008/minuto.
¿Qué es GitHub Actions?
GitHub Actions es la plataforma de automatización integrada de GitHub. Defines workflows en archivos YAML dentro de .github/workflows/. GitHub los ejecuta en sus propios servidores (llamados runners) en respuesta a eventos — un push, un pull request, un horario.
El workflow aquí se ejecuta en cada push a main y en cada pull request apuntando a main. Tiene dos jobs:
- test — se ejecuta en todos los pushes y PRs; bloquea el despliegue si falla
- deploy — solo se ejecuta en pushes a
main(no en PRs); solo se ejecuta sitestpasa
Crear el archivo del workflow
Crea .github/workflows/deploy.yml en la raíz del repositorio:
name: Test & Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGION: southamerica-east1
IMAGE: southamerica-east1-docker.pkg.dev/mycoolproject-prod/mycoolproject-repo/app
jobs:
# ── Job 1: test ──────────────────────────────────────────────────────────────
# Se ejecuta en cada push y PR. Bloquea el despliegue si los tests fallan.
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Descarga el código de tu repo en el runner
- uses: astral-sh/setup-uv@v4
with:
working-directory: web
# Instala uv (el gestor de paquetes) en el runner
- name: Instalar dependencias
run: cd web && uv sync --frozen
# Instala todas las dependencias Python desde uv.lock
# --frozen: falla si el lockfile está desactualizado
- name: Ejecutar tests
run: cd web && uv run manage.py test web/tests --settings=core.settings.test
# Ejecuta la suite de tests de Django usando SQLite en memoria (no se necesita BD)
env:
SECRET_KEY: ci-secret-not-real
# test.py requiere que SECRET_KEY esté configurada; este valor ficticio está bien para tests
# ── Job 2: deploy ────────────────────────────────────────────────────────────
# Solo se ejecuta en pushes a main, solo si el job de test pasó.
deploy:
runs-on: ubuntu-latest
needs: test # espera a que el job de test tenga éxito
if: github.ref == 'refs/heads/main' # omitir en pull requests
permissions:
contents: read
id-token: write # requerido para el intercambio de tokens de Workload Identity
steps:
- uses: actions/checkout@v4
# Autenticarse en GCP usando Workload Identity (sin claves JSON)
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
# Instalar gcloud CLI en el runner
- uses: google-github-actions/setup-gcloud@v2
# Configurar Docker para subir a Artifact Registry
- name: Configurar Docker
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet
# Construir la imagen Docker con dos tags:
# - :latest (siempre apunta al más reciente)
# - :<git-sha> (único por commit — permite rollbacks precisos)
- name: Construir imagen
run: |
docker build \
-t ${{ env.IMAGE }}:${{ github.sha }} \
-t ${{ env.IMAGE }}:latest \
.
# Subir ambos tags a Artifact Registry
- name: Subir imagen
run: docker push --all-tags ${{ env.IMAGE }}
# Actualizar el job de migración para usar la nueva imagen, luego ejecutarlo
# Esto aplica cualquier nueva migración de base de datos antes de que el tráfico llegue al nuevo código
- name: Ejecutar migraciones
run: |
gcloud run jobs update migrate \
--image=${{ env.IMAGE }}:${{ github.sha }} \
--region=${{ env.REGION }}
gcloud run jobs execute migrate \
--region=${{ env.REGION }} \
--wait
# Desplegar la nueva imagen en Cloud Run
# Cloud Run crea una nueva revisión y desvía el 100% del tráfico a ella
- name: Desplegar en Cloud Run
run: |
gcloud run deploy mycoolproject \
--image=${{ env.IMAGE }}:${{ github.sha }} \
--region=${{ env.REGION }}
Bloque env a nivel de workflow
Al comienzo del workflow, antes de la sección jobs:, hay un bloque env::
env:
REGION: southamerica-east1
IMAGE: southamerica-east1-docker.pkg.dev/mycoolproject-prod/mycoolproject-repo/app
Esto define variables de entorno a nivel de workflow — constantes disponibles para cada job y cada paso del archivo. Se referencian más adelante como ${{ env.REGION }} y ${{ env.IMAGE }}. La ventaja es que si alguna vez cambias la región o el nombre del proyecto, lo actualizas en exactamente un lugar en lugar de buscar en cada comando gcloud.
| Variable | Valor | Por qué está aquí |
|---|---|---|
REGION |
southamerica-east1 |
La región GCP donde viven Cloud Run, Artifact Registry y Cloud SQL. Se repite en cada comando gcloud y docker. |
IMAGE |
southamerica-east1-docker.pkg.dev/mycoolproject-prod/mycoolproject-repo/app |
La ruta completa en Artifact Registry para la imagen Docker. Desglosada: <región>-docker.pkg.dev/<proyecto>/<repositorio>/<nombre-imagen>. Se usa al construir, subir y desplegar. |
env:vssecrets:—env:es para configuración no sensible que estás cómodo commiteando en tu repo.secrets:(usado paraGCP_WORKLOAD_IDENTITY_PROVIDERyGCP_SERVICE_ACCOUNT) es para valores sensibles almacenados cifrados en GitHub, nunca visibles en logs ni en el historial del repo.
Qué sucede en cada push a main
push a main
│
├── job de test
│ ├── descargar código
│ ├── instalar uv + dependencias
│ └── ejecutar suite de tests de Django (171 tests, ~90 segundos)
│ │
│ ¿falla? → el job de deploy se omite, el código roto nunca llega a producción
│ ¿pasa? ↓
│
└── job de deploy
├── autenticarse en GCP (Workload Identity)
├── docker build (usa caché de capas — rápido si las dependencias no cambiaron)
├── docker push → Artifact Registry
├── actualizar imagen del job de migración
├── ejecutar job de migración → aplica migraciones de BD → espera a que termine
└── gcloud run deploy → nueva revisión de Cloud Run sale a producción
Qué sucede en un pull request
Solo se ejecuta el job test. El job de deploy se omite porque github.ref no es refs/heads/main. Esto significa que cada PR tiene sus tests ejecutados, pero el sitio activo solo se actualiza cuando el código se fusiona a main.
Rollback
Si un mal despliegue se cuela, vuelve a la revisión anterior en segundos:
# Lista las revisiones recientes con sus nombres y distribución de tráfico — encuentra aquí el nombre de la última revisión buena.
gcloud run revisions list --service=mycoolproject --region=southamerica-east1
# Desvía el 100% del tráfico a una revisión específica, revirtiendo el sitio activo al instante.
# Reemplaza mycoolproject-<revisión-anterior> con el nombre de la revisión del comando anterior.
# Resultado: el sitio sirve inmediatamente la revisión anterior — no se necesita recompilación.
gcloud run services update-traffic mycoolproject \
--region=southamerica-east1 \
--to-revisions=mycoolproject-<revisión-anterior>=100
O vuelve a ejecutar el workflow de GitHub Actions del commit anterior — redespliega la imagen etiquetada con el SHA de ese commit.
📖 Navegación
- 01 — Configuración del Proyecto GCP
- 02 — Artifact Registry
- 03 — Cloud SQL (Base de datos PostgreSQL)
- 04 — Secret Manager
- 05 — Cloud Storage (Media & static files)
- 06 — Dockerfile
- 07 — Primer Despliegue
- 08 — Dominio Personalizado y SSL
- 09 — Workload Identity Federation (Auth de GitHub sin llaves)
- 10 — Pipeline CI/CD con GitHub Actions (Capítulo actual)
- 11 — Referencia Rápida
- 12 — Bonus: Email Personalizado (@dominio.cl)