DevOps from Zero to Hero: CI/CD, El Pipeline Completo
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-nodeconcache: "npm". Esto cachea el store global de npm y lo restaura basandose enpackage-lock.json.- Capas de Docker: Usa BuildKit con
cache-from: type=ghaycache-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:

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: 0Por favor inicie sesión para poder escribir comentarios.