CI/CD con GitHub Actions para Selenium: pipeline, Allure Reports y GitHub Pages
Pipeline completo: 96 tests Selenium en headless, reporte Allure público en GitHub Pages. Los 7 errores reales que tuve que resolver.
Quería que mis tests corran solos. Sin abrir IntelliJ, sin hacer click en "Run", sin estar yo presente. Push al repo y que todo se ejecute: tests, reporte Allure, publicación automática.
Este post documenta el proceso real. Y cuando digo real, me refiero a que hice más de 10 commits arreglando cosas que no funcionaban antes de que el pipeline quedara estable.

Fue frustrante. Pero funcionó.
Qué es CI/CD (y por qué importa)
CI/CD son dos siglas que se usan juntas pero significan cosas distintas:
CI — Continuous Integration. Cada vez que alguien sube código al repositorio, un sistema automático lo compila, ejecuta tests y verifica que nada se rompió. La idea es detectar errores rápido, no dos semanas después cuando alguien hace merge de una branch gigante.
CD — Continuous Delivery / Continuous Deployment. Una vez que los tests pasan, el sistema puede preparar una nueva versión para desplegar (Delivery) o directamente desplegarla a producción sin intervención humana (Deployment).
En QA Automation, CI/CD significa: tus tests corren automáticamente cada vez que hay cambios. No dependés de acordarte de ejecutarlos, no dependés de tener la máquina libre, no dependés de que alguien haga click en "Run". El pipeline se encarga.
¿Por qué se usa tanto en la industria? Porque las empresas necesitan entregar software rápido y con confianza. Si cada release depende de que alguien corra tests manuales o ejecute suites de automation a mano, el cuello de botella es humano. CI/CD elimina eso.
Herramientas comunes: Jenkins, GitHub Actions, GitLab CI, Azure DevOps, CircleCI, Bamboo. Todas hacen lo mismo conceptualmente: escuchan un evento (push, merge, schedule) y ejecutan una secuencia de pasos definida.
Ya uso CI/CD en mi trabajo (con Jenkins + TestExecute)
Esto no es nuevo para mí. En mi trabajo actual hacemos algo parecido con otro stack.
Usamos Jenkins para orquestar la ejecución de proyectos de TestComplete (el IDE de automation que uso en el trabajo con VBScript). La mecánica: Jenkins lanza TestExecute (la versión de ejecución de TestComplete, sin interfaz de diseño) y corre los proyectos de automation uno atrás de otro.
¿Cuándo corren? De noche y los fines de semana. Son las horas donde nadie está usando las máquinas, así que aprovechamos ese tiempo muerto para que las suites de regression se ejecuten solas.
Jenkins lee una lista de proyectos, los ejecuta en secuencia, y genera reportes. Si algo falla, queda registrado. Cuando llegamos el lunes, revisamos qué pasó sin haber tocado nada.
La diferencia con lo que construí acá:
| Trabajo actual | Este lab |
|---|---|
| Jenkins | GitHub Actions |
| TestComplete / TestExecute | Selenium + Java + Maven |
| Máquinas físicas de la oficina | Máquinas virtuales de GitHub (Ubuntu) |
| Ejecución nocturna programada | Ejecución en cada push |
| Reportes internos | Allure Reports en GitHub Pages (público) |
El concepto es el mismo: automatizar la ejecución para que no dependa de una persona. Lo que cambia son las herramientas.
Quería replicar eso en mi framework personal, pero con el stack que el mercado pide: GitHub Actions en vez de Jenkins, Selenium en vez de TestComplete.
Qué quería lograr
Un workflow de GitHub Actions que haga esto en cada push a main:
- Levante un entorno Linux con Java 17 y Chrome
- Ejecute los 96 tests del framework con Maven
- Genere el reporte Allure
- Lo publique en GitHub Pages automáticamente
La URL final: https://cesarbeassuarez.github.io/qa-automation-lab/
Cualquier persona puede ver los resultados de la última ejecución. Sin pedir acceso, sin instalar nada.
El archivo: tests.yml
Todo arranca con un archivo YAML en .github/workflows/tests.yml. GitHub Actions lee ese archivo y ejecuta lo que dice cada vez que hay un push.
La estructura general:
name: QA Automation Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch: # permite ejecutar manualmente
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: write # necesario para publicar en GitHub Pages
steps:
- Checkout código
- Setup Java 17
- Setup Chrome
- Configurar headless para CI
- Ejecutar tests (mvn clean test)
- Instalar Allure CLI
- Generar reporte Allure
- Guardar artifacts
- Publicar en GitHub Pages
La estructura del workflow. Cada paso depende del anterior.
workflow_dispatch permite disparar el pipeline manualmente desde la pestaña Actions, sin necesitar un push. Útil para re-ejecutar sin cambiar código.
La pestaña Actions en GitHub
Una vez que el archivo tests.yml está en el repo, GitHub lo detecta automáticamente. No hay que "activar" nada. La pestaña Actions aparece en el repo y ahí se ve todo: cada ejecución, cada paso, los logs en tiempo real.
Cuando hacés push, aparece un circulito amarillo girando (en progreso), y después pasa a verde (todo bien) o rojo (algo falló). Podés hacer click en cualquier run y ver paso por paso qué hizo: cuánto tardó Maven, si los tests pasaron, si Allure generó el reporte, si se publicó en GitHub Pages.

También podés descargar los artifacts (archivos generados durante la ejecución). En mi caso guardo dos: allure-results (los datos crudos) y allure-report (el reporte HTML generado). Quedan disponibles por 90 días.
Un detalle importante: el paso de "Ejecutar tests" aparece en rojo si hay test failures. Pero eso no significa que el pipeline falló. Los tests detectaron errores (que es exactamente lo que deben hacer). Los pasos de reporte y publicación corren igual gracias a if: always() en el YAML.

GitHub Pages: el reporte público
Para que el reporte Allure sea accesible desde una URL, usé GitHub Pages. La configuración es mínima:
- Settings del repo → Pages
- En "Branch" seleccioné
gh-pages(la crea el workflow automáticamente la primera vez) - Carpeta:
/ (root) - Save

Con eso, GitHub publica lo que haya en la branch gh-pages en la URL: https://cesarbeassuarez.github.io/qa-automation-lab/
Cada vez que el workflow corre, el paso "Publicar reporte en GitHub Pages" actualiza esa branch con el reporte Allure nuevo. La URL no cambia, siempre muestra el último reporte.
Lo que salió mal (casi todo)
Error 1: config.properties no existía en el repo
El primer run falló porque config.properties no estaba en Git. Existía en mi máquina pero nunca lo había committeado.
sed: can't read selenium-java/selenium-java/src/test/resources/config.properties: No such file or directory
Usé git ls-files para verificar: efectivamente, no estaba trackeado. Tampoco estaban testng.xml, logback.xml ni clientes-data.xlsx.
La solución fue agregarlos:
git add selenium-java/selenium-java/src/test/resources/config.properties
git add selenium-java/selenium-java/src/test/resources/testng.xml
git add selenium-java/selenium-java/src/test/resources/logback.xml
git add selenium-java/selenium-java/src/test/resources/testdata/clientes-data.xlsx
Lección: si un archivo no está en Git, no existe para CI. Da igual que funcione en tu máquina.
Error 2: permisos para GitHub Pages
Permission to cesarbeassuarez/qa-automation-lab.git denied to github-actions[bot]
El workflow no tenía permiso para crear la branch gh-pages. Faltaba esto en el YAML:
permissions:
contents: write
Una línea. Pero sin ella, nada se publica.
Error 3: rutas equivocadas para allure-results
El reporte Allure salía vacío. 0 test cases. El problema: la ruta estaba mal.
El workflow buscaba allure-results en la raíz del proyecto, pero Maven genera los resultados dentro de target/. La ruta correcta era selenium-java/selenium-java/target/allure-results.
selenium-java/selenium-java/allure-results does not exist
Tuve que cambiar tres referencias en el YAML: el paso de generar reporte y los dos de guardar artifacts.
Error 4: el reporte mostraba datos viejos
Después de arreglar las rutas, el reporte seguía mostrando 96 tests al 100% con fecha del 26 de febrero. Datos de una ejecución local vieja.
La causa: tenía la carpeta allure-results/ committeada en el repo desde antes. Los resultados viejos se mezclaban con los nuevos.
La solución:
git rm -r --cached selenium-java/selenium-java/allure-results/
Y agregar allure-results/ al .gitignore.
Error 5: la app se mostraba en inglés
Los tests esperaban textos en español ("Tableros", "Nombres de usuario o contraseña inválidos!") pero en el entorno de GitHub Actions (Ubuntu Linux en inglés), la app de Serenity respondía en inglés ("Dashboard", "Invalid username or password!").
Resultado: 6 failures por idioma, 92 tests skipped (porque el @BeforeClass de Clientes fallaba al no encontrar "Tableros").
Primera solución: agregar --lang=es a las ChromeOptions.
options.addArguments("--lang=es");
No alcanzó. El --lang=es cambia el idioma de la interfaz de Chrome, pero no el header Accept-Language que la app web usa para decidir qué idioma mostrar.
Segunda solución: forzar el header Accept-Language directamente:
options.addArguments("--lang=es");
Map<String, Object> prefs = new HashMap<>();
prefs.put("intl.accept_languages", "es-ES,es");
options.setExperimentalOption("prefs", prefs);
Esto le dice a Chrome: "cuando hagas requests HTTP, pedí que te respondan en español". Con eso la app respondió en español y los tests de login dejaron de fallar por idioma.
Error 6: el timeout de espera no alcanzaba
Con el idioma resuelto, los tests de Clientes seguían fallando. El @BeforeClass hacía timeout esperando un menú:
Expected condition failed: waiting for element to be clickable: By.cssSelector: a[href='#nav_menu1_2_1'] (tried for 10 second(s))
En mi máquina, 10 segundos sobra. En CI headless, no alcanza.
La solución: usar sed en el workflow para aumentar el timeout solo en CI:
- name: Configurar headless para CI
run: |
sed -i 's/headless=false/headless=true/' selenium-java/selenium-java/src/test/resources/config.properties
sed -i 's/timeout.explicit=10/timeout.explicit=30/' selenium-java/selenium-java/src/test/resources/config.properties
Así el código local sigue con 10 segundos (suficiente) y CI usa 30.
Error 7: Chrome headless abría en resolución mobile
Este fue el más frustrante. El menú lateral de la app (sidebar de Serenity) seguía colapsado en headless. Los screenshots de Allure lo confirmaban: resolución de celular, menú hamburguesa en vez de sidebar expandido.
Probé --window-size=1920,1080 en las ChromeOptions. No funcionó. Probé driver.manage().window().setSize(new Dimension(1920, 1080)). Tampoco.
La solución que funcionó fue usar CDP (Chrome DevTools Protocol) para forzar el viewport:
if (headless) {
((ChromeDriver) driver).executeCdpCommand(
"Emulation.setDeviceMetricsOverride",
Map.of(
"width", 1920,
"height", 1080,
"deviceScaleFactor", 1,
"mobile", false
)
);
}
setDeviceMetricsOverride le dice a Chrome exactamente qué resolución emular, ignorando cualquier default del entorno. Es la forma más directa de forzar resolución desktop en headless.
Con esto, el screenshot del reporte Allure finalmente mostró la app en resolución completa: sidebar expandido, grilla visible, menú clickeable.
El DriverManager final
Después de todos los ajustes, el DriverManager quedó así para manejar headless:
ChromeOptions options = new ChromeOptions();
if (headless) {
options.addArguments("--headless=new");
}
options.addArguments("--disable-gpu");
options.addArguments("--no-sandbox");
options.addArguments("--window-size=1920,1080");
options.addArguments("--lang=es");
Map<String, Object> prefs = new HashMap<>();
prefs.put("intl.accept_languages", "es-ES,es");
options.setExperimentalOption("prefs", prefs);
driver = new ChromeDriver(options);
if (headless) {
((ChromeDriver) driver).executeCdpCommand(
"Emulation.setDeviceMetricsOverride",
Map.of("width", 1920, "height", 1080,
"deviceScaleFactor", 1, "mobile", false)
);
}
El bloque clave del DriverManager: headless, idioma español forzado y resolución desktop vía CDP.

Cero cambios en los tests. Todo el ajuste fue en la configuración del driver.
El workflow final
name: QA Automation Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout código
uses: actions/checkout@v4
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Chrome
uses: browser-actions/setup-chrome@v1
with:
chrome-version: 'stable'
- name: Configurar headless para CI
run: |
sed -i 's/headless=false/headless=true/' selenium-java/selenium-java/src/test/resources/config.properties
sed -i 's/timeout.explicit=10/timeout.explicit=30/' selenium-java/selenium-java/src/test/resources/config.properties
- name: Ejecutar tests
working-directory: selenium-java/selenium-java
run: mvn clean test
- name: Instalar Allure CLI
if: always()
run: |
curl -o allure.tgz -L https://github.com/allure-framework/allure2/releases/download/2.25.0/allure-2.25.0.tgz
tar -xzf allure.tgz
echo "$PWD/allure-2.25.0/bin" >> $GITHUB_PATH
- name: Generar reporte Allure
if: always()
run: allure generate selenium-java/selenium-java/target/allure-results -o allure-report --clean
- name: Guardar allure-results
if: always()
uses: actions/upload-artifact@v4
with:
name: allure-results
path: selenium-java/selenium-java/target/allure-results/
- name: Guardar allure-report
if: always()
uses: actions/upload-artifact@v4
with:
name: allure-report
path: allure-report/
- name: Publicar reporte en GitHub Pages
if: always()
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: allure-reportEl if: always() es clave: hace que los pasos de reporte se ejecuten incluso cuando los tests fallan. Sin eso, si hay failures el reporte nunca se genera.
Estado actual
El pipeline funciona. Cada push a main ejecuta los 96 tests en headless, genera el reporte Allure y lo publica en GitHub Pages.
Resultado del último run:
- 96 tests ejecutados
- 89 passed
- 6 failed (errores intencionales en Excel y textos de validación)
- 1 broken (timeout en menú, ajuste pendiente)
- Reporte Allure con Test body, @Steps y screenshots en fallos
URL pública: https://cesarbeassuarez.github.io/qa-automation-lab/


Lo que aprendí
Headless no es lo mismo que tu máquina. Cosas que funcionan en local fallan en CI: idioma, resolución, tiempos de espera, archivos que no están en Git. Cada una de esas diferencias me costó al menos un commit de arreglo.
El DriverManager absorbió toda la complejidad. Los tests no se tocaron. Eso es lo que permite una buena arquitectura: adaptar el entorno sin romper la lógica.
Fueron más de 10 commits y varias horas resolviendo problemas uno atrás de otro. Pero el resultado es un pipeline que corre solo, genera evidencia visual y la publica en una URL pública. Eso tiene valor real.
🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/qa-automation-lab
📂 selenium-java
—