AGENTIC QA · MINI-CURSO · CI PARA TESTING

Parte I · Fundamentos

ci-pipelines

Cómo GitHub Actions ejecuta nuestras suites sin que nadie apriete un botón: triggers, jobs, steps, runners, secrets, artifacts. Todo explicado sobre los workflows reales de este repo, línea por línea.

workflows reales de .github/workflows/: build · smoke · sanity · regression 5 capítulos · 10 quizzes · 8 simuladores De cero a escribir tu propio .yml

→ avanzar · O overview · N notas · F pantalla completa · clic en los bloques interactivos

Parte I del mini-curso de CI. Audiencia: testers que nunca abrieron un workflow. Todo YAML citado viene de .github/workflows/ en este repo (extractos: si el archivo y la slide difieren, manda el archivo). La Parte II cubre la estrategia de suites, el write-back al TMS y la integración con el pipeline de desarrollo.

mapa: haz clic en un capítulo para saltar

Cinco capítulos, de la idea al archivo

Primero el vocabulario, luego cada pieza de un workflow con nuestros archivos como ejemplo. Cada capítulo cierra con un quiz que suma al marcador de arriba a la derecha.

CAP 01

Qué es CI y por qué te importa

El pipeline como red de seguridad. Workflow, job, step, runner, action, artifact.

→ slide 3
CAP 02

Triggers: quién despierta al workflow

pull_request, schedule, workflow_dispatch con inputs, cron, contextos y expresiones.

→ slide 8
CAP 03

Jobs, steps y runners

runs-on, needs, uses vs run, if: con always(), outputs y job summary.

→ slide 14
CAP 04

Secrets, variables, concurrencia y permisos

Tres niveles de configuración, environments, concurrency, permissions.

→ slide 20
CAP 05

Artifacts y el esqueleto

Subir, bajar, retener evidencia. La plantilla que usamos en las tres suites.

→ slide 25
Parte IIestrategia smoke/sanity/regression · regression.yml a fondo · resultados y write-back al TMS · integración con el pipeline de desarrollo · escribe tu propio workflow → ci-pipelines-architecture.es.html

Lo que este curso NO repite: cómo el agente analiza un run y emite GO / CAUTION / NO-GO. Eso vive en regression-testing/how-it-works.es.html.

Índice clicable. Parte I = mecánica de GitHub Actions. Parte II = arquitectura de suites + write-back + futuro. El deck how-it-works de regression-testing ya cubre el análisis del run; no lo duplicamos, lo enlazamos.

Capítulo 01

Qué es CI y por qué un tester lo necesita

Integración Continua: cada cambio dispara una verificación automática en una máquina limpia. Para QA significa que la suite corre sola, siempre igual, y deja evidencia que nadie tiene que pedir.

Al terminar sabrás qué es un workflow, un job, un step y un runnerY leerás build.yml sin miedo
Capítulo 1: vocabulario y anatomía. Usamos build.yml porque es el más corto y no corre tests: solo valida el framework.

el dolor y el patrón

Sin CI, la suite corre cuando alguien se acuerda

Sin CI

«Corré la regresión antes del release». Corre en TU laptop, con TU .env, con TU versión de Chromium. El reporte queda en tu disco. Si fallás vos, no hay evidencia. Si la app se rompe a las 3 AM, nadie se entera hasta las 9.

Con CI

Un archivo .yml describe QUÉ correr y CUÁNDO. GitHub levanta una máquina limpia, instala todo, corre la suite, guarda screenshots, traces y el reporte Allure, y escribe el resultado en el TMS. Cada noche. Igual. Sin que nadie toque nada.

el pipeline de testing de este repo, de punta a punta

build.yml
El framework compila y su documentación sigue consistente? Types, lint, gates de documentación, playwright test --list.
en cada Pull Request
→
unit-tests.yml
Las herramientas del propio repo (cli/, scripts/) siguen andando? bun test + los gates que build.yml no corre.
en cada push
→
smoke.yml
La app está viva? Solo tests @critical.
02:00 UTC + manual
→
sanity.yml
Funciona ESTE cambio? Un archivo o un --grep.
manual, con inputs
→
regression.yml
Se rompió algo más? Integration + E2E completos.
00:00 UTC + manual
→
Allure + TMS
Reporte publicado en GitHub Pages (o en el portal privado si está configurado); resultados al TMS (en Xray, una entrada por ATC al RTR, sin crear Tests).
jobs finales

Cada caja es un archivo en .github/workflows/. Los cinco comparten el mismo esqueleto; por eso, cuando entiendas uno, entiendes todos. La estrategia (por qué varios y no uno) es el Capítulo 6, en la Parte II.

Dolor → patrón. CI = archivo declarativo + máquina limpia + evidencia persistente. Mostramos los cinco workflows como cadena; la estrategia se justifica en Parte II.

seis palabras que vas a leer mil veces

Workflow → jobs → steps, ejecutados por un runner

TérminoQué esEn este repo
WorkflowUn archivo .yml en .github/workflows/. Define cuándo corre y qué hace.smoke.yml, regression.yml…
Event / triggerLo que despierta al workflow: un PR, un cron, un clic manual, un evento externo.on: schedule / workflow_dispatch
JobUn conjunto de steps que corre en UNA máquina. Los jobs corren en paralelo salvo que declares needs.IntegrationTests, E2ETests
StepUna acción dentro del job: un comando (run) o una acción reutilizable (uses).run: bun run test:smoke
RunnerLa máquina virtual que ejecuta el job. GitHub la crea limpia y la destruye al terminar.runs-on: ubuntu-latest
ActionUn step empaquetado por alguien (GitHub, Oven, Playwright…) que enchufás con uses:.actions/checkout@v4
ArtifactArchivos que el job guarda al terminar para que los bajes o los use otro job.smoke-allure-results
la jerarquía en 12 líneasesquema
name: Smoke Tests           # el workflow
on:                         # los triggers
  schedule: [...]
  workflow_dispatch: [...]
jobs:
  SmokeTest:                # job 1
    runs-on: ubuntu-latest  # el runner
    steps:
      - uses: actions/checkout@v4   # step (action)
      - run: bun run test:smoke      # step (comando)
  allure-report:            # job 2
    needs: SmokeTest        # espera al job 1

La imagen mental

Un workflow es una receta. Cada job es un cocinero en su propia cocina vacía (el runner). Los steps son los pasos de la receta. Los artifacts son los platos que dejás en la ventanilla para el siguiente cocinero: si no los pasás, el otro no los ve.

Siete términos. Insistir: cada job = máquina nueva y vacía. Por eso cada job repite checkout + setup, y por eso los artifacts existen.

explorador: haz clic en cada bloque del YAML

Anatomía de un workflow: build.yml, pieza por pieza

.github/workflows/build.yml · extractoreal
name: TestBuild Checks
run-name: ${{ github.actor }} triggered TestBuild Checks on ${{ github.ref_name }}
on:
  pull_request:
    branches:
      - main
concurrency:
  group: build-${{ github.ref }}
  cancel-in-progress: true
env:
  CI: true
  TEST_ENV: 'staging'
  # Test-user pair for the selected TEST_ENV. OPTIONAL here: nothing in this
  # job logs in … A fork PR has no secrets and still passes.
  STAGING_USER_EMAIL: ${{ secrets.STAGING_USER_EMAIL }}
  STAGING_USER_PASSWORD: ${{ secrets.STAGING_USER_PASSWORD }}
jobs:
  TestBuild:
    name: Framework Validation
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          # Full history: the documentation-contract step diffs the PR range.
      - name: Install Dependencies
        run: bun install
      - name: ESLint Check
        run: bun run lint:check
      - name: Docs Gates
        run: bun run docs:check · bun run skills:check
      - name: Playwright Compile Check
        run: bunx playwright test --list
Haz clic en un bloque del YAML →
Explorador clicable. build.yml es el mejor primer archivo: no corre tests, valida el framework. Recorrer name → on → concurrency → env → job → uses → run.

quiz 01: vocabulario y anatomía

Se entendió la jerarquía?

Q1El job E2ETests instala Bun y los browsers aunque IntegrationTests ya lo hizo un minuto antes. Por qué?
APor descuido: se podría borrar y ahorrar tiempo
BCada job corre en un runner nuevo y vacío; nada del job anterior sobrevive
CPorque needs borra el estado del job anterior
Runner nuevo por job. Un job es una VM limpia. Lo único que cruza de un job a otro son los artifacts (y los outputs). Por eso las suites repiten los 5 steps de setup en cada job, y por eso allure-report tiene que descargar los resultados con download-artifact.
Q2Un compañero propone escribir la contraseña de staging directamente en env: del YAML «porque total el repo es privado». Qué le decís?
AVale, si el repo es privado no hay riesgo
BMejor ponerla en un comentario para que quede documentada
CNo: va en GitHub Secrets y se referencia con ${{ secrets.X }}; el YAML queda en el historial de git para siempre
Secrets, siempre. El YAML viaja en cada clone, fork y PR; un secret se inyecta en runtime, se enmascara en los logs y se rota sin tocar el código. Además los secrets no llegan a PRs desde forks, que es exactamente la protección que querés.
Q1: runner limpio por job (justifica artifacts y setup repetido). Q2: secrets nunca en YAML.

Capítulo 02

Triggers: quién despierta al workflow

Un workflow no corre solo. Alguien o algo lo dispara: un PR, un reloj, un clic con parámetros, un evento de otro repo. La elección del trigger ES la estrategia.

on: con varios eventosworkflow_dispatch con inputs tipadoscron y sus trampascontextos y expresiones ${{ }}
Capítulo 2. Los triggers que usan los workflows del repo; los triggers externos (repository_dispatch, workflow_call, deployment_status) se dejan para la Parte II, Cap 9.

simulador: elige un evento y mira qué workflows despiertan

Un evento, cero o varios workflows

pull_request → mainalguien abre o actualiza un PR
push a mainse mergea un PR que toca packages/decks/
reloj · 00:00 UTCcron '0 0 * * *'
reloj · 02:00 UTCcron '0 2 * * *'
botón «Run workflow»workflow_dispatch con inputs
1° del mes · 04:00 UTCcron '0 4 1 * *'
build.yml
on: pull_request: branches: [main]
Valida types + lint + gates de documentación + compile de los specs en cada PR. No corre tests.
unit-tests.yml
on: push (cualquier rama, sin tags) · pull_request de forks · workflow_dispatch
Corre bun test (los tests de cli/ y scripts/, no las suites de Playwright) y los gates de repo que build.yml no corre. Inerte fuera del repo del boilerplate.
smoke.yml
schedule 02:00 · workflow_dispatch
@critical contra staging. Con el botón podés elegir env, Allure y execution_key.
sanity.yml
solo workflow_dispatch
Nunca automático: siempre le decís QUÉ correr (grep, archivo, tipo).
regression.yml
schedule 00:00 · workflow_dispatch
Integration + E2E completos. El único con input video_record.
pages.yml / squash
push a main con paths · dispatch · squash mensual
pages.yml publica la portada (packages/pages-home), /kata/, /decks/ y el sitio estático de docs/ en /docs/, solo si el push tocó sus fuentes; bun run docs sigue sirviendo docs/ en local. pages-squash.yml aplana gh-pages el 1° de cada mes.
Simulador: 6 eventos vs los archivos de .github/workflows/. Idea clave: un evento puede disparar 0, 1 o varios workflows; cada archivo declara sus propios triggers en on:. PR no corre suites (decisión explícita).

formulario: cambia los valores y mira el comando que genera

workflow_dispatch: el botón con parámetros

.github/workflows/smoke.yml · extracto: inputs de workflow_dispatchreal
  workflow_dispatch:
    inputs:
      environment:
        description: Select the Test Environment
        required: true
        default: 'staging'
        type: choice
        options:
          - local
          - staging
      generate_allure:
        description: Generate Allure Report
        required: true
        default: true
        type: boolean
      execution_key:
        description: 'Key of the Test Execution this run imports into (…)'
        required: false
        default: ''
        type: string
Actions → Smoke Tests → Run workflow ▾  (así lo dibuja GitHub a partir de los inputs)
Run workflow

Tipos válidos: choice, boolean, string, number, environment. Máximo 25 inputs. Se leen con github.event.inputs.X (todo llega como string) o con inputs.X (conserva los booleanos). Por eso el repo compara generate_allure != 'false' entre comillas (el if del job allure-report en smoke.yml).

El patrón ${{ github.event.inputs.environment || 'staging' }} es el fallback para el cron: un run programado no tiene inputs, la expresión devuelve vacío y el || rescata el default. Sin ese ||, la regresión nocturna correría sin entorno.

Formulario vivo: los inputs generan la UI de GitHub y el comando gh equivalente. Dos gotchas: inputs llegan como string en github.event.inputs; el || 'staging' existe por el cron.

cron: haz clic en cada campo

schedule: el reloj que nadie olvida

extracto del bloque schedule: de .github/workflows/regression.yml · .github/workflows/smoke.yml · .github/workflows/pages-squash.ymlreal
schedule:
  - cron: '0 0 * * *' # Runs daily at midnight UTC   (regression)
  - cron: '0 2 * * *' # Runs daily at 2:00 AM UTC    (smoke)
  - cron: '0 4 1 * *' # 04:00 UTC, 1st of every month (squash)
0
minuto
2
hora
*
día mes
*
mes
*
día semana

Las cuatro trampas del cron (todas documentadas por GitHub)

  • Solo corre en la rama por defecto. Un cron en tu feature branch no hace nada hasta que se mergea a main.
  • No tiene inputs. Por eso cada valor que depende de un input lleva su || 'default'. Y por eso execution_key cae al secret STP_EXECUTION_KEY en la regresión nocturna.
  • Se retrasa bajo carga. Puede arrancar minutos tarde; en casos extremos, saltarse. No es un reloj suizo: es una cola.
  • Se apaga solo. En repos públicos, tras 60 días sin actividad GitHub desactiva los workflows programados. Un repo dormido deja de vigilar.

Por qué dos crons y no uno

Smoke y regression podrían ser un solo archivo con dos crons. Se separan porque contestan preguntas distintas y tienen SLOs distintos: nunca mezcles sus pass-rates (regla R6 de /regression-testing). Estrategia completa en la Parte II.

Cron: 5 campos clicables. Trampas: default branch only, sin inputs (fallbacks), retrasos, auto-desactivación 60 días. Todo de docs.github.com.

el lenguaje dentro de ${{ }}

Contextos: de dónde salen los valores

ContextoQué contieneEjemplo real en el repo
githubMetadatos del run: quién, qué rama, qué evento, número de run.${{ github.actor }} · github.ref_name · github.run_number (smoke.yml)
github.eventEl payload del evento que disparó el run. Con dispatch, trae inputs.${{ github.event.inputs.environment || 'staging' }} (bloque env)
inputsLos inputs tipados (booleanos siguen siendo booleanos). Sirve para dispatch y para workflow_call.${{ inputs.environment }} (no usado en este repo)
secretsValores cifrados, enmascarados en logs. Solo en env, with e if de step.${{ secrets.XRAY_CLIENT_ID }} (regression.yml, bloque env)
varsConfiguración no secreta (repo / env / org). Legible en if de job.${{ vars.TMS_PROVIDER || 'xray' }} (regression.yml: bloque env y el if del job XrayImport)
envLas variables de entorno definidas con env:. NO disponible en if de job.${{ env.TEST_ENV }} (smoke.yml, step Run Smoke Tests)
stepsOutputs de steps anteriores (mismo job) por su id.steps.check_allure.outputs.has_results (smoke.yml, job allure-report)
needsResultado y outputs de los jobs de los que dependés.needs.SmokeTest.result (en el job Slack comentado)
operadores que vas a usarejemplos
# fallback: si A está vacío, usa B
${{ github.event.inputs.environment || 'staging' }}

# comparación (siempre string vs string)
${{ github.event.inputs.generate_allure != 'false' }}

# y / o / negación
${{ always() && env.AUTO_SYNC == 'true' }}
${{ !cancelled() }}

# dentro de `if:` las llaves son opcionales
if: always() && env.AUTO_SYNC == 'true'

La regla que rompe más workflows

Un if: a nivel de job puede leer github, vars, inputs y needs, pero no env ni secrets. Por eso TMS_PROVIDER es una variable y no un secret, y por eso el if del job XrayImport de regression.yml repite vars.TMS_PROVIDER en vez de leer env.TMS_PROVIDER. El comentario justo encima lo dice textual.

Tabla de contextos con ejemplos del repo. La regla dorada: job-level if no lee env ni secrets → TMS_PROVIDER es var. Sacado del propio comentario del workflow.

quiz 02: triggers, inputs y contextos

Quién despertó al workflow?

Q3Agregás schedule: cron '0 6 * * *' a sanity.yml en tu rama feat/sanity-cron y abrís el PR. Pasan tres días. Cuántas veces corrió el cron?
ATres: una por día a las 06:00
BCero: los crons solo corren sobre la rama por defecto; hasta el merge a main no existe
CTres, pero fallaron por falta de inputs
Cero. schedule se evalúa sobre el último commit de la rama por defecto. Tu rama puede tener el cron perfecto y GitHub no lo mira. Después del merge, además, recordá que un cron no trae inputs: cada valor necesita su || 'default'.
Q4Querés que un job entero se salte cuando el proyecto no usa Xray. Escribís if: env.TMS_PROVIDER == 'xray' a nivel de job. Qué pasa?
AFunciona: env está disponible en todo el workflow
BNo funciona: el if de job no puede leer env; hay que usar vars.TMS_PROVIDER
CFunciona solo si TMS_PROVIDER es un secret
vars, no env. El contexto env no existe cuando GitHub decide si arranca un job. Por eso el job XrayImport de regression.yml escribe if: always() && (vars.TMS_PROVIDER || 'xray') == 'xray' y guarda el proveedor como variable de repositorio, que sí se puede leer ahí. Un secret tampoco serviría: secrets también está vedado en el if de job.
Q3: cron solo en default branch. Q4: job-level if no lee env/secrets → vars.

Capítulo 03

Jobs, steps y runners: la ejecución

Cuándo dos jobs corren en paralelo y cuándo uno espera al otro. Qué es una action y qué es un comando. Cómo un step decide si corre aunque el anterior haya fallado. Y cómo un job le habla al siguiente.

needs y el grafo de jobsuses vs runif: always() / failure()$GITHUB_OUTPUT y job summary
Capítulo 3: ejecución. regression.yml como ejemplo de grafo (4 jobs), smoke.yml para steps.

simulador: pulsa ▶ y mira el orden real de ejecución

needs: quién espera a quién en regression.yml

needs needs: [int, e2e]
IntegrationTests
API sola · timeout 30 min
queued
E2ETests
needs: IntegrationTests · timeout 60
queued
allure-report
needs: [IntegrationTests, E2ETests]
if: always() && …
queued
XrayImport
needs: [IntegrationTests, E2ETests]
if: always() && vars…
queued
  • Sin needs, todo corre en paralelo. GitHub lanza cada job en su runner apenas puede. El orden lo das vos con dependencias, no con la posición en el archivo.
  • needs encadena. E2ETests espera a que IntegrationTests termine bien. Si la API está rota, no tiene sentido pagar 60 minutos de navegador: el job queda skipped.
  • Un needs fallido salta la cadena… salvo que el job diga if: always(). Los dos jobs finales lo dicen: el reporte y la importación al TMS deben existir sobre todo cuando algo falló.
  • Lista = espera a todos. needs: [IntegrationTests, E2ETests] arranca cuando ambos terminaron (o se saltaron). needs.X.result te dice cómo: success / failure / cancelled / skipped.

Por qué Integration primero

Los tests de API son más rápidos y más baratos que los de UI, y si el backend está caído la UI va a fallar por la misma causa. Ponerlos primero convierte una caída de backend en 6 minutos rojos en vez de 66. Es un fail fast hecho con una sola línea: needs.

Grafo animado. Dos escenarios: noche normal y API caída. Puntos: paralelo por defecto, needs encadena, always() en los finales, lista de needs, fail fast.

los cinco steps que toda suite repite

Steps: uses enchufa una action, run ejecuta un comando

.github/workflows/smoke.yml · extracto: steps de setup (idéntico en sanity y regression)real
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v4        # 1 · trae el código
        with:
          fetch-depth: 1
      - name: Setup Bun
        uses: oven-sh/setup-bun@v2        # 2 · el runtime
        with:
          bun-version: latest
      - name: Install Dependencies
        run: bun install                  # 3 · node_modules
      - name: Validate Test Environment Variables
        run: bun run test:env:check       # 4 · faltan secrets?
      - name: Install Playwright Browsers
        run: bun run pw:install           # 5 · chromium + deps
      - name: Run Smoke Tests (@critical) on ${{ env.TEST_ENV }}
        run: bun run test:smoke           # la suite

uses: una action

Código empaquetado por otro (owner/repo@version). Recibe parámetros en with:. actions/* son de GitHub; oven-sh/setup-bun es de los autores de Bun. Sin checkout, el runner no tiene ni el package.json.

run: un comando

Shell del runner (bash en Ubuntu). Multilínea con run: |. Regla del repo: llama a los scripts de package.json, nunca reinventa el comando. Así CI y tu terminal ejecutan lo mismo.

  • El orden importa. Los steps de un job corren en secuencia, en la misma máquina. Instalar browsers antes de bun install fallaría: el script pw:install vive en node_modules.
  • test:env:check es el step 4 a propósito. Falla en 2 segundos si falta un secret, antes de gastar 3 minutos instalando Chromium. Fallar temprano y con un mensaje claro es diseño, no suerte.
  • @v4, @v2: versiones mayores. Se actualizan solas dentro del mayor. Para acciones de terceros críticas, la doc de GitHub recomienda fijar el SHA completo del commit (40 caracteres) con un comentario # v4.2.2: una etiqueta se puede mover; un SHA no.
  • name es para humanos. Aparece en el log del run. «Install Playwright Browsers» se lee; «step 5» no.
Los 5 steps de setup. uses vs run. Orden secuencial. env:check temprano = fail fast. SHA pinning como recomendación de docs.

simulador: cambia cómo terminó la suite y mira qué steps corren

if: steps que corren aunque todo haya fallado

Run Smoke Tests(sin if)
Sync Results to TMSif: always() && env.AUTO_SYNC == 'true' && (…)
Upload Allure Resultsif: always()
Upload Test Resultsif: always()
Upload Failure Evidenceif: failure()
Cleanup Workspaceif: always()
job allure-reportif: always() && github.event.inputs.generate_allure != 'false'
FunciónDevuelve true cuando…Uso típico
success()ningún step/job anterior falló (es el default implícito)casi nunca se escribe
always()siempre, incluso canceladouploads, cleanup, reporte
failure()algún step anterior fallóevidencia de fallo, alertas
cancelled()el run fue canceladolimpieza específica
Simulador de if. Mensaje central: always() en uploads es lo que preserva la evidencia cuando falla. failure() para evidencia extra. cancelled ≠ failure.

cómo un step le habla al siguiente, y al humano

$GITHUB_OUTPUT, $GITHUB_STEP_SUMMARY y ::warning

.github/workflows/smoke.yml · extracto: job allure-reportreal
      - name: Check Allure Results
        id: check_allure                    # ← el id hace legibles sus outputs
        shell: bash
        run: |
          if [ -d "smoke-artifacts/allure-results" ] && [ -n "$(ls -A … )" ]; then
            echo "Allure results found."
            echo "has_results=true" >> $GITHUB_OUTPUT   # ← escribe un output
          else
            echo "No Allure results found. Skipping report generation."
            echo "has_results=false" >> $GITHUB_OUTPUT
          fi
      - name: Setup Bun
        if: steps.check_allure.outputs.has_results == 'true'   # ← lo lee
        uses: oven-sh/setup-bun@v2
…
            echo "## Allure Report" >> $GITHUB_STEP_SUMMARY
            echo "View the report: https://${{ github.repository_owner }}.github.io/…/${{ env.TEST_ENV }}/smoke/" >> $GITHUB_STEP_SUMMARY

$GITHUB_OUTPUT

Un archivo. Escribís clave=valor y GitHub lo expone como steps.<id>.outputs.clave para los steps siguientes. Para pasarlo a OTRO job, lo mapeás en jobs.X.outputs y lo leés con needs.X.outputs.clave.

$GITHUB_STEP_SUMMARY

Markdown que aparece en la página del run, arriba de los logs. Aquí se escribe el link al reporte Allure: el PM no abre logs, abre el summary. regression.yml también escribe cuántos JUnit importó a Xray.

::notice / ::warning

Anotaciones visibles en la UI sin fallar el step. el step de gate del job XrayImport de regression.yml avisa «Xray import skipped» con ::notice y con ::warning cuando falta la execution_key. El step sale con 0: informar no es fallar.

El patrón «detectar → decidir → saltar»

Un step barato comprueba si hay resultados y publica un output. Los steps siguientes llevan if: steps.check_allure.outputs.has_results == 'true'. Si la suite no dejó nada (un run cancelado a los 10 segundos), el job no instala Bun ni genera un reporte vacío. Diseño defensivo con un booleano de texto.

$GITHUB_ENV es el primo: echo "VAR=valor" >> $GITHUB_ENV crea una variable de entorno para los steps siguientes del mismo job.

Tres canales de comunicación: outputs (step→step, job→job), summary (→humano), anotaciones (→UI sin fallar). Patrón check_allure real.

quiz 03: ejecución

Corre o se salta?

Q5Borrás el if: always() del step «Upload Allure Results» para «simplificar». La suite falla esa noche. Qué encontrás por la mañana?
AEl reporte Allure con los tests rojos, como siempre
BNingún artifact ni reporte: al fallar un step, los siguientes se saltan por defecto
CSolo el artifact de evidencia de fallo
Nada. El default implícito de cada step es success(): si algo falló antes, no corre. Perdés los resultados precisamente la noche que los necesitabas. always() en los uploads no es decoración: es la diferencia entre «algo falló» y «esto falló, aquí está el trace».
Q6El job allure-report quiere saber si el job SmokeTest dejó resultados. Cómo puede enterarse?
ALeyendo allure-results/ del disco: los jobs comparten el filesystem
BCon steps.check_allure: los outputs de steps son globales
CBajando el artifact con download-artifact, o leyendo needs.SmokeTest.outputs.* si el job los declaró
Artifacts u outputs de job. Runners distintos = discos distintos, así que A es imposible. steps.* solo existe dentro del mismo job, así que B tampoco. smoke.yml usa la vía del artifact (step Download Smoke Test Artifacts) y luego comprueba localmente si llegó algo (step Check Allure Results).
Q5: sin always() perdés evidencia. Q6: comunicación entre jobs = artifacts u outputs, nunca disco compartido ni steps.*.

Capítulo 04

Secrets, variables, concurrencia y permisos

Cómo llegan las credenciales al runner sin escribirlas nunca. Qué pasa cuando dos runs se pisan. Y qué puede hacer el token que GitHub le da a cada job.

env vs vars vs secretsenvironment: y sus reglasconcurrency: cancelar o encolarpermissions mínimos
Capítulo 4: configuración y seguridad operativa.

clasificador: dónde va cada valor? un clic por fila

Tres niveles de configuración, tres razones distintas

NivelPara quéPropiedades
env: literalConstantes del workflowVisible en el YAML y en logs. CI: true.
${{ vars.X }}Configuración que cambia por repo/entorno pero no es secretaSe define en Settings → Variables. Legible en if de job. Vacío si no existe.
${{ secrets.X }}CredencialesCifrado, enmascarado en logs, no llega a PRs de forks, no legible en if de job.

environment: name: = un cajón de secrets con reglas

Cada job de suite declara environment: name: ${{ github.event.inputs.environment || 'staging' }}. Eso desbloquea los secrets/vars definidos en ESE environment de GitHub y aplica sus protection rules (revisores obligatorios, espera). Así production puede exigir un aprobador aunque el YAML sea el mismo.

No existe un secret ATLASSIAN_URL a propósito: el host se lee de .agents/project.yaml. Una segunda copia de un valor es la copia que envejece.

STAGING_USER_PASSWORD
TMS_PROVIDER (xray | jira)
CI: true
XRAY_CLIENT_SECRET
AUTO_SYNC ('true' | 'false')
TEST_ENV: ${{ github.event.inputs.environment || 'staging' }}
Clasificador con 6 valores reales. La trampa AUTO_SYNC es deliberada: enseña a distinguir naturaleza vs implementación actual. environment: name: = cajón de secrets + protection rules.

simulador: dos runs del mismo grupo se solapan

concurrency: cancelar o encolar, según qué publica

extracto del bloque concurrency: de .github/workflows/build.yml vs .github/workflows/regression.ymlreal
# build.yml: valida, no publica
concurrency:
  # One build per PR branch: a force-push supersedes the run already in flight.
  # Safe to cancel: this job only validates, it publishes nothing.
  group: build-${{ github.ref }}
  cancel-in-progress: true

# regression.yml: publica Allure a gh-pages
concurrency:
  # Suite runs publish Allure to gh-pages. Two overlapping runs race on that
  # deploy and one silently overwrites the other's report. Queue instead of
  # cancelling: a half-finished suite that still uploaded results would leave
  # a partial report standing as if it were the full one.
  group: regression-${{ github.ref }}
  cancel-in-progress: false
runner
run #1 · in_progress
run #2 · nuevo
  • group define quién compite. Puede usar expresiones (github, inputs, vars). build-${{ github.ref }}: un grupo por rama. Los nombres no distinguen mayúsculas.
  • Por defecto solo hay UN pending. Si llegan #2 y #3 mientras #1 corre, #2 se cancela y queda #3. Con queue: max podés encolar hasta 100, pero no combinado con cancel-in-progress: true.
  • FIFO. Los pending arrancan en el orden en que empezaron a esperar.
  • Nivel workflow o nivel job. Todos los workflows lo declaran a nivel workflow; pages.yml usa un grupo fijo (pages-docs-hub) porque siempre publica al mismo sitio.

La pregunta que decide

«Este workflow deja algo publicado o compartido?» Si no (validar, lint, compilar): cancelar, es más barato. Si sí (reporte, deploy, rama gh-pages): encolar, porque cancelar a medias deja un estado que parece completo y no lo es. Todos nuestros workflows publican salvo build.yml y unit-tests.yml, que cancelan (unit-tests.yml nunca en main: ahí cada push es trabajo que ya aterrizó y merece su propio run, así que cada commit de main lleva su propio grupo; cancel-in-progress: false no alcanza, porque el grupo guarda un solo run pendiente y lo cancela cuando llega otro).

Concurrency: cancel vs queue, con los comentarios reales del repo como justificación. Regla: publica → encola; valida → cancela.

el token que GitHub le presta a cada job

permissions: qué puede hacer GITHUB_TOKEN

.github/workflows/pages.yml · extractoreal
permissions:
  contents: write        # puede hacer push (a gh-pages)
…
# El deploy no usa una acción de terceros: el workflow arma _site, clona
# gh-pages, sincroniza solo las superficies del hub (rsync --delete) y
# hace git push con el mismo GITHUB_TOKEN.
forma recomendada para un workflow nuevoejemplo
permissions: {}            # nada por defecto…
jobs:
  publish:
    permissions:
      contents: write    # …y solo este job puede escribir
  notify:
    permissions:
      statuses: write    # p.ej. devolver un commit status a otro repo
  • GITHUB_TOKEN existe siempre. GitHub lo crea por run, con permisos sobre este repo, y lo destruye al terminar. Se usa como ${{ secrets.GITHUB_TOKEN }} o ${{ github.token }}.
  • permissions lo recorta. Cada scope acepta read, write o none. Regla dura: si nombrás UN scope, todos los demás pasan a none. Así pages.yml, con solo contents: write, no puede tocar issues ni PRs.
  • Mínimo privilegio. Las suites (smoke, sanity, regression) no declaran permissions y heredan el default del repo: no necesitan escribir en GitHub, solo leer el código. Publicar a gh-pages lo hace un script con el mismo token, dentro del job allure-report.
  • No cruza repos. El token no puede disparar un workflow en OTRO repositorio. Para eso hace falta un PAT o una GitHub App: lo vemos en la Parte II (Cap 9).
  • Anti-recursión. Un evento provocado con GITHUB_TOKEN no dispara otros workflows (para que no se llamen en bucle), salvo workflow_dispatch y repository_dispatch.
permissions: pages.yml como único ejemplo real con contents: write. Regla: nombrar uno pone los demás en none. No cruza repos → Parte II.

quiz 04: configuración y seguridad operativa

Cancelo, encolo, secret o var?

Q7Disparás la regresión a mano a las 23:58. A las 00:00 arranca el cron. Qué pasa con el segundo run, según regression.yml?
ACancela al primero: el cron tiene prioridad
BQueda pending y arranca cuando el manual termine: mismo grupo, cancel-in-progress: false
CCorren los dos en paralelo: son triggers distintos
Encola. Ambos runs son de la misma rama, así que caen en el grupo regression-refs/heads/main. Con cancel-in-progress: false el segundo espera. Si cancelara, el manual podría haber subido ya sus artifacts y allure-report publicaría un reporte parcial como si fuera la regresión completa (el comentario del bloque concurrency).
Q8Un PR llega desde un fork externo. build.yml corre. Qué valor tiene ${{ secrets.STAGING_USER_PASSWORD }} dentro de ese run?
AEl mismo de siempre: los secrets son del repo, no del PR
BVacío: los secrets no se pasan a workflows disparados por pull_request desde forks
CEnmascarado como *** pero funcional
Vacío, por diseño. Un fork podría modificar el YAML para imprimir el secret; por eso GitHub no lo entrega y además le da al token permisos de solo lectura. El propio build.yml lo anticipa: su bloque env declara el par de test-user como opcional («A fork PR has no secrets and still passes»), y el comentario del step de varlock dice que el único requerido es TEST_ENV, así que el fork pasa varlock y test:env:check con esos secrets vacíos.
Q7: mismo grupo → pending. Q8: forks no reciben secrets; el comentario del propio build.yml lo dice.

Capítulo 05

Artifacts y el esqueleto de una suite

El runner se destruye al terminar. Lo que no subas como artifact, desaparece. Y una vez que sabés subir, bajar y retener, el esqueleto de nuestras tres suites se lee de una pasada.

upload-artifact / download-artifactretención y nombres únicosla plantilla común
Capítulo 5: artifacts + esqueleto. Cierra la Parte I.

lo que sobrevive al runner

Artifacts: subir en un job, bajar en otro

.github/workflows/smoke.yml · extracto: steps Upload … (sube) · Download Smoke Test Artifacts (baja)real
      - name: Upload Allure Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: smoke-allure-results     # único por run
          path: allure-results/
          retention-days: 10
      - name: Upload Failure Evidence
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: smoke-failure-evidence
          path: |                        # varias rutas
            test-results/
            playwright-report/
          retention-days: 10
…   # en el job allure-report (otro runner):
      - name: Download Smoke Test Artifacts
        uses: actions/download-artifact@v4
        with:
          name: smoke-allure-results
          path: smoke-artifacts/allure-results
        continue-on-error: true   # si no existe, no rompas el job
ArtifactContenidoCuándoQuién lo consume
*-allure-resultsJSON de Allure por testalways()job allure-report → reporte publicado
*-test-resultsjunit.xml, results.json, tracesalways()vos con gh run download
*-atc-resultsreports/atc_results.json: un veredicto por ATCalways() (regression)job XrayImport: una entrada por clave de Test, nunca crea un Test
*-playwright-reportHTML de Playwrightalways() (sanity, e2e)humanos: abrir y ver el trace viewer
*-failure-evidencetest-results + playwright-reportfailure()triage del fallo, adjuntar al bug
merged-allure-results-{env}Integration + E2E fusionadossi hay resultadosanálisis del agente (/regression-testing Fase 2)

Snapshot: la lista viva son los steps Upload … de cada workflow.

  • Nombres únicos por run. En v4 los artifacts son inmutables: dos uploads con el mismo nombre en el mismo run fallan. Por eso los prefijos smoke-, sanity-, integration-, e2e-.
  • retention-days: 10. Después, se borran. El máximo por defecto es 90 (ajustable en Settings). Regla R4 de /regression-testing: bajá la evidencia de un build rojo antes de que expire.
  • continue-on-error: true en la descarga. Si la suite no llegó a subir nada, el job de reporte no debe morir por eso: sigue, y el step «Check Allure Results» decide.
  • El step Cleanup Workspace borra node_modules y resultados. En un runner de GitHub es redundante (la VM muere igual); en un runner self-hosted evita que el disco se llene.
Artifacts: upload (always/failure), download en otro job (continue-on-error), tabla de los tipos que producimos, inmutabilidad v4, retención.

explorador: el mismo esqueleto en smoke, sanity y regression

El esqueleto de una suite, bloque por bloque

plantilla destilada de .github/workflows/smoke.yml (cada bloque mapea a ese archivo)esquema
# ==== Purpose: qué pregunta contesta, cuándo corre ====
name: Smoke Tests
run-name: ${{ github.actor }} triggered … on ${{ … || 'staging' }}
on:
  schedule: [{ cron }]
  workflow_dispatch: { environment, generate_allure, execution_key }
concurrency: { group: smoke-${{ github.ref }}, cancel-in-progress: false }
env:
  CI · TEST_ENV · <ENV>_USER_EMAIL/PASSWORD (secrets)
  TMS_PROVIDER (vars) · AUTO_SYNC · XRAY_CLIENT_* · STP_EXECUTION_KEY
jobs:
  SmokeTest:
    runs-on · timeout-minutes · environment: { name }
    steps:
      checkout → setup-bun → bun install → test:env:check → pw:install
      - run: bun run test:smoke
      - Sync Results to TMS   (if: always() && AUTO_SYNC…)
      - Upload Allure Results / Test Results   (if: always())
      - Upload Failure Evidence                (if: failure())
      - Cleanup Workspace                      (if: always())
  allure-report:   needs: SmokeTest · if: always() && …
    checkout · checkout gh-pages (history) · download · check → publish
Haz clic en un bloque →

Lo que acabás de aprender a leer

Con estos nueve bloques leés smoke.yml, sanity.yml y regression.yml de una pasada. La diferencia entre ellos no es el esqueleto: es qué corre el step central y cuántos jobs de suite hay antes del reporte. Eso es la Parte II.

Esqueleto destilado, 9 bloques clicables con referencias a smoke.yml. Puente a la Parte II.

quiz 05: artifacts y esqueleto

Último control de la Parte I

Q9Copiás el job E2ETests para crear un job «VisualTests» en regression.yml y dejás el upload con name: e2e-allure-results. Qué pasa en el primer run?
ALos dos jobs escriben en el mismo artifact y se fusionan
BEl segundo upload falla: en v4 los artifacts son inmutables y el nombre debe ser único por run
CEl segundo sobrescribe al primero en silencio
Falla. Desde upload-artifact v4 un nombre solo puede subirse una vez por run (salvo overwrite: true, que pisa el anterior). La solución es el prefijo por job (visual-allure-results) y, si querés fusionarlos, bajar con pattern: '*-allure-results' + merge-multiple: true, que es justo lo que hace a mano el step «Merge Allure Results» de .github/workflows/regression.yml.
Q10La regresión del viernes falló. Es lunes (3 días después) y querés el trace. Dónde está?
AEn el runner: los runners se conservan una semana
BEn el artifact e2e-failure-evidence del run, disponible hasta el día 10: gh run download <RUN_ID> -n e2e-failure-evidence
CYa no existe: los artifacts duran 24 horas
En el artifact, 10 días. El runner murió al terminar el job; lo único que quedó es lo que se subió con upload-artifact. retention-days: 10 te da hasta el día 10. Después, nada. Por eso el skill baja la evidencia de todo build rojo antes de analizarlo, y por eso el reporte Allure publicado (que vive en gh-pages, no en artifacts) conserva los últimos 10 runs por suite.
Q9: nombres únicos v4 → pattern+merge-multiple o merge manual. Q10: evidencia = artifact con retención 10 días.

cierre de la parte I: qué sabés ahora y qué sigue

Ya leés cualquiera de nuestros workflows. Ahora, la estrategia.

Lo que ya dominás

Workflow → jobs → steps → runner. Triggers (pull_request, schedule, workflow_dispatch con inputs). Contextos y el || de fallback. needs, uses/run, if: always(). Secrets vs vars vs env. Concurrencia y permisos. Artifacts.

Parte II · Arquitectura

Por qué cuatro suites y qué gatea cada una. regression.yml a fondo (merge, Allure, gh-pages). Cómo se ven los resultados. El write-back RTR/STR al TMS. Cómo conectar estas suites al pipeline de la app. Y el constructor de workflows para escribir el tuyo.

→ ci-pipelines-architecture.es.html

Fuentes para profundizar

docs.github.com/en/actions: workflow-syntax, events-that-trigger-workflows, contexts, control-workflow-concurrency, security/secure-use. En el repo: .agents/skills/regression-testing/references/ci-cd-integration.md. El análisis del run: how-it-works.es.html.

Un ejercicio antes de la Parte II

Abrí .github/workflows/sanity.yml en el repo. Sin mirar este deck, identificá: los inputs (y cuántos son), el grupo de concurrencia, qué steps llevan always() y cuál lleva failure(), y cómo el step «Run Sanity Tests» decide qué comando ejecutar. Si podés explicárselo a alguien, la Parte I está aprobada.

Un workflow es una receta que corre igual a las 3 AM que a las 3 PM. Tu trabajo es escribir la receta; el de GitHub, cocinarla.

Cierre Parte I. Recap, puente a Parte II, fuentes, ejercicio con sanity.yml.
Quiz 0 / 10
← → navegar · O resumen · S presentador · N notas · F pantalla