Construye CI/CD con GitHub Actions

Intermedio | 120 min de lectura | 2025.12.18

Lo que aprenderas en este tutorial

  • Conceptos basicos de GitHub Actions
  • Como escribir archivos de workflow
  • Ejecucion automatica de tests
  • Build y guardado de artefactos
  • Configuracion de despliegue por ambiente
  • Gestion de secrets y seguridad

Requisitos previos: Tener cuenta de GitHub. Conocimiento basico de proyectos Node.js facilitara la comprension.

Que es CI/CD y por que es necesario

Historia de CI/CD

El concepto de Integracion Continua (CI) fue sistematizado en 2000 por Martin Fowler y Kent Beck.

“La integracion continua es una practica de desarrollo de software donde los miembros del equipo integran su trabajo frecuentemente” - Martin Fowler

Entrega/Despliegue Continuo (CD) extiende CI para automatizar los releases a produccion.

Evolucion de las herramientas CI/CD

AnoHerramientaCaracteristicas
2004Hudson/JenkinsOn-premise, basado en plugins
2011Travis CIBasado en cloud, integracion GitHub
2014CircleCISoporte Docker, builds rapidos
2017GitLab CIIntegrado en GitLab
2019GitHub ActionsIntegrado en GitHub, marketplace

Ventajas de GitHub Actions

  1. Integrado en GitHub: Sin servicios adicionales, experiencia fluida
  2. Definicion YAML: Infraestructura como codigo (IaC)
  3. Marketplace: Abundantes actions reutilizables
  4. Nivel gratuito: Ilimitado para repositorios publicos
  5. Matrix builds: Tests paralelos en multiples entornos

DORA Metrics (Metricas DevOps)

Segun la investigacion de Google (DORA), los equipos de alto rendimiento logran:

MetricaEliteBajo rendimiento
Frecuencia de despliegueVarias veces/diaMenos de 1/mes
Lead timeMenos de 1 horaMas de 1 mes
Tasa de fallo0-15%46-60%
Tiempo de recuperacionMenos de 1 horaMas de 1 semana

CI/CD es un elemento importante para mejorar estas metricas.

Documentacion oficial: GitHub Actions Documentation

Step 1: Conceptos basicos de GitHub Actions

GitHub Actions funciona colocando archivos YAML en el directorio .github/workflows/ del repositorio.

Estructura basica

# Nombre del workflow
name: CI Pipeline

# Trigger (cuando ejecutar)
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# Definicion de jobs
jobs:
  job-name:
    runs-on: ubuntu-latest
    steps:
      - name: Step 1
        run: echo "Hello"

Conceptos principales

ConceptoDescripcionEjemplo
WorkflowProceso de automatizacion definido en YAMLCI, despliegue
EventTrigger que inicia el workflowpush, pull_request, schedule
JobConjunto de steps ejecutados en el mismo runnertest, build, deploy
StepTareas individualescheckout, npm install
ActionUnidad reutilizable de tareasactions/checkout@v4
RunnerServidor que ejecuta el workflowubuntu-latest, windows-latest

Ciclo de vida del workflow

flowchart LR
    Event["Evento ocurre"] --> Workflow["Workflow inicia"] --> Job["Job ejecuta<br/>(paralelo/secuencial)"] --> Step["Step ejecuta"] --> Done["Completado"]

    subgraph Events["Tipos de evento"]
        direction TB
        E1["push"]
        E2["pull_request"]
        E3["schedule (cron)"]
        E4["workflow_dispatch (manual)"]
        E5["repository_dispatch (API)"]
    end

    Events -.-> Event

Step 2: Crear el primer workflow

Estructura de directorios

your-project/
├── .github/
│   └── workflows/
│       └── ci.yml
├── src/
├── package.json
└── README.md

.github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      # Checkout del repositorio
      - name: Checkout repository
        uses: actions/checkout@v4

      # Setup de Node.js
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      # Instalar dependencias
      - name: Install dependencies
        run: npm ci

      # Ejecutar Lint
      - name: Run linter
        run: npm run lint

      # Ejecutar tests
      - name: Run tests
        run: npm test

      # Subir reporte de cobertura
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

Diferencia entre npm ci y npm install

ComandoUsoCaracteristicas
npm ciEntorno CIUsa package-lock.json estrictamente, rapido
npm installEntorno desarrolloPrioriza package.json, puede actualizar lock

Mejores practicas: Usa siempre npm ci en entornos CI.

Step 3: Matrix builds

Ejecuta tests paralelos en multiples versiones de Node.js y sistemas operativos.

name: Matrix Build

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 22]
      fail-fast: false  # Continuar aunque uno falle

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - run: npm ci
      - run: npm test

Exclusion e inclusion en matrix

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node-version: [18, 20]
    # Excluir combinaciones especificas
    exclude:
      - os: windows-latest
        node-version: 18
    # Agregar combinaciones
    include:
      - os: ubuntu-latest
        node-version: 22
        experimental: true

Step 4: Build y guardado de artefactos

name: Build and Upload

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install and Build
        run: |
          npm ci
          npm run build

      # Subir artefactos de build
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7

  # Usar artefactos en el siguiente job
  deploy:
    needs: build
    runs-on: ubuntu-latest

    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-output

      - name: Deploy
        run: |
          echo "Deploying..."
          ls -la

Step 5: Variables de entorno y secrets

Como configurar secrets

  1. Repositorio GitHub -> Settings -> Secrets and variables -> Actions
  2. Click en “New repository secret”
  3. Ingresar nombre (ej: DEPLOY_TOKEN) y valor

Tipos de secrets

TipoAlcanceUso
Repository secretsRepositorio unicoAPI keys, tokens
Environment secretsSolo ambiente especificoPara produccion/staging
Organization secretsTodos los repos de la orgCuentas de servicio comunes
name: Deploy with Secrets

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # Especificar environment (permite flujo de aprobacion)

    steps:
      - uses: actions/checkout@v4

      # Usar secrets como variables de entorno
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          echo "Deploying with API key..."
          ./scripts/deploy.sh

      # Token automatico proporcionado por GitHub
      - name: Create Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release create v1.0.0

Mejores practicas de seguridad

  1. Principio de minimo privilegio: Otorgar solo permisos necesarios
  2. Rotacion de secrets: Actualizar periodicamente
  3. Prevencion de exposicion en logs: Los secrets se enmascaran automaticamente
  4. Restriccion de acceso desde forks: Limitar acceso a secrets en PRs
# Establecer permisos explicitamente
permissions:
  contents: read
  packages: write
  id-token: write  # Para autenticacion OIDC

Nota de seguridad: Los secrets se enmascaran automaticamente como *** en logs, pero aun asi ten cuidado de no exponerlos accidentalmente.

Step 6: Condiciones y filtros

name: Conditional Workflow

on:
  push:
    branches: [main, develop]
    # Ejecutar solo cuando cambian paths especificos
    paths:
      - 'src/**'
      - 'package.json'
    # Excluir paths especificos
    paths-ignore:
      - '**.md'
      - 'docs/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    # Ejecutar solo en rama develop
    if: github.ref == 'refs/heads/develop'
    steps:
      - run: echo "Deploying to staging..."

  deploy-production:
    needs: test
    runs-on: ubuntu-latest
    # Solo en rama main con tag
    if: github.ref == 'refs/heads/main' && startsWith(github.ref, 'refs/tags/')
    steps:
      - run: echo "Deploying to production..."

  notify-on-failure:
    needs: [test]
    runs-on: ubuntu-latest
    # Ejecutar solo si fallo
    if: failure()
    steps:
      - name: Notify Slack
        run: echo "Tests failed!"

Ejemplos de expresiones condicionales

# Nombre de rama
if: github.ref == 'refs/heads/main'

# Tipo de evento
if: github.event_name == 'pull_request'

# Actor
if: github.actor == 'dependabot[bot]'

# Condicion compuesta
if: github.ref == 'refs/heads/main' && github.event_name == 'push'

# Basado en resultado
if: success()  # Job anterior exitoso
if: failure()  # Job anterior fallo
if: always()   # Siempre ejecutar
if: cancelled() # Al cancelar

Step 7: Uso de cache

Reduce tiempo de build con cache de dependencias.

name: Build with Cache

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'  # Cache automatico

      # Gestionar cache manualmente
      - name: Cache node modules
        id: cache-npm
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      # Instalar si no hay cache
      - name: Install dependencies
        if: steps.cache-npm.outputs.cache-hit != 'true'
        run: npm ci

      - run: npm run build

Estrategias de cache

ObjetivoEjemplo de keyRestore key
npm${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}${{ runner.os }}-node-
pip${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}${{ runner.os }}-pip-
Gradle${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}${{ runner.os }}-gradle-

Step 8: Workflows reutilizables

Composite Action (Action compuesta)

.github/actions/setup-node-and-install/action.yml

name: 'Setup Node and Install'
description: 'Setup Node.js and install dependencies'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '20'

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci
      shell: bash

Ejemplo de uso:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup-node-and-install
        with:
          node-version: '20'
      - run: npm test

Reusable Workflow (Workflow reutilizable)

.github/workflows/reusable-test.yml

name: Reusable Test Workflow

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'
    secrets:
      npm-token:
        required: false

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
      - run: npm test

Lado que invoca:

name: CI

on: [push]

jobs:
  call-test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '20'
    secrets:
      npm-token: ${{ secrets.NPM_TOKEN }}

Step 9: Workflows de despliegue

Despliegue a Vercel

name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Despliegue a AWS S3 + CloudFront

name: Deploy to AWS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - name: Build
        run: |
          npm ci
          npm run build

      - name: Deploy to S3
        run: aws s3 sync dist/ s3://${{ secrets.S3_BUCKET }}

      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \
            --paths "/*"

Consejos de debug

Habilitar logs de debug

Configurar en Secrets del repositorio:

  • ACTIONS_STEP_DEBUG = true
  • ACTIONS_RUNNER_DEBUG = true

Debug dentro del workflow

- name: Debug info
  run: |
    echo "Event: ${{ github.event_name }}"
    echo "Ref: ${{ github.ref }}"
    echo "SHA: ${{ github.sha }}"
    echo "Actor: ${{ github.actor }}"
    echo "Repository: ${{ github.repository }}"
    env

- name: Debug context
  run: echo '${{ toJSON(github) }}'

Test local (act)

# Instalar act
brew install act

# Ejecutar localmente
act push

# Ejecutar job especifico
act -j test

Referencia: nektos/act

Errores comunes y antipatrones

1. Hardcodear secrets

# Mal ejemplo
- run: curl -H "Authorization: Bearer abc123" https://api.example.com

# Buen ejemplo
- run: curl -H "Authorization: Bearer ${{ secrets.API_TOKEN }}" https://api.example.com

2. Configuracion inapropiada de cache key

# Mal ejemplo: Key fija, cache no se actualiza
key: my-cache

# Buen ejemplo: Incluir hash de archivos de dependencia
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

3. Loop infinito

# Mal ejemplo: Se triggerea por su propio commit
on:
  push:
    branches: [main]

jobs:
  commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          echo "update" >> file.txt
          git add .
          git commit -m "Auto update"
          git push

Resumen

Con GitHub Actions obtienes las siguientes ventajas:

  • Verificacion automatica de calidad de codigo
  • Prevencion de errores humanos con despliegue automatizado
  • Workflow consistente compartido por todo el equipo
  • Excelente experiencia de desarrollo integrada con GitHub

Comienza con una simple automatizacion de tests y expande gradualmente hasta el despliegue.

Enlaces de referencia

Documentacion oficial

Mejores practicas y articulos

Herramientas y recursos

Libros

  • “Continuous Delivery” (Jez Humble, David Farley) - Libro de texto de CD/CD
  • “The DevOps Handbook” - Guia completa de DevOps
← Volver a la lista