TL;DR — Resumen Rápido
Guía de GitHub Actions: automatice pipelines de despliegue con protección de entornos, secretos, múltiples destinos, caché y estrategias de rollback.
Desplegar manualmente — conectarse por SSH a un servidor, ejecutar comandos de build, cruzar los dedos — es propenso a errores, imposible de auditar y no escala. GitHub Actions permite definir todo el pipeline de despliegue como código: lint, pruebas, build, aprobación, despliegue y notificación — todo activado automáticamente con cada push a main. Esta guía cubre la construcción de un workflow de despliegue automatizado en producción, desde el primer archivo YAML hasta despliegues multi-destino, protección de entornos y estrategias de rollback.
Prerrequisitos
Antes de crear tu workflow de despliegue, ten lo siguiente en su lugar:
- Un repositorio de GitHub con código de aplicación (ejemplos en Node.js a lo largo de la guía)
- Un entorno destino — cuenta de Cloudflare Workers, un VPS con acceso SSH, o un runner autohospedado en Windows para IIS
- Familiaridad básica con YAML — los workflows de GitHub Actions son archivos YAML donde la indentación importa
- Acceso a la configuración del repositorio en GitHub para guardar secretos y crear entornos
Conceptos Básicos de GitHub Actions
Un workflow de GitHub Actions es un archivo YAML almacenado en .github/workflows/. Se activa mediante uno o más eventos (push, pull request, programación, disparo manual). Un workflow contiene uno o más jobs. Cada job corre en un runner — una máquina virtual hospedada por GitHub (Ubuntu, Windows, macOS) o autohospedada. Cada job contiene steps secuenciales, cada uno de los cuales ejecuta un comando shell (run) o una unidad reutilizable llamada action (uses).
Los bloques clave:
name: Desplegar # nombre del workflow en la pestaña Actions
on: # disparador
push:
branches: [main]
jobs:
deploy: # ID del job
runs-on: ubuntu-latest # runner
steps:
- uses: actions/checkout@v4 # action (descarga el código fuente)
- run: npm ci # comando shell
- run: npm run build
Los jobs corren en paralelo por defecto. Usa needs: [job-id] para imponer un orden.
Tu Primer Workflow de Despliegue
Aquí hay un workflow completo para un proyecto Node.js que despliega en Cloudflare Workers usando Wrangler. Primero ejecuta lint y pruebas, luego hace build y por último despliega — solo en pushes a main.
name: Desplegar en Cloudflare Workers
on:
push:
branches: [main]
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm test
build:
needs: lint-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Desplegar con Wrangler
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Notificar éxito en Slack
if: success()
run: |
curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d "{\"text\":\"Desplegado ${{ github.sha }} en producción por ${{ github.actor }}\"}"
El bloque concurrency en la parte superior evita que dos despliegues se superpongan. Si haces push dos veces seguidas, la primera ejecución se cancela antes de que comience la segunda.
Reglas de Protección de Entorno
Hacer push directamente a producción sin una verificación humana es arriesgado para sistemas críticos. Los entornos de GitHub permiten agregar reglas de protección a cualquier job que los apunte.
Para configurar la protección de entorno:
- Ve a Settings → Environments en tu repositorio
- Haz clic en New environment y nómbralo
production - Activa Required reviewers — agrega uno o más usuarios o equipos de GitHub
- Opcionalmente establece un Wait timer (por ejemplo, 10 minutos) antes de que el job pueda ejecutarse
- Opcionalmente restringe qué ramas pueden desplegar en este entorno (por ejemplo, solo
main)
Una vez configurado, cualquier job con environment: production se pausará y esperará a que un revisor apruebe antes de continuar. El revisor ve el despliegue pendiente en la UI de GitHub y puede aprobar o rechazar con un comentario.
Gestión de Secretos
Nunca escribas tokens de API, claves SSH o contraseñas en los archivos de workflow. GitHub ofrece tres niveles de almacenamiento de secretos:
Secretos de repositorio — Disponibles para todos los workflows del repo. Ve a Settings → Secrets and variables → Actions → New repository secret.
Secretos de entorno — Acotados a un entorno específico (por ejemplo, production). Solo se exponen cuando un job apunta a ese entorno.
Secretos de organización — Compartidos entre múltiples repositorios de una organización de GitHub, ideales para tokens usados por muchos proyectos.
Referencia los secretos en los workflows con la sintaxis ${{ secrets.NOMBRE_SECRETO }}:
- name: Desplegar con Wrangler
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
OIDC (OpenID Connect) es una alternativa a los secretos de larga duración para proveedores de nube que lo soportan (AWS, GCP, Azure). En lugar de almacenar un token estático, el runner solicita una credencial de corta duración al proveedor en tiempo de ejecución.
Despliegue en Diferentes Destinos
Cloudflare Workers con Wrangler
La action cloudflare/wrangler-action maneja la autenticación y ejecuta cualquier comando de Wrangler:
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env production
VPS mediante SSH y rsync
Para un VPS Linux, usa SSH para conectarte y rsync para transferir archivos:
- name: Desplegar en VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
rsync -az --delete dist/ /var/www/myapp/dist/
systemctl restart myapp
Almacena la clave SSH privada en GitHub Secrets. Agrega la clave pública correspondiente a ~/.ssh/authorized_keys en el servidor. Usa un usuario de despliegue dedicado con permisos mínimos, nunca root.
IIS mediante Runner Autohospedado
Para despliegues en Windows IIS, ejecuta un runner autohospedado directamente en el servidor:
deploy-iis:
needs: build
runs-on: [self-hosted, iis-deploy]
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: C:\inetpub\wwwroot\myapp
- name: Reiniciar sitio IIS
run: |
Import-Module WebAdministration
Restart-WebItem 'IIS:\Sites\MyApp'
shell: powershell
Caché y Rendimiento
Caché de Dependencias
Re-descargar paquetes npm en cada ejecución desperdicia tiempo. Cachéalos:
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
Para .NET:
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: ${{ runner.os }}-nuget-
Grupos de Concurrencia
Los grupos de concurrencia evitan despliegues en paralelo — una fuente común de condiciones de carrera:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Comparativa: GitHub Actions vs GitLab CI vs Jenkins
| Característica | GitHub Actions | GitLab CI | Jenkins |
|---|---|---|---|
| Hospedaje | GitHub (SaaS) | GitLab o autohospedado | Solo autohospedado |
| Archivo de config | .github/workflows/*.yml | .gitlab-ci.yml | Jenkinsfile (Groovy) |
| Tier gratuito | 2.000 min/mes (privado) | 400 min/mes (privado) | Ilimitado (tu infra) |
| Marketplace | 20.000+ actions | Limitado | 1.800+ plugins |
| Compuertas de entorno | Integradas (gratis para público) | Integradas (Ultimate para reglas) | Vía plugins |
| OIDC | AWS, GCP, Azure, Cloudflare | AWS, GCP, Azure | Vía plugins |
| Curva de aprendizaje | Baja | Baja | Alta (DSL Groovy) |
| Ideal para | Proyectos en GitHub | Monorepos en GitLab | Pipelines complejos heredados |
Escenario Real: Pipeline Completo
Un pipeline de producción para Node.js con notificaciones Slack:
name: Despliegue Producción
on:
push:
branches: [main]
concurrency:
group: production-deploy
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run lint
test:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm test -- --coverage
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist/
retention-days: 14
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist/
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Notificar éxito
if: success()
run: |
curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-d "{\"text\":\"Desplegado ${{ github.sha }} por ${{ github.actor }}\"}"
- name: Notificar fallo
if: failure()
run: |
curl -s -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-d "{\"text\":\"FALLO de despliegue para ${{ github.sha }}\"}"
Badges de Estado
Agrega un badge de estado en vivo a tu README:

Casos Límite y Problemas Comunes
El enmascaramiento de secretos solo funciona para coincidencias exactas. GitHub enmascara el valor literal del secreto en los logs, pero si el secreto está codificado en base64 o URL antes de usarse, el valor original puede aparecer sin enmascarar.
Los cambios en la imagen del runner rompen versiones de herramientas ancladas. GitHub actualiza ubuntu-latest periódicamente. Si tu workflow depende de una versión específica de una herramienta preinstalada, instálala explícitamente con una action de setup.
cancel-in-progress: true cancela la ejecución actual, no la entrante. El nuevo push gana. Esto es casi siempre lo que quieres para despliegues, pero puede sorprenderte en suites de tests largas.
Los secretos de entorno no están disponibles para pull requests de forks. Esto es intencional — los forks no pueden acceder a secretos para evitar filtraciones.
Solución de Problemas
El workflow no se dispara en push a main. Verifica el filtro on.push.branches — el nombre de la rama debe coincidir exactamente. Confirma que el archivo de workflow está en .github/workflows/.
El secreto está disponible pero la autenticación de Wrangler falla. Confirma que el nombre del secreto coincide exactamente (sensible a mayúsculas). Verifica que el token de API de Cloudflare tenga los permisos correctos: Workers Scripts: Edit y Account: Read.
El job espera indefinidamente la aprobación del entorno. Verifica que estás listado como revisor requerido del entorno. Comprueba que el nombre del entorno en el YAML del workflow coincida exactamente con el nombre en Settings.
Resumen
- Define los workflows como archivos YAML en
.github/workflows/— están versionados junto con tu código - Estructura los pipelines como jobs secuenciales: lint → test → build → deploy, usando
needspara imponer el orden - Usa
environment: productioncon revisores requeridos para compuertas de aprobación humanas - Almacena todos los secretos en el almacén de secretos de GitHub — nunca en el archivo YAML
- Los despliegues en Cloudflare Workers usan
cloudflare/wrangler-action; los destinos VPS usan SSH/rsync; IIS usa un runner autohospedado - Agrega grupos de
concurrencyconcancel-in-progress: truepara evitar despliegues superpuestos - Cachea dependencias con
actions/setup-node cache: npmpara ejecuciones más rápidas - Nombra los artefactos con
${{ github.sha }}para facilitar el rollback re-ejecutando el workflow en cualquier commit anterior