Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -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
19 changes: 13 additions & 6 deletions docs/specs/features/ci-quality-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
146 changes: 146 additions & 0 deletions docs/sprints/sprint-19b-e2e-manual-workflow-plan.md
Original file line number Diff line number Diff line change
@@ -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 <run-id>
```

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.
Loading