Skip to content

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érminoQué es
WorkflowUn 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).
JobUn conjunto de pasos que corre en una máquina. Los jobs pueden ir en paralelo o depender de otros (needs).
StepUn comando (run) o una acción reutilizable (uses).
RunnerLa máquina que ejecuta el job (ubuntu-latest).
SecretValor cifrado (contraseñas, llaves SSH) que se configura en el repo y se usa sin exponerlo.
ActionPaso 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-present

Haz 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@v4

La 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_actions

Crea 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-env

Opció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.sh

Entornos

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 ​

  1. Escribe un workflow que se ejecute solo manualmente (workflow_dispatch) y muestre la versión de Node y npm.
  2. Modifica el ejemplo de FTP para que solo se dispare cuando cambien archivos dentro de docs/.
  3. ¿Qué palabra clave hace que el job deploy espere al job test?
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 -v
yaml
on:
  push:
    branches: [main]
    paths:
      - 'docs/**'
  1. 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 needs y protege producción con entornos.
  • Siguiente: PaaS: Vercel, Netlify y más.

Academia Web — aprende, copia y construye.