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.

Vista de un run individual en Actions mostrando pasos del pipeline: Setup Java, Chrome, Configurar headless, Ejecutar tests (en rojo), Allure CLI, generar reporte, guardar artifacts.
Un run individual. "Ejecutar tests" está en rojo porque hay test failures intencionales, pero el resto de los pasos (reporte, publicación) corrieron sin problemas.

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.

Historial de commits en SourceTree mostrando más de 10 commits de ajustes en DriverManager y tests.yml para el pipeline CI/CD
Más de 10 commits arreglando cosas. Así se ve el proceso real de hacer funcionar un pipeline.

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:

  1. Levante un entorno Linux con Java 17 y Chrome
  2. Ejecute los 96 tests del framework con Maven
  3. Genere el reporte Allure
  4. 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.

Lista de workflow runs en la pestaña Actions mostrando 26 ejecuciones con nombres como "ajustes en drivermanager", "ajuste en driver manager", "agregado options.addArguments window-size"
26 workflow runs. Los nombres de los commits cuentan la historia: cada uno es un intento de arreglar algo que no funcionaba en headless.

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.

Vista de un run individual en Actions mostrando los pasos del pipeline
Un run individual. "Ejecutar tests" está en rojo porque hay test failures intencionales, pero el resto de los pasos (reporte, publicación) corrieron sin problemas.

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:

  1. Settings del repo → Pages
  2. En "Branch" seleccioné gh-pages (la crea el workflow automáticamente la primera vez)
  3. Carpeta: / (root)
  4. Save
Configuración de GitHub Pages en Settings del repositorio con branch gh-pages seleccionada, carpeta root, URL pública activa y HTTPS habilitado
La configuración completa de GitHub Pages. Branch gh-pages, carpeta root, HTTPS forzado. Tres clicks y el reporte es público.

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.

DriverManager.java en IntelliJ IDEA mostrando el método initDriver con configuración de ChromeOptions, Accept-Language y executeCdpCommand para forzar viewport 1920x1080
El DriverManager final en IntelliJ. Toda la complejidad de headless quedó acá, sin tocar un solo test.

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-report

El 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/

Reporte Allure en GitHub Pages mostrando 96 test cases con 92.7% de pass rate, ejecutado automáticamente por GitHub Actions
96 tests, 92.7% pass rate. Esto se genera y publica solo en cada push. URL pública: cesarbeassuarez.github.io/qa-automation-lab
Vista de Suites en Allure mostrando los tests de ClientesTests con el fallo intencional de EASTCs, incluyendo screenshot automático de la grilla en resolución desktop
El error intencional de EASTCs detectado en CI. El screenshot confirma resolución desktop: sidebar expandido, grilla completa visible.

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

Temas conectados:

Validar grilla de clientes contra Excel 

Allure Reports — reporting profesional