diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..1f95a15 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,143 @@ +# JobIT E2E (manual) — smoke Playwright bajo demanda (Sprint 19B). +# Plan: docs/sprints/sprint-19b-e2e-manual-workflow-plan.md +# NO corre en pull_request ni push y NO es required check: se lanza a mano +# (UI de Actions o `gh workflow run e2e.yml --ref dev`). Sin secrets: todas +# las variables son dummies de CI. No toca apps/web/playwright.config.ts: +# el config ya tiene `reuseExistingServer: true`, así que reutiliza la API +# y la web que este workflow arranca. +name: JobIT E2E (manual) + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + name: E2E smoke (Playwright + stack completo) + runs-on: ubuntu-latest + timeout-minutes: 20 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: jobit_e2e + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + # Dummies de CI (base efímera del service): no son secretos reales. + # NODE_ENV=production NO se define a nivel de job: haría que + # `pnpm install` omitiera las devDependencies (tsx, prisma, playwright); + # se aplica solo en el step que arranca la API. + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/jobit_e2e?schema=public + JWT_ACCESS_SECRET: e2e-ci-access-secret + PORT: 4000 + CORS_ORIGIN: http://localhost:3000 + NEXT_PUBLIC_API_BASE_URL: http://localhost:4000 + steps: + - uses: actions/checkout@v5 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v5 + with: + node-version: 20 + cache: pnpm + + - name: Instalar dependencias + run: pnpm install --frozen-lockfile + + - name: Generar cliente Prisma + run: pnpm --filter @jobit/api exec prisma generate + + # Aquí no hay globalSetup de Vitest que migre: el deploy es explícito. + - name: Migrar base de datos E2E + run: pnpm --filter @jobit/api exec prisma migrate deploy + + # Seed autónomo e idempotente (solo tabla Job); los journeys de jobs/match + # dependen de sus ofertas (la búsqueda E2E "Developer" casa con el seed). + - name: Sembrar ofertas de ejemplo + run: pnpm --filter @jobit/api exec tsx prisma/seed.ts + + - name: Build API + run: pnpm --filter @jobit/api build + + - name: Arrancar API en background + env: + NODE_ENV: production + working-directory: apps/api + run: | + nohup node dist/server.js > "$RUNNER_TEMP/jobit-api.log" 2>&1 & + + - name: Esperar API (http://localhost:4000/health) + run: | + for i in $(seq 1 30); do + if curl -sf http://localhost:4000/health > /dev/null; then + echo "API viva tras $i intentos" + exit 0 + fi + sleep 2 + done + echo "::error::La API no respondió en 60s" + cat "$RUNNER_TEMP/jobit-api.log" + exit 1 + + # Decisión del Director (Sprint 19B): build de producción + next start + # (sin compilación on-demand, menos flakiness que next dev en runner). + - name: Build web + run: pnpm --filter @jobit/web build + + - name: Arrancar web en background (next start) + working-directory: apps/web + run: | + nohup pnpm start > "$RUNNER_TEMP/jobit-web.log" 2>&1 & + + - name: Esperar web (http://localhost:3000) + run: | + for i in $(seq 1 30); do + if curl -sf http://localhost:3000 > /dev/null; then + echo "Web viva tras $i intentos" + exit 0 + fi + sleep 2 + done + echo "::error::La web no respondió en 60s" + cat "$RUNNER_TEMP/jobit-web.log" + exit 1 + + - name: Instalar Chromium de Playwright + run: pnpm --filter @jobit/web exec playwright install chromium --with-deps + + # reuseExistingServer: true → Playwright detecta la web viva en :3000 + # y no intenta levantar su propio dev server. + - name: Smoke E2E (Playwright) + run: pnpm --filter @jobit/web test:e2e + + # El config genera traces solo en fallo (trace: retain-on-failure). + - name: Subir traces de Playwright (solo en fallo) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-test-results + path: apps/web/test-results/ + if-no-files-found: ignore + retention-days: 7 + + # Logs de los servidores para diagnóstico si algo falló. + - name: Subir logs de API/web (solo en fallo) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: server-logs + path: ${{ runner.temp }}/jobit-*.log + if-no-files-found: ignore + retention-days: 7 diff --git a/docs/specs/features/ci-quality-gates.md b/docs/specs/features/ci-quality-gates.md index 01db9aa..fdf03c4 100644 --- a/docs/specs/features/ci-quality-gates.md +++ b/docs/specs/features/ci-quality-gates.md @@ -156,19 +156,26 @@ El CI no lleva tests propios (es infraestructura declarativa). Verificación en ## Estrategia E2E -**Documentada, manual y en fase posterior — NO automática en PR.** +**Manual bajo demanda (Sprint 19B) — NO automática en PR ni push, NO required check.** - El smoke E2E (7 tests Playwright) exige stack completo: PostgreSQL migrado **y seedeado**, API real en `:4000` (el config de Playwright no la arranca), web en `:3000` y Chromium instalado. Su config actual es deliberadamente local-first (`reuseExistingServer: true`, `retries: 0`). -- Camino de promoción aprobado: workflow manual (`workflow_dispatch`) en una fase posterior → - ejecución voluntaria en PRs relevantes → si demuestra estabilidad, promoción a check - automático no bloqueante → solo entonces, candidato a required check. -- Hasta entonces, el E2E sigue ejecutándose en local según el Sprint 18 +- El Sprint 19B añade `.github/workflows/e2e.yml` (**JobIT E2E (manual)**), con trigger + único `workflow_dispatch`: monta el stack completo en el runner (service `postgres:16` + migrado y seedeado, API con `node dist/server.js`, web con `next build && next start`) + usando solo variables dummy, ejecuta el smoke y sube traces/logs como artifacts solo en + fallo. Gracias a `reuseExistingServer: true` no fue necesario tocar + `apps/web/playwright.config.ts`. Detalle: + `docs/sprints/sprint-19b-e2e-manual-workflow-plan.md`. +- Camino de promoción por estabilidad: ejecución manual voluntaria en PRs relevantes → + si demuestra estabilidad, promoción a check automático no bloqueante → solo entonces, + candidato a required check. Cada salto es decisión explícita del operador. +- El E2E sigue ejecutándose también en local según el Sprint 18 (`pnpm --filter @jobit/web test:e2e` contra el stack local seedeado). - Adaptar `apps/web/playwright.config.ts` a CI (retries condicionales, reporter con - artefactos, arranque de servidores) requerirá autorización expresa en esa fase. + artefactos) seguirá requiriendo autorización expresa si alguna fase futura lo necesita. ## Fuera de alcance diff --git a/docs/sprints/sprint-19b-e2e-manual-workflow-plan.md b/docs/sprints/sprint-19b-e2e-manual-workflow-plan.md new file mode 100644 index 0000000..0988a4a --- /dev/null +++ b/docs/sprints/sprint-19b-e2e-manual-workflow-plan.md @@ -0,0 +1,146 @@ +# Sprint 19B — E2E manual workflow (plan) + +## Objetivo + +Ejecutar el smoke E2E de Playwright (Sprint 18) en GitHub Actions **bajo demanda**, mediante +un workflow manual `workflow_dispatch`, sin convertirlo en gate automático de PR ni en +required check. El E2E pasa de ser solo local a poder lanzarse también en CI cuando el +operador lo decida. + +## Contexto (Sprints 18 / 19 / 19E) + +- **Sprint 18**: smoke E2E local (7 tests Playwright, 4 specs) del flujo candidato real + contra web `:3000` + API `:4000` + PostgreSQL seedeado. Config local-first: + `reuseExistingServer: true`, Chromium only, `workers: 1`, `retries: 0`, + `trace: retain-on-failure`. +- **Sprint 19**: workflow `JobIT CI` con quality gates API/Web en cada PR (2 runs reales en + verde en PRs #75 y #76). +- **Sprint 19E**: branch protection en `dev` con los dos checks del CI como required. +- **Plan Mode 19B**: el workflow E2E manual es viable **sin tocar ningún archivo de runtime** + (ni `playwright.config.ts`, ni package.json, ni el CI existente). + +## Decisión: workflow manual, no required check + +- El historial de estabilidad del smoke es corto (3 runs locales + 0 en CI): convertirlo en + gate bloqueante sería prematuro. Un E2E flaky como required check entrena a re-lanzar sin + mirar, que es peor que no tener E2E en CI. +- Camino de promoción por estabilidad: workflow manual → ejecución voluntaria en PRs + relevantes durante 2–3 sprints → check automático no bloqueante → solo entonces, candidato + a required check. Cada salto es decisión explícita del operador. +- Trigger único `workflow_dispatch`: sin `pull_request`, sin `push`, sin `schedule`. + +## Requisitos del stack E2E en CI + +El smoke exige el stack completo vivo, en este orden: + +1. PostgreSQL efímero (service `postgres:16` con healthcheck, base `jobit_e2e`). +2. `prisma generate` + `prisma migrate deploy` explícito (aquí no hay `globalSetup` de + Vitest que migre solo). +3. Seed (`tsx prisma/seed.ts`): autónomo e idempotente, solo tabla `Job` (14 ofertas); la + búsqueda E2E "Developer" casa con 3 ofertas activas del seed. +4. API real en `:4000` (el config de Playwright no la arranca). +5. Web real en `:3000`. +6. Chromium de Playwright instalado en el runner. + +## Variables dummy + +Sin GitHub Secrets; ninguna es un secreto real: + +| Variable | Valor | Ámbito | +|---|---|---| +| `DATABASE_URL` | `postgresql://postgres:postgres@localhost:5432/jobit_e2e?schema=public` | job | +| `JWT_ACCESS_SECRET` | `e2e-ci-access-secret` | job | +| `PORT` / `CORS_ORIGIN` | `4000` / `http://localhost:3000` | job | +| `NEXT_PUBLIC_API_BASE_URL` | `http://localhost:4000` | job (se inlinea en el build de Next) | +| `NODE_ENV` | `production` | **solo el step de arranque de la API** | + +`NODE_ENV=production` no puede definirse a nivel de job: `pnpm install` omitiría las +devDependencies (tsx, prisma CLI, playwright, typescript) y rompería todos los pasos +siguientes. `JWT_REFRESH_SECRET` no se define: el runtime de la API no lo usa (verificado en +Plan Mode; solo `JWT_ACCESS_SECRET` en `jwt.util.ts`). + +## Estrategia PostgreSQL / migrate / seed + +Service `postgres:16` (paridad con el contenedor local y con el job `api` del CI), con +healthcheck `pg_isready`. Migración con `prisma migrate deploy` explícito y seed con +`tsx prisma/seed.ts`, ambos contra la base efímera del job. Cada run parte de una base +limpia: los usuarios `e2e+…@jobit.local` que crean los journeys desaparecen con el job. + +## Estrategia de arranque de la API + +`pnpm --filter @jobit/api build` (tsc) y arranque en background con `node dist/server.js` +(`NODE_ENV=production` en ese step), log a `$RUNNER_TEMP/jobit-api.log`. Espera activa de +`GET /health` con reintentos (30 × 2 s); si no llega, el step falla mostrando el log. Los +procesos en background persisten entre steps del mismo job en GitHub Actions. + +## Estrategia de arranque de la Web + +**Decisión del Director: `next build && next start`** (build de producción). Sin compilación +on-demand — la mayor fuente de timeouts de `next dev` en runners compartidos. +`NEXT_PUBLIC_API_BASE_URL` queda inlineada en el build. Arranque en background +(`pnpm start`, puerto 3000 por defecto), log a `$RUNNER_TEMP/jobit-web.log` y espera activa +de `:3000`. Matiz aceptado: los 7 tests del Sprint 18 se validaron contra `next dev`; la +primera run manual confirmará el comportamiento contra el build de producción (ese es +precisamente el valor de que el workflow sea manual y no bloqueante). + +## Estrategia de Playwright + +`pnpm --filter @jobit/web exec playwright install chromium --with-deps` (solo Chromium) y +`pnpm --filter @jobit/web test:e2e`. Gracias a `reuseExistingServer: true`, Playwright +detecta la web viva en `:3000` y **no** lanza su propio `webServer` → cero cambios en +`apps/web/playwright.config.ts`. Se mantienen `workers: 1` y `retries: 0` (señal honesta de +estabilidad; si tras varias runs aparece ruido real de infra, se valorará un cambio puntual +de config con autorización expresa). + +## Artefactos en fallo + +Solo si el job falla se suben dos artifacts (retención 7 días): + +- `playwright-test-results`: `apps/web/test-results/` (traces `retain-on-failure`). +- `server-logs`: logs de arranque de API y web (`$RUNNER_TEMP/jobit-*.log`). + +En verde no se sube nada (el reporter `list` queda en el log del job). + +## Riesgos y mitigaciones + +| Riesgo | Mitigación | +|---|---| +| `next start` es entorno no probado por el smoke (validado contra `next dev`) | Primera run manual como validación; fallo = diagnóstico, no bloqueo | +| `retries: 0` → un fallo transitorio tumba el run | Aceptado en fase 1: workflow no bloqueante; traces + logs como diagnóstico | +| Cold start de API/web en runner | Esperas activas con reintentos (60 s máx.) en vez de sleeps fijos | +| `workflow_dispatch` no visible en la UI de Actions hasta que `e2e.yml` exista en la rama por defecto (`main`) | Lanzar por CLI: `gh workflow run e2e.yml --ref dev` (funciona con el workflow en la ref) | +| `NODE_ENV=production` a nivel de job rompería `pnpm install` | Definido solo en el step de arranque de la API (comentado en el propio workflow) | + +## Fuera de alcance + +- E2E como required check o trigger en `pull_request`/`push`/`schedule`. +- Cambios en `.github/workflows/ci.yml`, branch protection, `playwright.config.ts`, + `package.json`, lockfile, Prisma/seed, código funcional o tests E2E. +- Deploy, Docker, secrets, environments, merge queue. +- Multi-browser, mobile, regresión visual, cache de browsers Playwright (optimización + futura si el uso lo justifica). + +## Criterios de aceptación + +- [ ] `.github/workflows/e2e.yml` existe con trigger único `workflow_dispatch`. +- [ ] El job monta el stack completo (Postgres + migrate + seed + API + web) con variables + dummy y ejecuta los 7 tests del smoke. +- [ ] Sin secrets configurados; sin cambios en archivos prohibidos. +- [ ] La spec `ci-quality-gates.md` refleja el nuevo estado de la estrategia E2E. +- [ ] PR hacia `dev` pasa los dos required checks del CI. +- [ ] Primera ejecución manual tras el merge documentada (verde o diagnóstico). + +## Primera ejecución manual tras el merge + +```bash +# Lanzar el workflow sobre dev (la UI de Actions puede no listarlo hasta que +# e2e.yml llegue a main; el CLI funciona con la ref directamente): +gh workflow run e2e.yml --ref dev -R David-LS-Bilbao/JobIT-platform + +# Seguir la ejecución: +gh run list -R David-LS-Bilbao/JobIT-platform --workflow=e2e.yml +gh run watch -R David-LS-Bilbao/JobIT-platform +``` + +Si la run falla: descargar los artifacts `playwright-test-results` y `server-logs` desde la +página de la run para diagnóstico antes de tocar nada.