En la ingeniería de software empresarial, los Quality Gates (umbrales de calidad) representan puntos de control automatizados y deterministas que bloquean sin excepciones la fusión de código a ramas protegidas si se incumple algún criterio de calidad. A medida que la base de código se expande, la ejecución secuencial de análisis estático, linters, pruebas unitarias y suites End-to-End (E2E) en navegadores alcanza duraciones de 30 a 60 minutos, comprometiendo la entrega continua.
La superación de este cuello de botella se basa en el patrón concurrente Fan-out / Fan-in:
strategy.matrix): Las cargas de trabajo se particionan en espacios vectoriales independientes (Fan-out). Los runners ejecutan los jobs simultáneamente en máquinas virtuales aisladas. Configurar fail-fast: false evita que el fallo prematuro de una tarea (como un error tipográfico en tipos) aborte suites E2E extensas, proporcionando al desarrollador un informe integral de fallos en una sola ejecución.tsc --noEmit): Los compiladores de Next.js 15+ (SWC y Turbopack) están optimizados para la máxima velocidad mediante "Type Stripping"—eliminan anotaciones de tipos para transpilar sin validar la corrección del sistema de tipos. La comprobación estática debe desacoplarse en un Quality Gate exclusivo. La combinación del flag --incremental con la preservación de .tsbuildinfo en caché reduce los tiempos entre 4 y 7 veces.eslint.config.mjs). Este formato descarta la resolución en cascada heredada (.eslintrc), ejecuta análisis en paralelo mediante worker threads y optimiza la caché del Árbol de Sintaxis Abstracta (AST) vía .eslintcache.--shard=x/y. Una suite de 500 pruebas se divide entre $N$ máquinas virtuales independientes. Cada fragmento emite un informe binario (blob-report), que luego se descarga y consolida en un dashboard HTML único mediante npx playwright merge-reports (Fan-in).ci-gatekeeper) que depende de todas las etapas previas mediante needs: [...] y evalúa sus estados con if: always().El patrón Fan-out / Fan-in descompone las validaciones en flujos paralelos independientes antes de converger en un punto de control final:
[ Evento Push / Pull Request ]
|
+-------------------+-------------------+
| (Fan-out: Ejecución Paralela Runners) |
v v v
+-----------------+ +-----------------+ +-----------------+
| Job: Typecheck | | Job: ESLint v9 | | Job: Vitest |
| (tsc --noEmit) | | (.eslintcache) | | (>90% Coverage) |
+-----------------+ +-----------------+ +-----------------+
| | |
+-------------------+-------------------+
|
v
+-----------------------------------+
| Job: Playwright Sharding (4 VMs) |
| ├── Shard 1/4 -> Blob 1 |
| ├── Shard 2/4 -> Blob 2 |
| ├── Shard 3/4 -> Blob 3 |
| └── Shard 4/4 -> Blob 4 |
+-----------------------------------+
|
v
+-----------------------------------+
| Step: Playwright Merge Reports |
| (Consolidar Blobs -> HTML Report) |
+-----------------------------------+
|
v
+-----------------------------------+
| (Fan-in: Agregador Quality Gates) |
| Job: ci-gatekeeper |
| (Evaluar needs.*.result == 0) |
+-----------------------------------+
|
+-----------+-----------+
| |
v v
[ Todos los checks GREEN] [ Cualquier FAIL / ERROR ]
| |
v v
(Merge Permitido) (Merge Bloqueado)shard: 1/8 ... 8/8) en GitHub Actions reduce el tiempo a 7 minutos.noImplicitAny, exactOptionalPropertyTypes). Los Quality Gates se ejecutan en paralelo, garantizando la integridad de tipos antes de publicar paquetes al registro de npm.main frente a roturas de contratos de integración compartidos.La puesta en marcha de un marco riguroso de Quality Gates implica configurar cada herramienta para ejecución desatendida y orquestar sus dependencias en GitHub Actions.
En vitest.config.ts se definen umbrales porcentuales mínimos. Si la cobertura disminuye por debajo del umbral, el proceso culmina con error (exit code 1).
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
environment: 'happy-dom',
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json-summary', 'html', 'lcov'],
reportsDirectory: './coverage',
// Umbrales estrictos de Quality Gate empresarial
thresholds: {
lines: 90,
statements: 90,
functions: 85,
branches: 85,
},
exclude: [
'node_modules/**',
'.next/**',
'**/*.d.ts',
'**/*.config.*',
'**/test-utils/**',
],
},
},
});El formato moderno eslint.config.mjs proporciona configuración modular y declarativa sin requerir archivos .eslintignore obsoletos.
import nextPlugin from '@next/eslint-plugin-next';
import reactPlugin from 'eslint-plugin-react';
import hooksPlugin from 'eslint-plugin-react-hooks';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
export default [
{
ignores: ['.next/**', 'dist/**', 'coverage/**', 'node_modules/**'],
},
{
files: ['**/*.{ts,tsx}'],
plugins: {
'@typescript-eslint': tsPlugin,
'react': reactPlugin,
'react-hooks': hooksPlugin,
'@next/next': nextPlugin,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: true,
},
},
rules: {
...tsPlugin.configs['recommended-type-checked'].rules,
...hooksPlugin.configs.recommended.rules,
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs['core-web-vitals'].rules,
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
];La configuración de Playwright se adapta para la emisión de artefactos binarios en formato blob:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
// En CI cada fragmento escribe un reporte binario blob independiente
reporter: process.env.CI ? [['blob', { outputDir: 'blob-report' }]] : [['html']],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});En el ejecutor de CI, cada fragmento se lanza con:
npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
Al finalizar todos los shards, se invoca la consolidación:
npx playwright merge-reports --reporter=html ./all-blob-reports
GitHub Actions aprovisiona runners independientes simultáneos para Typecheck, ESLint, Vitest y 4 shards de Playwright.
Ejecución de tsc --noEmit, ESLint 9 y verificación de cobertura en Vitest (mínimo 90% Statements y 85% Branches).
Los shards 1/4, 2/4, 3/4 y 4/4 ejecutan pruebas E2E distribuidas y cargan reportes blob a los artefactos del workflow.
Descarga de todos los reportes blob y unificación mediante merge-reports en un dashboard HTML interactivo.
El job ci-gatekeeper analiza los resultados. Si todas las etapas culminan con éxito, se autoriza el merge del PR.
A continuación se muestra una configuración completa .github/workflows/quality-gates.yml con jobs paralelos, caché de navegadores, fragmentación Playwright y consolidación con Gatekeeper.
name: CI Quality Gates
on:
push:
branches:
- main
- develop
pull_request:
types:
- opened
- synchronize
- reopened
concurrency:
group: quality-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
checks: write
pull-requests: write
jobs:
# -------------------------------------------------------------
# Job 1: Comprobación aislada de tipos (TypeScript Typecheck)
# -------------------------------------------------------------
typecheck:
name: TypeScript Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configurar Node y pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
- name: Restaurar caché de .tsbuildinfo
uses: actions/cache@v4
with:
path: '**/.tsbuildinfo'
key: ${{ runner.os }}-tsbuildinfo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tsbuildinfo-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Ejecutar tsc --noEmit
run: pnpm tsc --noEmit --incremental
# -------------------------------------------------------------
# Job 2: Análisis estático de código (ESLint 9 Flat Config)
# -------------------------------------------------------------
lint:
name: ESLint v9 Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configurar Node y pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
- name: Restaurar caché de ESLint
uses: actions/cache@v4
with:
path: .eslintcache
key: ${{ runner.os }}-eslint-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-eslint-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Ejecutar ESLint
run: pnpm eslint . --cache --max-warnings 0
# -------------------------------------------------------------
# Job 3: Pruebas unitarias e integradas con umbrales de cobertura
# -------------------------------------------------------------
unit-tests:
name: Vitest & Coverage Thresholds
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configurar Node y pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
- name: Ejecutar Vitest con control de umbrales
run: pnpm vitest run --coverage
# -------------------------------------------------------------
# Job 4: Pruebas E2E de Playwright con fragmentación matricial
# -------------------------------------------------------------
playwright-e2e:
name: Playwright Shard ${{ matrix.shard }}/${{ strategy.job-total }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configurar Node y pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
- name: Obtener versión instalada de Playwright
id: playwright-version
shell: bash
run: |
echo "version=$(pnpm list --depth 0 @playwright/test | grep @playwright/test | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Restaurar caché de navegadores Playwright
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
- name: Instalar Chromium con dependencias del sistema operativo
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- name: Instalar solo dependencias del sistema (ante Cache HIT)
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium
- name: Compilar aplicación Next.js
run: pnpm build
env:
NEXT_TELEMETRY_DISABLED: 1
- name: Ejecutar pruebas E2E para el fragmento
run: pnpm exec playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
CI: true
- name: Cargar reporte blob del fragmento
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shard }}
path: blob-report
retention-days: 1
# -------------------------------------------------------------
# Job 5: Consolidación de reportes de Playwright (Fan-in)
# -------------------------------------------------------------
merge-e2e-reports:
name: Merge Playwright Reports
needs: [playwright-e2e]
if: always()
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configurar Node y pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'pnpm'
- name: Instalar dependencias
run: pnpm install --frozen-lockfile
- name: Descargar todos los reportes blob
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Consolidar reportes en un dashboard HTML
run: npx playwright merge-reports --reporter=html ./all-blob-reports
- name: Cargar dashboard consolidado
uses: actions/upload-artifact@v4
with:
name: merged-playwright-html-report
path: playwright-report
retention-days: 7
# -------------------------------------------------------------
# Job 6: Gatekeeper Agregador de Estado
# -------------------------------------------------------------
ci-gatekeeper:
name: Quality Gatekeeper Check
needs:
- typecheck
- lint
- unit-tests
- playwright-e2e
- merge-e2e-reports
if: always()
runs-on: ubuntu-latest
steps:
- name: Evaluar el estado de todos los Quality Gates
shell: bash
run: |
TYPECHECK="${{ needs.typecheck.result }}"
LINT="${{ needs.lint.result }}"
UNIT="${{ needs.unit-tests.result }}"
E2E="${{ needs.playwright-e2e.result }}"
echo "Resultados de las verificaciones:"
echo "Typecheck: $TYPECHECK"
echo "ESLint: $LINT"
echo "Unit Tests: $UNIT"
echo "Playwright E2E: $E2E"
if [ "$TYPECHECK" != "success" ] || [ "$LINT" != "success" ] || [ "$UNIT" != "success" ] || [ "$E2E" != "success" ]; then
echo "❌ Error: Uno o más Quality Gates no superaron la validación!"
exit 1
fi
echo "✅ Todos los Quality Gates superados con éxito. Merge autorizado."Por defecto, GitHub Actions activa strategy.fail-fast: true. Si el fragmento 1/4 falla debido a un selector incorrecto a los dos segundos de iniciar, GitHub cancela inmediatamente los fragmentos 2, 3 y 4. El desarrollador corrige ese único error, envía un commit, y la siguiente ejecución falla en el fragmento 3. Esto ocasiona ciclos tediosos de correcciones "de un error en un error".
# INCORRECTO: Cancela el resto de fragmentos ante el primer fallo
strategy:
matrix:
shard: [1, 2, 3, 4]
# CORRECTO: Ejecuta todas las pruebas para generar un reporte exhaustivo
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]Ejecutar npx playwright install --with-deps en cada máquina de la matriz. La herramienta descarga los binarios de Chromium, Firefox y WebKit (más de 1 GB de datos) en cada máquina virtual. En una matriz de 4 fragmentos, esto representa más de 4 GB de tráfico innecesario y un retraso de 2 a 3 minutos por runner.
# INCORRECTO: Descargar a ciegas todos los navegadores
- run: npx playwright install --with-deps
# CORRECTO: Almacenar en caché ~/.cache/ms-playwright e instalar solo el navegador objetivo
- uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
- if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromiumConfigurar en GitHub Branch Protection nombres como Playwright Shard (1/4). Si la suite de pruebas crece y el equipo amplía la matriz a shard: [1, 2, 3, 4, 5, 6], el nombre Playwright Shard (1/4) deja de emitirse y la fusión de PRs a la rama main queda bloqueada permanentemente.
INCORRECTO: Branch Protection -> Required Status Checks -> Playwright Shard (1/4)
CORRECTO: Branch Protection -> Required Status Checks -> Quality Gatekeeper Check (tarea agregadora única)Alcanzar el 90% de cobertura añadiendo módulos complejos al array coverage.exclude en vitest.config.ts. Esto crea una falsa sensación de seguridad mientras se despliega a producción código financiero o de negocio sin una sola prueba automatizada.
Aislar la comprobación de tipos de la compilación de Next.js. Habilitar tsc --incremental y la caché de .tsbuildinfo.
Actualizar archivos .eslintrc a eslint.config.mjs con soporte de caché nativa de AST en .eslintcache.
Integrar el proveedor de cobertura V8 con umbrales del 90% en Statements y 85% en Branches en la configuración de pruebas.
Configurar strategy.matrix con el flag --shard, almacenar en caché los binarios de Chromium y consolidar con merge-reports.
Crear el job ci-gatekeeper y definirlo como el único chequeo obligatorio en las reglas de protección de ramas.