Playwright: CI/CD con GitHub Actions — pipeline, errores reales y reporte público
GitHub Actions + GitHub Pages para Playwright. Errores de storageState, visual tests en Linux, reporte HTML público. Contraste con Selenium CI/CD.
Contexto: por qué CI/CD cierra el ciclo
Este post documenta cómo configuré CI/CD con GitHub Actions para mi framework de Playwright + TypeScript. Es parte de mi serie de Playwright.
Tengo un framework con tests de login, validación de grilla contra Excel, API testing, network interception, visual testing. Todo corre en mi máquina. Pero eso no es suficiente.
Un framework que solo corre local es un proyecto personal. Un framework con CI/CD es un framework de producción.
CI/CD (Continuous Integration / Continuous Delivery) hace que los tests corran automáticamente cada vez que pusheo código. No tengo que acordarme de ejecutarlos. No tengo que abrir la terminal. Push y listo — GitHub levanta una máquina, instala todo, corre los tests y me dice si algo se rompió.
Ya hice esto en la serie de Selenium — CI/CD con GitHub Actions para Selenium - documentó todo el proceso con Allure Reports y GitHub Pages. Acá es el mismo concepto, pero más simple. Mucho más simple.
El punto de partida
Cuando creé el proyecto con npm init playwright@latest, Playwright preguntó:
✔ Add a GitHub Actions workflow? → true
Eso generó .github/workflows/playwright.yml automáticamente. Un workflow básico que instala dependencias, browsers, y corre los tests.

El workflow corría en cada push a main. El problema: nunca pasó.

16 runs. Casi todos rojos. El último verde fue el run #9, cuando el framework era más simple. Desde que agregué Excel, e2e, API testing, visual testing — todo rojo.
Los errores del camino
Error 1: storageState no existe en CI
El primer error era claro:
Error: ENOENT: no such file or directory, open '.auth/user.json'

api-clientes.spec.ts leía .auth/user.json con fs.readFileSync en la línea 5 — fuera de cualquier test, a nivel de módulo. Eso se ejecuta cuando Playwright carga el archivo, antes de que el proyecto setup corra auth.setup.ts y genere el archivo de autenticación.
El archivo no existía todavía. Crash.
El fix: mover el readFileSync adentro de test.beforeAll.
Antes:
import { test, expect } from '@playwright/test';
import fs from 'fs';
// Esto se ejecuta al cargar el archivo — ANTES del setup
const storageState = JSON.parse(fs.readFileSync('.auth/user.json', 'utf-8'));
const csrfToken = storageState.cookies.find(c => c.name === 'CSRF-TOKEN')?.value || '';
test.describe('API testing nativo de Playwright', () => {
// ...
Después:

En local funcionaba porque .auth/user.json ya existía de corridas anteriores. En CI cada ejecución empieza de cero — no hay archivos previos. Este es el tipo de error que solo aparece en CI.
Error 2: --ignore-pattern no existe
Quise excluir los visual tests de CI con --ignore-pattern:
error: unknown option '--ignore-pattern=**/visual-regression*'
Playwright no tiene esa flag. La alternativa: --grep-invert, que excluye tests por nombre.
run: npx playwright test --project=chromium --grep-invert="visual-regression"
Error 3: permisos para GitHub Pages
El step de deploy a GitHub Pages falló:
remote: Permission to cesarbeassuarez/playwright-typescript-framework.git denied to github-actions[bot].
fatal: unable to access '...': The requested URL returned error: 403
El workflow necesita permisos explícitos para pushear a la branch gh-pages. Fix:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: write
Decisión: qué corre en CI y qué no
No todo tiene sentido en CI.
Tests de learning/ — son exploratorios: prueban conceptos, fuerzan errores, documentan el proceso. No son de producción. Los excluí con testIgnore en la config:
testIgnore: ['**/learning/**'],
Visual tests de e2e/ — Las golden files las generé en Windows con Chromium. CI corre en Ubuntu. El rendering de fonts, anti-aliasing y sub-pixel es diferente entre sistemas operativos. Los screenshots no matchean aunque la página sea idéntica. Los excluí con --grep-invert.
¿En producción cómo se resuelve? Se generan las golden files directamente en CI con --update-snapshots, se bajan, se commitean. Así las referencia siempre son del mismo entorno donde corren los tests. O se usa Docker local con la misma imagen de CI para que el rendering sea idéntico.
Para este framework, excluirlos y documentar por qué es la decisión correcta.
El workflow final

Qué hace cada step:
- Checkout del código
- Instala Node.js (versión LTS)
npm ci— instala dependencias desdepackage-lock.json- Instala browsers de Playwright con dependencias del sistema (
--with-depsinstala libs de Linux que Chromium necesita) - Corre los tests solo en Chromium, excluyendo visual regression
- Sube el reporte como artifact (descargable desde la pestaña de Actions)
- Publica el reporte en GitHub Pages (accesible públicamente)
Reporte público en GitHub Pages
El reporte de Playwright es un solo archivo HTML autocontenido. No necesita servidor, no necesita Java, no necesita configuración. Es un .html de ~15 MB que se abre en cualquier browser.
Para publicarlo: peaceiris/actions-gh-pages toma el contenido de playwright-report/ y lo pushea a la branch gh-pages. GitHub Pages sirve esa branch como sitio estático.
Configuración en GitHub: Settings → Pages → Branch: gh-pages → / (root) → Save.
Resultado: cesarbeassuarez.github.io/playwright-typescript-framework

Cada test muestra su estado, duración, retries. Los que fallaron tienen links a View Trace — el trace completo de la ejecución con screenshots, network, console, todo.

CI detectó un cambio real
Los 5 tests que fallan no son errores del pipeline. Son cambios reales en demo.serenity.is.
Alguien agregó un cliente nuevo: C001 — ACME. La grilla pasó de 91 a 92 clientes.

Eso rompió:
- El test de API que espera 91 entidades → recibe 92
- La validación contra Excel que tiene 91 registros → la grilla tiene 92
- El ordenamiento que espera ALFKI primero → C001 viene antes alfabéticamente
- El test de filtros que espera volver a 91 → vuelve a 92
Esto es exactamente para lo que sirve CI/CD: detectar que algo cambió. No es un bug del framework. Es un cambio en el entorno de pruebas. En un proyecto real, este fallo dispara una pregunta: "¿se esperaba este cambio? ¿hay que actualizar los tests?" Eso es proceso de QA.
No ajusté los tests para forzar el verde. El rojo es honesto.
CI/CD en Playwright vs Selenium
En la serie de Selenium, el post de CI/CD fue el Post 12. Tardé 15+ commits en hacer funcionar el pipeline. Tuve que lidiar con headless mode, WebDriverManager, rutas de ChromeDriver, maven-surefire-plugin, Allure Reports con Java, deploy con acciones custom.
En Playwright:
| Concepto | Selenium + Java | Playwright + TypeScript |
|---|---|---|
| Workflow inicial | Creado desde cero | Generado por npm init playwright |
| Headless | Configurar ChromeOptions manualmente | Headless por defecto en CI |
| Drivers | WebDriverManager, problemas de versión | Incluidos, npx playwright install |
| Reporter | Allure — instalar Java, generar reporte, deploy custom | HTML reporter nativo — un archivo |
| Deploy del reporte | Allure + acciones de terceros + Java runtime | peaceiris/actions-gh-pages + un HTML |
| Config de CI | Variables de entorno para headless, chromedriver | process.env.CI ya configurado |
| Retries | Configurar en TestNG/Maven | retries: process.env.CI ? 2 : 0 en config |
| Workers | Configurar parallelism manualmente | workers: process.env.CI ? 1 : undefined |
El playwright.config.ts ya tenía todo preparado para CI desde el principio:
forbidOnly: !!process.env.CI, // no dejar .only en CI
retries: process.env.CI ? 2 : 0, // reintentar en CI
workers: process.env.CI ? 1 : undefined // secuencial en CI, paralelo en local
Esas líneas las puso Playwright cuando generó el proyecto. En Selenium tuve que descubrir cada una por mi cuenta.
Allure vs Playwright HTML Reporter
En Selenium uso Allure Reports — más configurable, con @Step, categorías, historial de ejecuciones. Requiere Java instalado y una acción custom para generarlo.
En Playwright el HTML reporter es nativo. No necesita configuración, no necesita Java, se genera automáticamente. Es un solo archivo HTML con todo: resultados, traces, screenshots, diffs de visual testing.
Para un framework de Playwright, Allure es innecesario. El reporter nativo cubre todo lo que necesito.
La config que hace funcionar todo
export default defineConfig({
testDir: './tests',
testIgnore: ['**/learning/**'], // solo e2e en CI
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
trace: 'on-first-retry',
locale: 'es-AR',
baseURL: 'https://demo.serenity.is',
},
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' },
dependencies: ['setup'],
},
// firefox, webkit...
],
});
testIgnore separa learning de producción. forbidOnly previene que un .only olvidado llegue a CI. retries reintenta en CI para manejar flakiness. trace: 'on-first-retry' captura traces solo cuando un test falla la primera vez — información de debug sin overhead innecesario.
Los projects con dependencies: ['setup'] aseguran que auth.setup.ts corra primero y genere .auth/user.json antes de cualquier test.
Estado actual
Pipeline de CI/CD corriendo en cada push a main. 17 tests ejecutándose en Chromium, reporte HTML publicado automáticamente en GitHub Pages. 12 tests verdes, 5 rojos por cambio de datos en el entorno de prueba.
El framework tiene el ciclo completo: código → push → tests automáticos → reporte público.
Próximo post: Selenium vs Playwright — el comparativo final con evidencia real de ambas series.
—
🔗 Reporte público: cesarbeassuarez.github.io/playwright-typescript-framework
🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/playwright-typescript-framework