Tema
CI/CD con GitHub Actions
Desplegar a mano (conectarte por SSH, hacer git pull, compilar, reiniciar) es lento y propenso a errores. Con CI/CD cada git push dispara automáticamente pruebas y, si todo pasa, el despliegue. GitHub Actions viene integrado en GitHub y tiene minutos gratuitos generosos para repos públicos y privados.
Concepto
- CI (Integración Continua): en cada push o pull request se instalan dependencias, se ejecuta el linter, las pruebas y el build. Si algo falla, te enteras antes de que llegue a producción.
- CD (Despliegue Continuo): si la CI pasa en la rama principal, se publica automáticamente.
Vocabulario de GitHub Actions
| Término | Qué es |
|---|---|
| Workflow | Un archivo YAML en .github/workflows/. Un repo puede tener varios. |
Evento (on) | Lo que dispara el workflow: push, pull_request, workflow_dispatch (botón manual), schedule (cron). |
| Job | Un conjunto de pasos que corre en una máquina. Los jobs pueden ir en paralelo o depender de otros (needs). |
| Step | Un comando (run) o una acción reutilizable (uses). |
| Runner | La máquina que ejecuta el job (ubuntu-latest). |
| Secret | Valor cifrado (contraseñas, llaves SSH) que se configura en el repo y se usa sin exponerlo. |
| Action | Paso reutilizable publicado por la comunidad, p. ej. actions/checkout. |
Los secretos se crean en Settings → Secrets and variables → Actions → New repository secret y en el YAML se leen con la sintaxis de expresiones de Actions (lo verás en los ejemplos).
Ejemplo 1: CI para un proyecto Node
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
pruebas:
runs-on: ubuntu-latest
strategy:
matrix:
# Probar en varias versiones de Node
node-version: [20, 22]
steps:
- name: Descargar el código
uses: actions/checkout@v4
- name: Configurar Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Instalar dependencias
run: npm ci
- name: Lint
run: npm run lint --if-present
- name: Pruebas
run: npm test --if-present
- name: Build
run: npm run build --if-presentHaz push y ve a la pestaña Actions del repo: verás cada job en verde o rojo, con sus logs.
Protege la rama main
En Settings → Branches → Add rule (o Rulesets) exige que la CI pase antes de poder hacer merge de un pull request. Así nadie (ni tú con prisa) rompe producción.
Ejemplo 2: desplegar VitePress (o cualquier sitio estático) a GitHub Pages
Primero, en Settings → Pages → Source, elige GitHub Actions.
yaml
# .github/workflows/deploy-pages.yml
name: Desplegar en GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
# Permisos necesarios para publicar en Pages
permissions:
contents: read
pages: write
id-token: write
# Evitar dos despliegues simultáneos
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # VitePress usa el historial para "última actualización"
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- uses: actions/configure-pages@v5
- run: npm ci
- run: npm run docs:build
- uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Publicar
id: deployment
uses: actions/deploy-pages@v4La opción base
Si el sitio queda en https://usuario.github.io/mi-repo/, configura base: '/mi-repo/' en .vitepress/config.mts. Con dominio propio o repo usuario.github.io, deja base: '/'.
Ejemplo 3: desplegar a un VPS por SSH
Preparación (una sola vez):
bash
# En tu computadora: genera una llave SOLO para despliegues (sin passphrase)
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/deploy_actions
# Copia la pública al servidor
ssh-copy-id -i ~/.ssh/deploy_actions.pub deploy@203.0.113.10
# Muestra la privada para copiarla al secreto SSH_KEY
cat ~/.ssh/deploy_actionsCrea en el repo los secretos: SSH_HOST (IP o dominio), SSH_USER (deploy) y SSH_KEY (contenido completo de la llave privada).
Opción A: ejecutar el script de despliegue en el servidor
yaml
# .github/workflows/deploy-vps.yml
name: Desplegar en VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Conectar por SSH y desplegar
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd ~/apps/api-tienda
git pull origin main
npm ci --omit=dev
pm2 reload ecosystem.config.js --update-envOpción B: compilar en Actions y subir con rsync
Ideal para sitios estáticos o SPAs: el servidor no necesita Node.
yaml
# .github/workflows/deploy-rsync.yml
name: Build y rsync
on:
push:
branches: [main]
jobs:
deploy:
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
- name: Configurar llave SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Subir archivos
run: |
rsync -avz --delete dist/ \
${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/misitio/public/Ejemplo 4: desplegar por FTP a un hosting compartido (cPanel)
Crea los secretos FTP_SERVER, FTP_USERNAME y FTP_PASSWORD (puedes crear una cuenta FTP limitada a public_html en cPanel).
yaml
# .github/workflows/deploy-ftp.yml
name: Desplegar por FTP
on:
push:
branches: [main]
jobs:
deploy:
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 docs:build
- name: Subir por FTP
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
protocol: ftps
local-dir: ./docs/.vitepress/dist/
server-dir: ./public_html/Esta acción guarda un archivo de estado en el servidor y solo sube los archivos que cambiaron.
Para un proyecto PHP sin build, basta con quitar los pasos de Node y usar local-dir: ./public/, excluyendo lo que no debe subirse con la opción exclude.
Ejemplo 5: construir y publicar una imagen Docker en GHCR
yaml
# .github/workflows/docker.yml
name: Imagen Docker
on:
push:
branches: [main]
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
build-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Iniciar sesión en GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generar etiquetas
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- name: Construir y publicar
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}En el servidor, tu docker-compose.yml usaría image: ghcr.io/usuario/repo:main y para actualizar: docker compose pull && docker compose up -d.
Workflow completo: CI + CD encadenados
El despliegue solo ocurre si las pruebas pasan, gracias a needs:
yaml
# .github/workflows/pipeline.yml
name: Pipeline
on:
push:
branches: [main]
pull_request:
jobs:
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 test --if-present
- run: npm run build --if-present
deploy:
needs: test
# Solo despliega en push a main (no en pull requests)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: ~/apps/api-tienda/deploy.shEntornos
Con environment: production puedes definir en Settings → Environments secretos específicos de producción y exigir aprobación manual antes de desplegar. Muy útil en proyectos de clientes.
Nunca pongas secretos en el YAML
Contraseñas, tokens y llaves van siempre en Secrets. GitHub los oculta en los logs. Un secreto escrito en el repositorio debe considerarse comprometido: cámbialo de inmediato.
Error común
El workflow no se ejecuta porque el archivo no está en .github/workflows/ o tiene un error de indentación YAML. Usa espacios (nunca tabuladores) y revisa la pestaña Actions: ahí aparece el error de sintaxis.
Ejercicios
- Escribe un workflow que se ejecute solo manualmente (
workflow_dispatch) y muestre la versión de Node y npm. - Modifica el ejemplo de FTP para que solo se dispare cuando cambien archivos dentro de
docs/. - ¿Qué palabra clave hace que el job
deployespere al jobtest?
Ver solución
yaml
name: Info manual
on:
workflow_dispatch:
jobs:
info:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: 22
- run: node -v && npm -vyaml
on:
push:
branches: [main]
paths:
- 'docs/**'needs: test.
Resumen
- CI ejecuta lint, pruebas y build en cada push/PR; CD despliega si todo pasa.
- Los workflows viven en
.github/workflows/*.yml. - Secretos en Settings, nunca en el código.
- Recetas: GitHub Pages, SSH a VPS, rsync, FTP a cPanel y Docker a GHCR.
- Encadena jobs con
needsy protege producción con entornos. - Siguiente: PaaS: Vercel, Netlify y más.