Las pruebas de la lección anterior responden a la pregunta "¿funciona?". Queda otra pregunta, distinta y también importante: "¿es este código algo con lo que podamos convivir dentro de un año?". Hay una familia entera de problemas que ninguna prueba detecta —código incoherente, variables sin usar, promesas sin await, funciones de doscientas líneas, la misma lógica copiada en tres sitios— y que se paga en cada lectura futura. El análisis estático examina el código sin ejecutarlo y detecta buena parte de eso en segundos. En esta lección veremos por qué ese trabajo pertenece al pipeline y no a la revisión humana, en qué se diferencian exactamente Prettier, ESLint y tsc --noEmit con ejemplos concretos de lo que cada uno pilla y los otros no, cómo se configura el job calidad de Reservalia con --max-warnings 0, qué es un quality gate y por qué el criterio ganador es "nuevo código limpio" en lugar de arreglar toda la deuda de golpe. Terminaremos con los ganchos de pre-commit y las convenciones de commit. El análisis de vulnerabilidades y el SAST de seguridad no entran aquí: son la lección 04-03.
Contenido
- Por qué el análisis estático pertenece al pipeline
- Formateo, linting y comprobación de tipos: tres herramientas, tres problemas
- La configuración de Reservalia y el job
calidad - Análisis más profundo: complejidad, duplicación y code smells
- Quality gates y el criterio de "nuevo código limpio"
- Ganchos de pre-commit con Husky y lint-staged
- Convenciones de commit y su verificación automática
- Cómo empezar en un proyecto con miles de avisos
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Por qué el análisis estático pertenece al pipeline
Marta revisa un pull request de Diego y escribe seis comentarios: cuatro sobre comillas simples frente a dobles y sangrado, uno sobre una variable sin usar y uno señalando que la nueva ruta no valida el identificador del negocio y permitiría leer citas ajenas.
Los cinco primeros comentarios son desperdicio. Los puede producir una máquina en dos segundos, sin cansarse, sin excepciones y sin que nadie se lo tome como algo personal. El sexto es la razón por la que existe la revisión humana.
El principio. Automatiza todo lo objetivable para que la revisión humana pueda hablar de diseño, de seguridad y de si el problema se ha entendido bien.
Hay tres razones adicionales, y las tres importan. Coherencia sin discusiones: cuando la herramienta decide el formato, deja de existir el debate —ninguna configuración de Prettier es objetivamente mejor que otra; lo valioso es que haya una—. Sin sesgo ni cansancio: una persona que revisa el quinto PR del día ya no ve las variables sin usar; la herramienta ve siempre lo mismo. Feedback inmediato y sin coste emocional: que un linter te diga que falta un await es información; que te lo diga un compañero en un comentario público es información y una pequeña factura social.
- Formateo, linting y comprobación de tipos: tres herramientas, tres problemas
Se confunden constantemente. Son tres capas independientes y complementarias.
| Prettier (formateo) | ESLint (linting) | tsc --noEmit (tipos) |
|
|---|---|---|---|
| Pregunta que responde | ¿Está escrito de forma uniforme? | ¿Hay patrones problemáticos? | ¿Encajan los tipos? |
| Cómo decide | Reimprime el código desde su AST | Reglas sobre el AST | Sistema de tipos completo |
| ¿Corrige solo? | Siempre | A veces (--fix) |
Nunca |
| ¿Es opinable? | Sí, pero da igual cuál elijas | Sí, se configura por equipo | No: es correcto o no lo es |
| Velocidad en Reservalia | ~2 s | ~15 s | ~20 s |
Veamos qué detecta cada uno y los otros no.
Solo Prettier. Este código es perfectamente válido y no tiene ningún error de tipos ni ninguna mala práctica; simplemente está escrito de forma distinta a todo lo demás del repositorio:
Prettier lo reescribe sin preguntar. Ni ESLint ni tsc dirían nada.
Solo ESLint. Aquí no hay ningún error de tipos y el formato es impecable, pero el código está mal:
async function cancelarCita(id: number) {
enviarCorreoCancelacion(id); // ← devuelve Promise y nadie la espera
await marcarCancelada(id);
}tsc no protesta: llamar a una función asíncrona sin await es legal. Prettier tampoco: el formato está bien. La regla @typescript-eslint/no-floating-promises lo detecta al instante, y es un fallo real: si el envío del correo falla, el error se pierde en el vacío y nadie se entera.
Solo tsc. Aquí el formato es correcto y no hay patrón sospechoso, pero el programa está roto:
import type { Cita } from '@reservalia/tipos-compartidos';
function resumen(cita: Cita): string {
return `${cita.negocio.nombre} · ${cita.inicioHora}`; // ← el campo se llama 'inicio'
}Solo el sistema de tipos sabe que Cita no tiene un campo inicioHora. Es exactamente el tipo de error que en producción se manifiesta como un undefined en la interfaz del cliente.
La conclusión práctica es que las tres capas son necesarias y que ninguna sustituye a las otras. Y hay una cuarta, complementaria, que no compite con ellas: el análisis de complejidad y duplicación del apartado 4.
- La configuración de Reservalia y el job
calidad
calidadPrettier se configura en .prettierrc.json, en la raíz del monorepo:
Cuatro decisiones que nadie volverá a discutir. Lo importante no es su contenido, sino que estén escritas en el repositorio.
ESLint vive en eslint.config.js. Este es el núcleo de la configuración de Reservalia:
export default [{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: { parser: tsParser, parserOptions: { project: './tsconfig.json' } },
rules: {
'@typescript-eslint/no-floating-promises': 'error', // 1
'@typescript-eslint/no-explicit-any': 'warn', // 2
'no-unused-vars': 'error',
'complexity': ['warn', 12], // 3
'max-lines-per-function': ['warn', 80],
'eqeqeq': 'error', // 4
},
}];no-floating-promiseses la regla del ejemplo anterior. En una aplicación llena de operaciones asíncronas —correos, base de datos, pasarela de pago— es la que más errores reales atrapa.no-explicit-anycomo aviso, no error: convertirla en error de golpe rompería el proyecto entero. El plan para subirla aerrores el apartado 8.complexityymax-lines-per-functionson señales de diseño, no de corrección; por eso son avisos.eqeqeqprohíbe==. En JavaScript,'0' == 0es cierto ynull == undefinedtambién: comparaciones que parecen inocentes y no lo son.
Y ahora el job del pipeline:
calidad:
name: Calidad
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
# ... checkout, setup-node y npm ci ...
- name: Formato
run: npx prettier --check . # 1
- name: Linting
run: npm run lint # 2 → eslint ... --max-warnings 0
- name: Comprobación de tipos
run: npm run typecheck # 3 → tsc --noEmitprettier --check, no--write. En CI queremos comprobar, no modificar: si el pipeline reformatease y volviese a empujar, cambiaría el commit que se está verificando y crearía un lío de trazabilidad. El mensaje de error indica el fichero y la orden exacta para arreglarlo en local.--max-warnings 0es la política más importante del job, y ya venía en lospackage.jsonde la lección 01-04. Sin ella, ESLint termina con código de salida 0 aunque haya 300 avisos, el pipeline se pone verde y los avisos se acumulan hasta que nadie los mira. Con ella, el número de avisos no puede crecer: cada aviso nuevo pone el PR en rojo. La distinciónwarn/errorsigue siendo útil para clasificar la gravedad, pero en el pipeline ambos bloquean.typecheckva aparte delbuild.tsc --noEmitsolo comprueba y no escribe nada, así que puede correr en paralelo con las pruebas y da feedback antes.
Los tres pasos son rápidos: el job calidad completo tarda menos de un minuto, y como es un job independiente, corre en paralelo con test y build.
- Análisis más profundo: complejidad, duplicación y code smells
Los linters miran fichero a fichero. Hay problemas que solo se ven con perspectiva de proyecto:
| Medida | Qué indica | Umbral orientativo |
|---|---|---|
| Complejidad ciclomática | Número de caminos posibles dentro de una función | > 10-15 conviene dividir |
| Complejidad cognitiva | Cuánto cuesta entenderla (penaliza el anidamiento) | > 15 es difícil de leer |
| Duplicación | Porcentaje de líneas repetidas en el proyecto | > 3-5 % merece atención |
| Deuda técnica estimada | Tiempo que costaría arreglar lo detectado | Sirve para comparar módulos |
| Code smells | Patrones que no son errores pero avisan | Su tendencia, no su valor |
Un ejemplo real de Reservalia. La función calcularHuecos empezó con 20 líneas; entre horarios partidos, festivos, duraciones variables y bloqueos manuales, ha llegado a 140 líneas y a una complejidad ciclomática de 23. No falla ninguna prueba —de hecho está bien cubierta— pero cada cambio nuevo tarda el doble y asusta el doble. Eso es exactamente lo que mide la complejidad: el coste futuro de tocarla.
Cuidado con los umbrales: son señales para conversar, no verdades. Una función de complejidad 18 que traduce una tabla de tarifas puede ser perfectamente legible, y una de complejidad 8 con tres niveles de anidamiento y nombres crípticos puede ser infernal.
- Quality gates y el criterio de "nuevo código limpio"
Un quality gate es un conjunto de condiciones que un cambio debe cumplir para considerarse aceptable. Herramientas como SonarQube (autogestionado) o SonarCloud (SaaS) lo evalúan y devuelven un veredicto que el pipeline puede usar para bloquear el merge.
La configuración mínima es un fichero sonar-project.properties en la raíz:
sonar.projectKey=reservalia_monorepo
sonar.sources=apps/api/src,apps/web/src,packages/tipos-compartidos/src
sonar.tests=apps/api/tests,apps/web/tests
sonar.javascript.lcov.reportPaths=apps/api/coverage/lcov.info
sonar.qualitygate.wait=truesonar.qualitygate.wait=true hace que el step espere el veredicto en lugar de terminar en cuanto sube los datos; sin esto, el pipeline pasaría siempre y el gate no bloquearía nada. El paso en el job calidad necesita un secreto:
- name: Análisis de calidad
uses: SonarSource/sonarcloud-github-action@v2
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}Y aquí llega la decisión que separa las adopciones que funcionan de las que fracasan. Reservalia tiene tres años de código y, la primera vez que pasan Sonar, aparecen 1.847 avisos. Hay dos caminos:
| Estrategia | Qué exige | Resultado real |
|---|---|---|
| Arreglar toda la deuda | Parar semanas a limpiar 1.847 avisos | Nunca se hace; el gate se desactiva |
| Nuevo código limpio | Solo el código que este PR toca debe cumplir | Se adopta el mismo día y la deuda baja sola |
El criterio de "nuevo código limpio" (clean as you code) aplica el gate únicamente a las líneas añadidas o modificadas en el PR. Sus condiciones típicas:
- 0 errores nuevos y 0 vulnerabilidades nuevas en el código nuevo.
- Cobertura del código nuevo ≥ 80 %.
- Duplicación del código nuevo < 3 %.
Las ventajas son enormes: es alcanzable desde el primer día, el equipo no tiene que parar, y como cada PR toca código antiguo para modificarlo, la deuda se reduce de forma natural por las zonas que de verdad se tocan —que son las que importan—. Un módulo que nadie ha abierto en dos años puede tener 300 avisos; si nadie lo toca, no molestan a nadie.
- Ganchos de pre-commit con Husky y lint-staged
Un gancho de pre-commit ejecuta comprobaciones en tu máquina antes de crear el commit. Su valor es el ciclo corto: descubrir un fallo de formato en 2 segundos en lugar de en 3 minutos de pipeline. Se instala con npm i -D husky lint-staged, npx husky init y un .husky/pre-commit que contenga npx lint-staged. La configuración va en el package.json de la raíz:
"lint-staged": {
"*.{ts,tsx}": ["prettier --write", "eslint --max-warnings 0 --fix"],
"*.{json,md,yml}": ["prettier --write"]
}La clave está en el nombre: lint-staged solo procesa los ficheros que has puesto en el índice, no todo el repositorio. Por eso tarda un par de segundos en lugar de veinte, y por eso la gente no lo desactiva.
Ahora la advertencia importante: los ganchos locales NUNCA sustituyen al control del pipeline. Tres motivos:
- Se pueden saltar.
git commit --no-verifylos omite, y con prisa se usa. - Están en la máquina de cada uno. Quien clone el repositorio y no ejecute la instalación no tiene ganchos, y no se entera.
- Solo ven lo que has tocado tú. Un cambio en
tipos-compartidospuede romperapps/websin que ningún fichero deapps/webesté en tu índice.
El modelo correcto: los ganchos son una comodidad (feedback rápido y opcional); el pipeline es el control (obligatorio y no evitable). Si tienes que elegir uno solo, elige el pipeline.
- Convenciones de commit y su verificación automática
Un historial de mensajes tipo arreglos, wip, ahora sí no sirve para nada. Commits convencionales impone un formato mínimo:
<tipo>(<ámbito>): <descripción>
feat(citas): permitir reservas recurrentes semanales
fix(agenda): no ofrecer huecos que se solapen con el descanso
chore(deps): actualizar vitest a 1.6.0Los tipos habituales son feat, fix, chore, docs, test, refactor y perf. La verificación se automatiza con commitlint, tanto en local (gancho commit-msg) como en el pipeline sobre los commits del PR. La configuración cabe en un fichero:
¿Por qué molestarse? Porque un formato legible por máquinas habilita tres cosas concretas que veremos más adelante: generar el changelog automáticamente, deducir la siguiente versión (feat → menor, fix → parche, BREAKING CHANGE → mayor) —ambas en la lección 02-06— y filtrar el historial para responder a "¿qué correcciones entraron en producción esta semana?".
Un matiz de estilo que conviene fijar: si el equipo usa squash al fusionar (lección 02-07), el mensaje que cuenta es el título del PR, no los commits intermedios. En ese caso, valida el título del PR y deja libres los commits de trabajo.
- Cómo empezar en un proyecto con miles de avisos
Reservalia tiene 1.847 avisos de Sonar y unos 400 de ESLint. Este es el plan que funciona, en cuatro pasos:
Paso 1: congelar la situación. Activa las herramientas con --max-warnings 0 sobre una línea base existente, de modo que el número no pueda crecer. ESLint permite generar un fichero de referencia con los avisos actuales; alternativamente, empieza aplicando las reglas solo a las carpetas ya limpias e incorpora las demás por fases.
Paso 2: formatear todo de una vez. Ejecuta npx prettier --write . en un commit propio, sin ningún cambio funcional, y añade su SHA a .git-blame-ignore-revs para que git blame siga siendo útil:
npx prettier --write .
git commit -am "chore: aplicar Prettier a todo el repositorio"
git rev-parse HEAD >> .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revsEs el único cambio masivo recomendable, porque es mecánico y verificable: las pruebas siguen pasando y ninguna línea cambia de significado.
Paso 3: activar el gate solo sobre el código nuevo. El criterio del apartado 5. Desde ese momento, todo lo que entra está limpio.
Paso 4: subir el listón por escalones. Cada mes, una regla pasa de warn a error cuando su número de incumplimientos ya está cerca de cero. En Reservalia el orden acordado es: no-floating-promises (ya en error), luego no-unused-vars, luego no-explicit-any.
Lo que no funciona: la semana de limpieza general. Produce un diff de 20.000 líneas imposible de revisar, un riesgo de regresión alto, y a los tres meses el problema ha vuelto porque nada impedía que volviera.
Errores Comunes y Consejos
Error 1: revisar formato en los pull requests. Si tu equipo discute comillas en los comentarios de revisión, no le falta disciplina: le falta Prettier en el pipeline.
Error 2: linter sin --max-warnings 0. El pipeline sale verde con cientos de avisos dentro y todo el mundo aprende a ignorarlos. Es la forma más silenciosa de tener análisis estático que no sirve para nada.
Error 3: que el pipeline reformatee y empuje el resultado. Cambia el commit que se está verificando, dispara nuevas ejecuciones y enreda la trazabilidad del artefacto. En CI, --check; en local, --write.
Error 4: confiar solo en los ganchos de pre-commit. Se saltan con --no-verify, no existen para quien no los instaló y solo ven los ficheros del índice.
Error 5: exigir el 100 % de limpieza desde el primer día. Un gate inalcanzable se desactiva en dos semanas y se lleva por delante también la parte que sí funcionaba.
Consejo 1: la misma configuración en local y en CI. Un .prettierrc.json y un eslint.config.js versionados, y los mismos comandos npm en ambos sitios. Si el editor de Marta formatea distinto que el pipeline, la guerra es eterna.
Consejo 2: mide la deuda como tendencia. El número absoluto de avisos no dice nada; que baje mes a mes, sí. Publícalo en el resumen del job con $GITHUB_STEP_SUMMARY.
Consejo 3: cuando desactives una regla, deja escrito por qué. Un // eslint-disable-next-line sin explicación es una decisión perdida; con una frase, es una decisión documentada.
Ejercicios
Ejercicio 1
Para cada fragmento, di qué herramienta lo detecta (Prettier, ESLint o tsc --noEmit) y por qué las otras dos no:
// A
const total = precio * cantidad
+ descuento;
// B
function buscar(id: string) { return citas.find(c => c.id === id); } // c.id es number
// C
citas.forEach(async (c) => { await notificar(c); }); // el forEach no esperaEjercicio 2
El equipo activa Sonar y aparecen 1.847 avisos. El responsable propone: "dos semanas de parón para dejarlo a cero, y a partir de ahí el gate exige cero avisos en todo el proyecto". Argumenta en contra y propón un plan alternativo con pasos concretos.
Ejercicio 3
Diego propone quitar el job calidad del pipeline porque "ya tenemos Husky con lint-staged en local, y así ahorramos un minuto por PR". Da tres argumentos por los que la propuesta es peligrosa.
Soluciones
Solución 1.
- A → Prettier. Es un problema puramente de formato: falta un punto y coma y el salto de línea es arbitrario. El código es válido y con tipos correctos, así que
tsccalla; no hay patrón problemático, así que ESLint calla. - B →
tsc --noEmit. Compararc.id(número) conid(cadena) mediante===da siemprefalse. Solo el sistema de tipos conoce la forma deCita; el formato es correcto y la construcción es idiomática, así que ni Prettier ni ESLint tienen nada que decir. (Con la información de tipos activada, alguna regla de@typescript-eslinttambién puede detectarlo, precisamente porque usa el mismo motor de tipos.) - C → ESLint.
forEachno espera a las funciones asíncronas: el bucle termina antes de que se envíe ninguna notificación. Es código válido (tsccalla) y bien formateado (Prettier calla), pero está mal. Lo detectano-misused-promises.
Solución 2. Argumentos en contra: (a) dos semanas sin entregar valor son un coste enorme y difícil de justificar; (b) un diff de miles de líneas es irrevisable, y el riesgo de introducir regresiones en zonas sin cobertura es alto; (c) exigir cero avisos en todo el proyecto convierte cualquier PR que roce un fichero antiguo en una tarea de limpieza no planificada, lo que empuja al equipo a esquivar el gate; (d) buena parte de esos avisos están en código que nadie toca, así que arreglarlos no aporta valor real.
Plan alternativo: (1) aplicar Prettier a todo en un commit mecánico y añadirlo a .git-blame-ignore-revs; (2) activar --max-warnings 0 sobre la línea base actual para que el número no pueda crecer; (3) configurar el quality gate en modo nuevo código limpio con cobertura ≥ 80 % y 0 errores nuevos; (4) subir una regla de warn a error cada mes, empezando por las de mayor impacto; (5) publicar la tendencia mensual de avisos para que la mejora sea visible.
Solución 3. Tres argumentos: (1) los ganchos se saltan con git commit --no-verify y no existen para quien clone el repositorio sin instalarlos, así que no son un control sino una comodidad; (2) lint-staged solo analiza los ficheros del índice, de modo que un cambio en packages/tipos-compartidos que rompa apps/web pasaría desapercibido —solo un typecheck completo en el pipeline lo detecta—; (3) sin el check obligatorio en el PR, la regla de protección de rama de la 02-07 no tiene nada que exigir, y el criterio de calidad pasa a depender de la buena voluntad de cada persona. Además, el ahorro es falso: el job calidad corre en paralelo con test y build, así que no añade un minuto al tiempo total del pipeline.
Conclusión
El pipeline de Reservalia ya no solo comprueba que el código funciona, sino que sea sostenible:
- El análisis estático pertenece al pipeline y no a la revisión humana: automatizar lo objetivable libera la revisión para hablar de diseño, de seguridad y de si el problema se ha entendido bien.
- Prettier, ESLint y
tsc --noEmitresuelven tres problemas distintos y ninguno sustituye a los otros: formato uniforme, patrones problemáticos como una promesa sinawait, y coherencia de tipos como un campo que no existe. - El job
calidadejecutaprettier --check(nunca--writeen CI),npm run lintcon--max-warnings 0—la política que impide que la deuda crezca— ynpm run typecheck, en menos de un minuto y en paralelo contestybuild. - El análisis profundo aporta complejidad, duplicación y code smells: medidas del coste futuro de tocar el código, útiles como señal para conversar y no como verdad absoluta.
- Los quality gates solo se adoptan si son alcanzables. El criterio ganador es "nuevo código limpio": aplicar las condiciones únicamente a las líneas que el PR añade o modifica, con lo que la deuda se reduce sola por las zonas que de verdad se tocan.
- Los ganchos de pre-commit (Husky + lint-staged) dan feedback en dos segundos, pero se saltan con
--no-verify, no existen para quien no los instaló y solo ven el índice: son comodidad, nunca control. - Las convenciones de commit verificadas con commitlint hacen el historial legible por máquinas, y eso habilita el changelog automático y el versionado deducido de los commits.
- Y hay un plan realista para un proyecto con 1.847 avisos: congelar la línea base, formatear todo en un commit mecánico, aplicar el gate solo al código nuevo y subir el listón por escalones.
Reservalia tiene ya tres jobs —calidad, test y build— y una certeza razonable sobre cada commit. Lo que todavía no tiene es algo que guardar: la imagen que construye el job build se evapora al terminar el runner. En la siguiente lección, Artefactos, Versionado y Promoción, veremos el principio de construir una vez, desplegar muchas veces, por qué un artefacto inmutable es la base de la trazabilidad, qué estrategia de versionado elegir y por qué latest es peligrosa, cómo se promociona el mismo reservalia/api:a3f9c21 de dev a staging y a prod, y cómo saber desde una máquina en producción qué commit está ejecutando. Y añadiremos al ci.yml el job publicar.
Curso de CI/CD: Integración y Despliegue Continuo
Módulo 1: Introducción a CI/CD
- Conceptos Básicos de CI/CD
- Beneficios de CI/CD
- Herramientas Populares de CI/CD
- El Proyecto del Curso: la Aplicación que Vamos a Automatizar
- Métricas DORA: Cómo se Mide la Entrega de Software
Módulo 2: Integración Continua (CI)
- Introducción a la Integración Continua
- Configuración de un Entorno de CI
- Automatización de la Construcción
- Pruebas Automatizadas
- Calidad de Código y Análisis Estático
- Artefactos, Versionado y Promoción
- Integración con Control de Versiones
Módulo 3: Despliegue Continuo (CD)
- Introducción al Despliegue Continuo
- Automatización del Despliegue
- Infraestructura como Código y Entornos Reproducibles
- Estrategias de Despliegue
- Feature Flags, Rollback y Recuperación ante Fallos
- Monitoreo y Retroalimentación
Módulo 4: Prácticas Avanzadas de CI/CD
- Pipelines de CI/CD
- Gestión de Dependencias
- Seguridad en CI/CD
- Escalabilidad y Rendimiento
- Pipeline as Code: Plantillas, Reutilización y Pruebas del Pipeline
- Bases de Datos en el Pipeline: Migraciones Seguras
Módulo 5: Implementación de CI/CD en Proyectos Reales
- Caso de Estudio: Proyecto Web
- Caso de Estudio: Aplicación Móvil
- Caso de Estudio: Microservicios
- Caso de Estudio: Modernizar un Proyecto Legacy
Módulo 6: Herramientas y Tecnologías
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker y Kubernetes
- GitHub Actions a Fondo
- Comparativa y Criterios para Elegir Herramienta
Módulo 7: Ejercicios Prácticos
- Ejercicio 1: Configuración de un Pipeline Básico
- Ejercicio 2: Integración de Pruebas Automatizadas
- Ejercicio 3: Despliegue en un Entorno de Producción
- Ejercicio 4: Monitoreo y Retroalimentación
- Ejercicio 5: Endurecer el Pipeline con Seguridad y Secretos
- Proyecto Final: Pipeline Completo de Extremo a Extremo
