DevOps from Zero to Hero: CI/CD, El Pipeline Completo

2026-06-05 | Gabriel Garrido | 21 min de lectura
Share:

Apoya este blog

Si te resulta util este contenido, considera apoyar el blog.

Introduccion

Bienvenido al articulo dieciseis de la serie DevOps from Zero to Hero. A lo largo de los quince articulos anteriores cubrimos todo, desde escribir una API en TypeScript, pasando por control de versiones, testing, CI, infraestructura como codigo, Kubernetes, Helm, secretos, y mas. Cada pieza resolvio un problema especifico, pero todavia no las cosimos todas juntas en un pipeline cohesivo de punta a punta.


Eso cambia ahora. En este articulo vamos a construir un pipeline completo de CI/CD que lleva tu codigo desde un pull request hasta produccion. No un ejemplo de juguete. Un workflow real de GitHub Actions con multiples jobs que hace lint, testea, construye, despliega a staging, corre smoke tests, espera aprobacion manual, y despues promueve a produccion. Tambien vamos a cubrir estrategias de deploy, procedimientos de rollback, y buenas practicas para mantener tu pipeline rapido y confiable.


Si venias siguiendo la serie, pensa en este articulo como el pegamento que conecta todo. Si estas arrancando de cero, no te preocupes. Vamos a explicar cada parte a medida que avancemos.


Vamos a meternos de lleno.


La filosofia del pipeline

Antes de escribir una sola linea de YAML, establezcamos los principios que guian un buen pipeline de CI/CD:


  • Cada commit a main deberia ser desplegable: Si algo esta en main, fue linteado, testeado y construido. Esta listo para salir. Si no esta listo, no deberia estar en main.
  • Los ambientes son puertas, no destinos: Staging existe para validar, no para acumular. El codigo deberia fluir por staging rapido, no quedarse ahi semanas. Produccion es el destino.
  • Fallar rapido, fallar fuerte: Si algo esta roto, queres saberlo en segundos, no en minutos. Pone los checks mas baratos primero (lint, formato) y los caros despues (tests de integracion, builds).
  • Automatizacion sobre procesos manuales: Cada paso manual es un paso que se puede olvidar, hacer mal, o saltear bajo presion. Automatiza todo excepto la aprobacion final a produccion.
  • Reproducibilidad: Tu pipeline deberia producir el mismo resultado ya sea que lo corras hoy o dentro de tres meses. Fija tus versiones, cachea tus dependencias, y usa artefactos inmutables.

Estos no son ideales abstractos. Son decisiones de ingenieria que previenen caidas, reducen el toil, y te dejan deployar con confianza. Cada decision de diseno en el pipeline que vamos a construir se remonta a uno de estos principios.


Vista general de las etapas del pipeline

Nuestro pipeline va a tener siete etapas, organizadas en tres fases:


Fase 1: Validar (en cada PR y push a main)
  ├── Lint       -> ESLint, Prettier, type checking
  └── Test       -> Tests unitarios, de integracion, coverage

Fase 2: Build y Deploy a Staging (solo en push a main)
  ├── Build      -> Build de imagen Docker y push al registry
  ├── Deploy     -> Deploy al namespace de staging via ArgoCD
  └── Smoke Test -> Health check y tests de API contra staging

Fase 3: Promover a Produccion (aprobacion manual)
  ├── Aprobar    -> Gate de aprobacion manual via GitHub Environments
  └── Deploy     -> Deploy al namespace de produccion

La Fase 1 corre en cada pull request y cada push a main. Es tu red de seguridad. La Fase 2 solo corre en pushes a main (PRs mergeados) porque no queres deployar feature branches a staging. La Fase 3 requiere que un humano haga click en “Approve” antes de que el codigo llegue a produccion. Este es el unico paso manual que mantenemos a proposito, porque deployar a produccion deberia ser una decision consciente.


Environments de GitHub Actions

GitHub Actions tiene una funcionalidad llamada Environments que te da exactamente lo que necesitamos: secretos especificos por ambiente, reglas de proteccion, y historial de deploys. Vamos a configurarlos.


Anda a tu repositorio en GitHub, despues Settings, despues Environments. Crea dos environments:


  • staging: No necesita reglas de proteccion. Los deploys aca deberian ser automaticos despues de que pase el build.
  • production: Agrega una regla de proteccion “Required reviewers”. Elegi uno o mas miembros del equipo que tienen que aprobar antes de que un deploy pueda proceder.

Tambien podes agregar un “Wait timer” a production si queres un periodo de enfriamiento obligatorio entre deploys a staging y produccion. Algunos equipos lo ponen en 15 minutos para darle tiempo extra a los smoke tests de encontrar problemas.


Secretos y variables especificos por ambiente

Cada environment puede tener sus propios secretos y variables. Asi es como manejas el hecho de que staging y produccion usan clusters, namespaces, bases de datos y API keys diferentes sin llenar tu workflow de condicionales if.


Esto es lo que tipicamente configurarias:


Secretos del repositorio (compartidos):
  REGISTRY_USERNAME    -> tu usuario del registry de contenedores
  REGISTRY_PASSWORD    -> tu token del registry de contenedores

Secretos del environment staging:
  KUBE_CONFIG          -> kubeconfig para tu cluster de staging
  DATABASE_URL         -> connection string de la base de datos de staging
  ARGOCD_AUTH_TOKEN    -> token de ArgoCD para staging

Variables del environment staging:
  KUBE_NAMESPACE       -> staging
  APP_URL              -> https://staging.myapp.example.com

Secretos del environment production:
  KUBE_CONFIG          -> kubeconfig para tu cluster de produccion
  DATABASE_URL         -> connection string de la base de datos de produccion
  ARGOCD_AUTH_TOKEN    -> token de ArgoCD para produccion

Variables del environment production:
  KUBE_NAMESPACE       -> production
  APP_URL              -> https://myapp.example.com

Cuando un job especifica environment: staging, solo puede acceder a los secretos y variables de staging. Cuando especifica environment: production, obtiene los de produccion. Este aislamiento previene el peor tipo de error: correr accidentalmente una migracion de produccion contra la base de datos de staging, o viceversa.


Para configurar estos, anda a Settings, despues Environments, hace click en el environment, y agrega tus secretos y variables ahi. Funcionan exactamente como los secretos a nivel repositorio pero estan limitados al environment.


El workflow completo

Aca esta el pipeline completo. Vamos a repasar cada job en detalle despues, pero primero, mira el panorama general:


name: CI/CD Pipeline

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

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

permissions:
  contents: read
  packages: write

jobs:
  # Fase 1: Validar
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"

      - run: npm ci

      - name: Run ESLint
        run: npx eslint .

      - name: Check formatting
        run: npx prettier --check .

      - name: Type check
        run: npx tsc --noEmit

  test:
    name: Test
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: myapp_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    env:
      DATABASE_URL: postgres://test:test@localhost:5432/myapp_test
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"

      - run: npm ci

      - name: Run tests with coverage
        run: npm test -- --coverage

      - name: Upload coverage
        if: github.event_name == 'push'
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/
          retention-days: 14

  # Fase 2: Build y Deploy a Staging
  build:
    name: Build and Push Image
    runs-on: ubuntu-latest
    needs: [lint, test]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=raw,value=latest

      - name: Build and push
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: [build]
    environment: staging
    steps:
      - uses: actions/checkout@v4

      - name: Install ArgoCD CLI
        run: |
          curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
          chmod +x argocd
          sudo mv argocd /usr/local/bin/

      - name: Deploy to staging
        env:
          ARGOCD_SERVER: ${{ vars.ARGOCD_SERVER }}
          ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
        run: |
          argocd app set myapp-staging \
            --parameter image.tag=${{ github.sha }} \
            --grpc-web

          argocd app sync myapp-staging \
            --grpc-web \
            --timeout 300

          argocd app wait myapp-staging \
            --grpc-web \
            --timeout 300 \
            --health

  smoke-test:
    name: Smoke Tests
    runs-on: ubuntu-latest
    needs: [deploy-staging]
    environment: staging
    steps:
      - uses: actions/checkout@v4

      - name: Wait for deployment to stabilize
        run: sleep 30

      - name: Health check
        run: |
          for i in $(seq 1 10); do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
              "${{ vars.APP_URL }}/health")
            if [ "$STATUS" = "200" ]; then
              echo "Health check passed on attempt $i"
              exit 0
            fi
            echo "Attempt $i: got $STATUS, retrying in 10s..."
            sleep 10
          done
          echo "Health check failed after 10 attempts"
          exit 1

      - name: API smoke test
        run: |
          RESPONSE=$(curl -s -w "\n%{http_code}" \
            "${{ vars.APP_URL }}/api/v1/status")
          BODY=$(echo "$RESPONSE" | head -n -1)
          STATUS=$(echo "$RESPONSE" | tail -n 1)

          echo "Status: $STATUS"
          echo "Body: $BODY"

          if [ "$STATUS" != "200" ]; then
            echo "API smoke test failed with status $STATUS"
            exit 1
          fi

          echo "API smoke test passed"

      - name: Run E2E tests against staging
        env:
          BASE_URL: ${{ vars.APP_URL }}
        run: |
          npm ci
          npx playwright test tests/e2e/smoke.spec.ts

  # Fase 3: Promover a Produccion
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: [smoke-test]
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Install ArgoCD CLI
        run: |
          curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
          chmod +x argocd
          sudo mv argocd /usr/local/bin/

      - name: Deploy to production
        env:
          ARGOCD_SERVER: ${{ vars.ARGOCD_SERVER }}
          ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
        run: |
          argocd app set myapp-production \
            --parameter image.tag=${{ github.sha }} \
            --grpc-web

          argocd app sync myapp-production \
            --grpc-web \
            --timeout 300

          argocd app wait myapp-production \
            --grpc-web \
            --timeout 300 \
            --health

      - name: Verify production deployment
        run: |
          for i in $(seq 1 10); do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
              "${{ vars.APP_URL }}/health")
            if [ "$STATUS" = "200" ]; then
              echo "Production health check passed"
              exit 0
            fi
            echo "Attempt $i: got $STATUS, retrying in 10s..."
            sleep 10
          done
          echo "Production health check failed"
          exit 1

Es mucho YAML, asi que vamos a desarmarlo pieza por pieza.


Fase 1: Validar

Los jobs de lint y test corren en paralelo en cada push y pull request. Son los checks mas baratos y rapidos, asi que van primero.


El job de lint corre tres checks: ESLint para calidad de codigo, Prettier para formato, y el compilador de TypeScript para seguridad de tipos. Si cualquiera de estos falla, el pipeline se detiene. No tiene sentido construir una imagen Docker para codigo que no compila.


El job de test levanta un contenedor de servicio de PostgreSQL. GitHub Actions te deja definir servicios junto a tu job, y estan disponibles en localhost como una base de datos local. Los tests corren con coverage habilitado, y el reporte de coverage se sube como artefacto para revision posterior.


Nota que lint y test no dependen uno del otro. Corren en paralelo por defecto, lo que significa que la fase de validacion toma lo que dure el mas lento de los dos, no la suma de ambos.


Fase 2: Build y deploy a staging

El job de build solo corre en pushes a main (no en pull requests) y solo despues de que pasen tanto lint como test. Esto se controla con la dependencia needs: [lint, test] y el condicional if.


Usamos Docker Buildx con cache de GitHub Actions (cache-from: type=gha). Esto significa que los builds subsiguientes reusan capas cacheadas, lo que puede reducir el tiempo de build de minutos a segundos. La imagen se tagea con el SHA de Git y se pushea a GitHub Container Registry (GHCR).


El job deploy-staging usa el CLI de ArgoCD para actualizar el tag de la imagen y sincronizar la aplicacion. ArgoCD entonces maneja el deploy real a Kubernetes: actualiza el manifest del deployment, espera a que los nuevos pods esten healthy, y reporta. El comando argocd app wait bloquea hasta que el deploy este completamente rolleado y healthy, asi el pipeline sabe si el deploy fue exitoso o fallo.


Si no estas usando ArgoCD, podes reemplazar esto con comandos kubectl:


      - name: Deploy to staging with kubectl
        run: |
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
          export KUBECONFIG=kubeconfig

          kubectl set image deployment/myapp \
            myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
            -n staging

          kubectl rollout status deployment/myapp \
            -n staging \
            --timeout=300s

          rm kubeconfig

El punto clave es el mismo: actualizar la imagen, despues esperar a que el rollout termine antes de seguir adelante.


Smoke tests en detalle

El job de smoke test es el guardian entre staging y produccion. Responde una pregunta: lo que acabamos de deployar, esta funcionando?


Corremos tres niveles de smoke tests:


  • Health check: Un request HTTP simple a /health. Si el servidor no responde, todo lo demas es irrelevante. Reintentamos hasta 10 veces con intervalos de 10 segundos porque los deploys pueden tardar un momento en estabilizarse.
  • API smoke test: Un request a un endpoint real de la API. Esto valida que la aplicacion no solo esta corriendo sino que realmente esta sirviendo requests correctamente. Chequeamos tanto el status code como que el cuerpo de la respuesta sea valido.
  • E2E smoke test: Un test de Playwright que carga la aplicacion en un navegador y ejecuta algunos flujos criticos de usuario. Esto atrapa problemas que los tests a nivel API no ven, como bundles de JavaScript rotos o paths de CDN mal configurados.

No necesitas los tres niveles desde el dia uno. Arranca solo con el health check. Agrega el test de API cuando tengas una API. Agrega el test E2E cuando tengas Playwright configurado. Lo importante es tener algo que valide el deploy antes de promover a produccion.


Aca hay un smoke test minimo con Playwright:


import { test, expect } from "@playwright/test";

const BASE_URL = process.env.BASE_URL || "http://localhost:3000";

test.describe("Smoke Tests", () => {
  test("homepage loads successfully", async ({ page }) => {
    const response = await page.goto(BASE_URL);
    expect(response?.status()).toBe(200);
    await expect(page.locator("h1")).toBeVisible();
  });

  test("API returns valid response", async ({ request }) => {
    const response = await request.get(`${BASE_URL}/api/v1/status`);
    expect(response.status()).toBe(200);

    const body = await response.json();
    expect(body).toHaveProperty("status", "ok");
  });

  test("login page renders", async ({ page }) => {
    await page.goto(`${BASE_URL}/login`);
    await expect(page.locator('input[name="email"]')).toBeVisible();
    await expect(page.locator('button[type="submit"]')).toBeVisible();
  });
});

Mantene los smoke tests rapidos. Deberian correr en menos de un minuto. Si necesitas cobertura E2E completa, corré eso en un workflow separado. Los smoke tests son sobre confianza, no completitud.


Promocion a produccion y aprobacion manual

El job deploy-production tiene environment: production, lo que activa las reglas de proteccion que configuraste antes. Cuando el pipeline llega a este job, se pausa y muestra un boton “Review deployments” en la UI de GitHub Actions. Los reviewers requeridos que configuraste reciben una notificacion, y el pipeline espera hasta que uno de ellos haga click en “Approve.”


Esto es intencional. Los deploys a produccion deberian ser una decision deliberada. El paso de aprobacion le da a tu equipo un momento para preguntarse: los smoke tests se vieron bien? Hay algun problema conocido? Es un buen momento para deployar (no un viernes a la tarde)?


Una vez aprobado, el deploy a produccion sigue el mismo patron que staging: actualizar el tag de la imagen, sincronizar con ArgoCD, esperar el rollout, y verificar con un health check.


Te podrias preguntar por que no corremos la suite completa de smoke tests contra produccion. Algunos equipos lo hacen, y esta bien. Pero hay un tradeoff: correr tests contra produccion significa que tus tests pueden fallar por problemas especificos de produccion (rate limiting, edge cases de datos reales), y un test que falla despues del deploy puede causar confusion sobre si fue el deploy el que fallo. Un health check simple suele alcanzar para la verificacion de produccion.


Estrategias de deployment

El pipeline que construimos usa la estrategia de deployment por defecto de Kubernetes: rolling update. Pero vale la pena entender las alternativas y cuando usarlas.


Rolling update (por defecto)


Esto es lo que Kubernetes hace out of the box. Gradualmente reemplaza pods viejos con pods nuevos, uno a la vez (o en batches). En cualquier punto durante el rollout, algunos pods corren la version vieja y otros la nueva.


apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  replicas: 3
  template:
    spec:
      containers:
        - name: myapp
          image: ghcr.io/myorg/myapp:abc123
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10

  • maxSurge: 1 significa que Kubernetes puede crear un pod extra por encima del conteo de replicas deseado durante el rollout.
  • maxUnavailable: 0 significa que ningun pod se remueve hasta que su reemplazo este listo. Esto garantiza zero downtime.
  • readinessProbe le dice a Kubernetes cuando un pod nuevo esta listo para recibir trafico. Sin esto, Kubernetes podria enviar requests a un pod que todavia esta arrancando.

Los rolling updates son la eleccion correcta para la mayoria de las aplicaciones. Son simples, sin downtime, y bien soportados por todas las distribuciones de Kubernetes.


Blue-green deployment


En un deploy blue-green, tenes dos ambientes identicos corriendo: blue (produccion actual) y green (la version nueva). El trafico va a blue mientras green se esta deployando y testeando. Una vez que green esta verificado, switcheas el trafico de blue a green de una sola vez.


La ventaja es que el switch es instantaneo y podes rollbackear switcheando de vuelta a blue. La desventaja es que necesitas el doble de recursos durante el deploy. En Kubernetes, podes implementar blue-green manteniendo dos deployments y switcheando el selector del service:


# Deployar la nueva version como "green"
kubectl set image deployment/myapp-green \
  myapp=ghcr.io/myorg/myapp:new-version -n production

kubectl rollout status deployment/myapp-green -n production

# Switchear trafico de blue a green
kubectl patch service myapp \
  -p '{"spec":{"selector":{"version":"green"}}}' -n production

Canary deployment


Un canary deployment rutea un porcentaje chico de trafico (digamos 5%) a la version nueva mientras la mayoria sigue pegandole a la version vieja. Monitoreas las tasas de error y latencia del canary, y si todo se ve bien, gradualmente incrementas el split de trafico hasta que el 100% va a la version nueva.


Los canary deployments son poderosos pero requieren un service mesh (como Istio o Linkerd) o un ingress controller que soporte traffic splitting. Son mas complejos de configurar pero te dan el rollout a produccion mas seguro posible para aplicaciones de alto trafico.


Para esta serie, nos quedamos con rolling updates. Cubren la gran mayoria de los casos de uso, y siempre podes adoptar blue-green o canary mas adelante cuando tus necesidades crezcan.


Estrategias de rollback

Las cosas salen mal. Un deploy pasa todos los tests pero aparece un bug sutil bajo trafico real. Necesitas volver a un estado bueno conocido rapido. Aca estan tus opciones:


Opcion 1: Git revert y push


Este es el enfoque mas simple y confiable. Revertis el commit que causo el problema, pusheas a main, y el pipeline redeploya la version anterior automaticamente.


# Encontrar el commit que causo el problema
git log --oneline -5

# Revertirlo
git revert HEAD

# Pushear a main, lo que dispara el pipeline
git push origin main

La ventaja de este enfoque es que pasa por el pipeline completo: lint, test, build, staging, smoke test, produccion. Sabes que la version revertida funciona porque fue validada en cada etapa. La desventaja es que tarda lo mismo que un deploy normal (5-15 minutos dependiendo de tu pipeline).


Opcion 2: Rollback con ArgoCD


Si estas usando ArgoCD, podes hacer rollback a un sync anterior directamente:


# Listar el historial de syncs
argocd app history myapp-production

# Rollback a una revision especifica
argocd app rollback myapp-production <numero-de-revision>

Esto es mas rapido que un git revert porque saltea el paso de build. ArgoCD simplemente redeploya los manifests anteriores. Sin embargo, crea un drift entre tu estado de Git y lo que esta corriendo en el cluster. Igual deberias crear un git revert despues para mantener Git como la fuente de verdad.


Opcion 3: kubectl rollout undo


Kubernetes guarda un historial de deployments, y podes hacer rollback con un solo comando:


# Rollback a la version anterior
kubectl rollout undo deployment/myapp -n production

# O rollback a una revision especifica
kubectl rollout history deployment/myapp -n production
kubectl rollout undo deployment/myapp -n production --to-revision=3

Como el rollback de ArgoCD, esto es rapido pero crea drift con Git. Usalo para emergencias, despues segui con un git revert apropiado.


La recomendacion es: para rollbacks planificados, usa git revert. Para emergencias, usa kubectl rollout undo o rollback de ArgoCD, despues git revert como follow-up. De cualquier manera, Git siempre deberia reflejar lo que esta corriendo realmente en produccion.


Buenas practicas del pipeline

Ahora que tenes un pipeline funcionando, aca estan las practicas que lo mantienen rapido, confiable y mantenible a lo largo del tiempo:


Fallar temprano


Ordena tus jobs del mas rapido al mas lento. Lint tarda segundos, tests un minuto, builds de Docker varios minutos. Si el codigo no pasa lint, no tiene sentido esperar a que termine un build de Docker. La keyword needs impone este orden.


Paralelizar donde sea posible


Lint y test no dependen uno del otro. Correlos en paralelo. Si tenes multiples suites de test (unitarios, integracion, E2E), dividilos en jobs separados que corran simultaneamente. Cada minuto que le recortes al pipeline es un minuto que tu equipo recupera en cada commit.


Cachear agresivamente


Cachea todo lo que no cambia entre builds:


  • Dependencias de npm: Usa actions/setup-node con cache: "npm". Esto cachea el store global de npm y lo restaura basandose en package-lock.json.
  • Capas de Docker: Usa BuildKit con cache-from: type=gha y cache-to: type=gha,mode=max. Esto guarda y restaura caches de capas usando el backend de cache de GitHub.
  • Fixtures de tests: Si tus tests descargan fixtures grandes, cachealos con actions/cache.

Sin cache, un pipeline tipico tarda 8-12 minutos. Con cache, puede bajar a 3-5 minutos.


Mantenerlo DRY


Si tenes multiples repositorios con pipelines similares, extraé los pasos comunes en workflows reutilizables o composite actions:


# .github/workflows/reusable-deploy.yml
name: Deploy
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      argocd-app:
        required: true
        type: string
    secrets:
      ARGOCD_AUTH_TOKEN:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - name: Install ArgoCD CLI
        run: |
          curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
          chmod +x argocd
          sudo mv argocd /usr/local/bin/

      - name: Deploy
        env:
          ARGOCD_SERVER: ${{ vars.ARGOCD_SERVER }}
          ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
        run: |
          argocd app set ${{ inputs.argocd-app }} \
            --parameter image.tag=${{ github.sha }} \
            --grpc-web
          argocd app sync ${{ inputs.argocd-app }} \
            --grpc-web --timeout 300
          argocd app wait ${{ inputs.argocd-app }} \
            --grpc-web --timeout 300 --health

Despues llamalo desde tu pipeline principal:


  deploy-staging:
    needs: [build]
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      argocd-app: myapp-staging
    secrets:
      ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}

Esto evita duplicar logica de deploy entre los jobs de staging y produccion. Cuando necesites cambiar como funcionan los deploys, lo cambias en un solo lugar.


Fijar las versiones de tus actions


Siempre usa versiones especificas (o SHAs de commits) para las actions, no @main ni @latest. Las actions de terceros pueden cambiar sin aviso, y una version rota puede romper tu pipeline en todos los repositorios de una:


# Bien: fijado a una version especifica
- uses: actions/checkout@v4
- uses: docker/build-push-action@v6

# Mal: sin fijar, puede romperse sin aviso
- uses: actions/checkout@main
- uses: some-org/some-action@latest

Monitoreando tu pipeline

Un pipeline solo es util si sabes como esta performando. GitHub Actions te da varias formas de monitorear la salud del pipeline:


  • Historial de runs: Anda a la tab Actions en tu repositorio. Podes ver cada ejecucion, filtrar por workflow, branch o estado, y profundizar en jobs y pasos individuales.
  • Tendencias de build time: Trackea cuanto tarda tu pipeline a lo largo del tiempo. Si los builds se estan poniendo mas lentos, generalmente significa que tu suite de tests esta creciendo sin optimizacion correspondiente, o tu cache de Docker no esta funcionando bien.
  • Tasa de fallos: Si tu pipeline falla mas del 10% del tiempo en cambios de codigo legitimos, algo es flaky. Los culpables comunes son tests que dependen de la red, race conditions, y timing de startup de contenedores de servicio.
  • Badges de estado: Agrega un badge de estado del workflow a tu README asi el equipo puede ver la salud del pipeline de un vistazo.

Podes agregar un badge de estado a tu README con este markdown:


![CI/CD](https://github.com/myorg/myapp/actions/workflows/ci-cd.yml/badge.svg)

Para monitoreo mas avanzado, considera integrar con herramientas como Datadog CI Visibility o Grafana con el exporter de GitHub Actions. Estos te dan dashboards con percentiles de build time, breakdowns de fallas por job, y alertas cuando los tiempos de build superan un umbral.


Juntando todo

Recapitulemos lo que pasa cuando un developer pushea un cambio a traves de este pipeline:


  • El developer abre un PR: Lint y test corren automaticamente. El PR recibe una marca verde o una X roja. El code review pasa en paralelo.
  • El PR se mergea a main: Lint y test corren de nuevo sobre el codigo mergeado. Despues el job de build crea una imagen Docker tageada con el SHA del commit y la pushea a GHCR.
  • Deploy a staging: ArgoCD actualiza el deployment de staging con el nuevo tag de imagen. El pipeline espera hasta que el rollout este healthy.
  • Smoke tests: Health check, test de API, y test E2E corren contra staging. Si alguno falla, el pipeline se detiene y se notifica al equipo.
  • Aprobacion manual: Un reviewer chequea el deploy de staging, confirma que se ve bien, y hace click en “Approve” en la UI de GitHub Actions.
  • Deploy a produccion: ArgoCD actualiza el deployment de produccion. Un health check final confirma que el deploy esta live.

El proceso completo, desde el merge hasta produccion, tarda unos 10-15 minutos. La mayor parte de ese tiempo esta en las etapas de build y test. Los pasos de deploy en si tardan menos de un minuto cada uno.


Si algo sale mal, el pipeline se detiene en el paso que fallo. Ningun codigo llega a produccion a menos que haya pasado por cada gate. Y si algo se cuela, podes hacer rollback con un git revert en menos de un minuto.


Que viene despues

Ahora tenemos un pipeline completo de CI/CD de punta a punta que lleva codigo desde un pull request hasta produccion con validacion automatizada en cada etapa. En el proximo articulo, vamos a ver monitoreo y observabilidad: como saber que esta haciendo tu aplicacion una vez que esta corriendo en produccion.


Espero que te haya resultado util y que lo hayas disfrutado, hasta la proxima!


Errata

Si encontras mass algun error o tenes alguna sugerencia, por favor mandame un mensaje para que lo pueda corregir.

Tambien, podes ver el codigo fuente y los cambios en los fuentes aca



$ Comentarios

Online: 0

Por favor inicie sesión para poder escribir comentarios.

2026-06-05 | Gabriel Garrido