
Reduje el tiempo de validación de nuestro CI en un 95%.
No con caché. No con sharding. No con una herramienta externa sofisticada que agrega otra dependencia a tu pipeline.
Con 15 líneas de bash. Cero dependencias. Cero riesgo en la cadena de suministro.
El PR que incluyó este cambio era más pequeño que la mayoría de los mensajes de commit. Y le ahorra al equipo 7-8 horas al mes.
El Problema
Cada PR ejecutaba el pipeline completo de CI. Linting del backend, pruebas unitarias del backend, pruebas unitarias del frontend, pruebas E2E — toda la batería completa. Un desarrollador cambia un solo archivo de prueba E2E, hace push de la rama y espera 8 minutos con 18 segundos para que el linting del backend le diga que su código de Python está bien.
El código de Python que no tocó.
Este es el comportamiento predeterminado en la mayoría de las configuraciones de CI. Un solo archivo de workflow, un solo trigger, ejecutar todo en cada push. Es simple. Es seguro. También está quemando tiempo del desarrollador en validaciones que no pueden fallar para los cambios que se están haciendo.
La Solución: 15 Líneas de Bash
La idea es simple: usar git diff --name-only para determinar qué archivos cambiaron y luego omitir condicionalmente los jobs que son irrelevantes para esos cambios.
#!/bin/bash
# detect-changes.sh — Determine which CI jobs to run based on changed files
# Called by GitHub Actions workflow to set output variables
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
RUN_BACKEND=false
RUN_FRONTEND=false
RUN_E2E=false
for file in $CHANGED_FILES; do
case "$file" in
backend/*|api/*|*.py) RUN_BACKEND=true ;;
frontend/*|src/*|*.ts|*.tsx) RUN_FRONTEND=true ;;
tests/e2e/*|playwright/*) RUN_E2E=true ;;
.github/*|docker-compose*|Makefile)
# Infrastructure changes: run everything as a safety net
RUN_BACKEND=true; RUN_FRONTEND=true; RUN_E2E=true ;;
esac
done
echo "run-backend=$RUN_BACKEND" >> "$GITHUB_OUTPUT"
echo "run-frontend=$RUN_FRONTEND" >> "$GITHUB_OUTPUT"
echo "run-e2e=$RUN_E2E" >> "$GITHUB_OUTPUT"Eso es todo. El script lee el diff, clasifica cada archivo modificado por directorio y establece flags de salida que los jobs posteriores verifican antes de ejecutarse.
El Workflow de GitHub Actions: Antes y Después
Antes: Ejecutar Todo Siempre
# .github/workflows/ci.yml — BEFORE
name: CI
on: [push, pull_request]
jobs:
backend-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ruff
- run: ruff check backend/
backend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest backend/tests/
frontend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
e2e-test:
runs-on: ubuntu-latest
needs: [backend-test, frontend-test]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright testCada job se ejecuta en cada PR. Una corrección de typo en un archivo de prueba E2E dispara linting del backend, pruebas del backend, pruebas del frontend y pruebas E2E. Tiempo total: 8 minutos 18 segundos.
Después: Ejecutar Solo lo Relevante
# .github/workflows/ci.yml — AFTER
name: CI
on: [push, pull_request]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
run-backend: ${{ steps.changes.outputs.run-backend }}
run-frontend: ${{ steps.changes.outputs.run-frontend }}
run-e2e: ${{ steps.changes.outputs.run-e2e }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for accurate git diff
- id: changes
run: bash .github/scripts/detect-changes.sh
backend-lint:
needs: detect-changes
if: needs.detect-changes.outputs.run-backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ruff
- run: ruff check backend/
backend-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest backend/tests/
frontend-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
e2e-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-e2e == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright testLa única adición es el job detect-changes (se ejecuta en ~5 segundos) y los condicionales if: en cada job posterior. Todo lo demás es idéntico.
Los Resultados
| Tipo de PR | Antes | Después | Ahorro |
|---|---|---|---|
| Cambios solo en E2E | 8 min 18 seg | 39 seg | 95% |
| Cambios solo en frontend | 6 min 13 seg | 1 min 32 seg | 75% |
| Cambios solo en backend | 5 min 45 seg | 2 min 10 seg | 62% |
| Cambios full-stack | 8 min 18 seg | 8 min 18 seg | 0% (ejecuta todo) |
| Cambios en CI/infra | 8 min 18 seg | 8 min 18 seg | 0% (red de seguridad) |
En una semana típica — aproximadamente 40% PRs solo de E2E, 30% solo de frontend, 20% solo de backend, 10% full-stack — esto se proyecta a 7-8 horas al mes de tiempo de espera ahorrado para los desarrolladores.
La red de seguridad es importante: cualquier cambio en los workflows de .github/, configuraciones de Docker o el Makefile dispara el pipeline completo. Nunca te saltas la validación de la infraestructura que ejecuta tu validación.
¿Por Qué No Usar una GitHub Action Para Esto?
Aquí es donde la historia se pone interesante. Audité tres GitHub Actions populares para filtrado de CI basado en rutas antes de escribir el script de bash. Lo que encontré me convenció de que cero dependencias era la decisión correcta.
Action 1: Reporta a una API Comercial
Una action popular de filtrado por rutas hace una llamada API a un servicio comercial en cada ejecución de CI. Lee eso otra vez: tu pipeline de CI reporta a un tercero cada vez que se ejecuta. La action funciona correctamente cuando el servicio está activo y tu suscripción está vigente. Cuando no lo está — y encontré GitHub Issues de usuarios reportando esto — la action puede hacer fallar tu build por completo. Tu pipeline de CI ahora tiene una dependencia de disponibilidad con un producto SaaS que no controlas.
Action 2: Objetivo de Ataque a la Cadena de Suministro (CVE-2025-30066)
En marzo de 2025, la ampliamente utilizada GitHub Action tj-actions/changed-files — usada por más de 23,000 repositorios — fue comprometida en un ataque a la cadena de suministro. El atacante modificó tags de versión existentes para apuntar a código malicioso que volcaba la memoria del runner de CI, exponiendo secretos del workflow incluyendo API keys, tokens y credenciales. CISA emitió un aviso formal (CVE-2025-30066). El ataque fue rastreado hasta un token de acceso personal comprometido perteneciente a una cuenta bot con acceso de escritura al repositorio.
Esto no es un riesgo teórico. Esto pasó. A una action que hace exactamente lo mismo que mi script de bash de 15 líneas.
Action 3: Funciona Bien, Pero Es Innecesaria
La tercera action que evalué está bien mantenida, es open-source y no tiene problemas de seguridad conocidos. Funciona correctamente. También agrega una dependencia que necesita ser fijada a una versión, monitoreada para actualizaciones y auditada periódicamente. Para 15 líneas de bash que hacen lo mismo, la dependencia no se justifica.
El Argumento de Seguridad a Favor de Cero Dependencias
Cada GitHub Action que agregas a tu workflow es código que ejecutas con acceso a los secretos de tu repositorio, tu código fuente y tu entorno de CI. Cada action es una decisión de confianza. El incidente de tj-actions/changed-files demostró que incluso actions populares y ampliamente utilizadas pueden ser comprometidas.
El enfoque del script de bash tiene un perfil de seguridad diferente:
- Sin llamadas de red. El script lee la salida de
git diff. No descarga nada, no reporta a nadie ni depende de nada fuera del repositorio. - Sin ejecución de código de terceros. El script está registrado en tu repositorio y se revisa en el mismo proceso de PR que tu código de aplicación.
- Sin juegos de version pinning. No hay un tag
@v4de qué preocuparse. Sin vector de ataque a la cadena de suministro. El código es tuyo. - Auditoría completa. Cualquier persona del equipo puede leer 15 líneas de bash y entender exactamente qué hace.
A veces la mejor dependencia es no tener dependencia.
Casos Extremos y Redes de Seguridad
Algunas cosas que manejo y que una implementación ingenua pasaría por alto:
Archivos nuevos sin una categoría clara
# If a file doesn't match any known pattern, run everything.
# Better to over-run than under-run.
*)
RUN_BACKEND=true; RUN_FRONTEND=true; RUN_E2E=true ;;El caso por defecto dispara una ejecución completa. Un tipo de archivo no reconocido nunca debería omitir la validación silenciosamente.
Archivos eliminados
git diff --name-only incluye archivos eliminados. Un archivo de backend eliminado aún necesita que la suite de pruebas del backend se ejecute — la eliminación podría romper un import.
Merge commits
El fetch-depth: 0 en el paso de checkout es crítico. Sin el historial completo de git, el git diff contra la rama base produce resultados inexactos. He visto configuraciones de CI con fetch-depth: 1 (el valor predeterminado) que no detectan cambios porque el clon superficial no contiene la base del merge.
Lo Que Puedes Hacer el Lunes por la Mañana
Mide tu línea base actual. ¿Cuál es la duración promedio de CI para tu tipo de PR más común? Si no lo sabes, revisa las últimas 20 ejecuciones del workflow.
Categoriza tus jobs de CI por directorio. ¿Qué jobs corresponden a qué partes del codebase? La mayoría de los pipelines tienen un mapeo natural.
Escribe el script de bash. Empieza con la versión de arriba y adapta los patrones de directorios para que coincidan con la estructura de tu repositorio.
Agrega el job detect-changes. Conéctalo a tu workflow con condicionales
if:.Agrega la red de seguridad. Cualquier cambio en la configuración de CI, archivos de Docker o scripts de infraestructura debe disparar una ejecución completa. Siempre.
Mide el resultado. Compara los mismos tipos de PR antes y después. Los números van a hacer el caso mejor que cualquier argumento.
La Lección Más Amplia
El pipeline de CI es la infraestructura más descuidada en la mayoría de las organizaciones de ingeniería. Los equipos gastan semanas optimizando consultas a la base de datos que ahorran 200ms por request, y luego ven a cada desarrollador sentado durante 8 minutos de validación de CI irrelevante 5 veces al día sin cuestionarlo.
La solución aquí no es ingeniosa. No es novedosa. Son 15 líneas de bash que hacen una pregunta simple: ¿los cambios en este PR realmente afectan el código que este job valida?
Si la respuesta es no, omítelo. Eso no es tomar atajos. Es respetar el tiempo de tu equipo.
15 líneas. Cero dependencias. 95% más rápido. A veces la solución más simple es la que nadie intenta porque parece demasiado simple para funcionar.

