AGENTIC QA · MINI-CURSO · CI PARA TESTING
Parte I · FundamentosCó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.
→ avanzar · O overview · N notas · F pantalla completa · clic en los bloques interactivos
mapa: haz clic en un capítulo para saltar
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.
El pipeline como red de seguridad. Workflow, job, step, runner, action, artifact.
pull_request, schedule, workflow_dispatch con inputs, cron, contextos y expresiones.
runs-on, needs, uses vs run, if: con always(), outputs y job summary.
Tres niveles de configuración, environments, concurrency, permissions.
Subir, bajar, retener evidencia. La plantilla que usamos en las tres suites.
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.
Capítulo 01
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.
build.yml sin miedoel dolor y el patrón
«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.
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
playwright test --list.cli/, scripts/) siguen andando? bun test + los gates que build.yml no corre.@critical.--grep.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.
seis palabras que vas a leer mil veces
| Término | Qué es | En este repo |
|---|---|---|
| Workflow | Un archivo .yml en .github/workflows/. Define cuándo corre y qué hace. | smoke.yml, regression.yml… |
| Event / trigger | Lo que despierta al workflow: un PR, un cron, un clic manual, un evento externo. | on: schedule / workflow_dispatch |
| Job | Un conjunto de steps que corre en UNA máquina. Los jobs corren en paralelo salvo que declares needs. | IntegrationTests, E2ETests |
| Step | Una acción dentro del job: un comando (run) o una acción reutilizable (uses). | run: bun run test:smoke |
| Runner | La máquina virtual que ejecuta el job. GitHub la crea limpia y la destruye al terminar. | runs-on: ubuntu-latest |
| Action | Un step empaquetado por alguien (GitHub, Oven, Playwright…) que enchufás con uses:. | actions/checkout@v4 |
| Artifact | Archivos que el job guarda al terminar para que los bajes o los use otro job. | smoke-allure-results |
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
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.
explorador: haz clic en cada bloque del YAML
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
name es el título fijo del workflow en la pestaña Actions. run-name es el título de CADA ejecución y acepta expresiones ${{ }}: aquí muestra quién lo disparó (github.actor) y en qué rama (github.ref_name).
Sin run-name, GitHub usa el mensaje del commit. Ponerle nombre convierte la lista de runs en un log legible.
Este workflow despierta cuando se abre o actualiza un pull request hacia main. Ningún cron, ningún botón: solo PRs. Es el gate de la rama: si el framework no compila, el PR queda rojo.
Fíjate en branches: [main]: un PR hacia otra rama no lo dispara. Filtros de rama y de ruta (paths) se combinan con AND.
Un solo build por rama (group: build-${{ github.ref }}). Si haces push dos veces seguidas, el primer run se cancela: no publica nada, así que cancelarlo es gratis y ahorra minutos.
Compara con las suites: ellas usan cancel-in-progress: false porque publican un reporte. Lo vemos en el Capítulo 4.
A nivel de workflow, env define variables de entorno que heredan todos los jobs y steps. CI: true es una convención que muchas herramientas leen (Playwright activa forbidOnly, por ejemplo).
Las credenciales NO están escritas aquí: se leen de ${{ secrets.X }}, que GitHub inyecta enmascaradas. Copiar un valor de tu .env al YAML es la falta más grave del curso.
En build.yml el par de test-user es opcional: nada en este job hace login, así que un PR de fork, que no recibe secrets, pasa igual.
La clave TestBuild es el id (lo usa needs); name es lo que ves en la UI. runs-on: ubuntu-latest pide una VM Linux de GitHub, limpia. timeout-minutes: 15 mata el job si se cuelga: sin él, el default es 360 minutos de factura.
actions/checkout@v4 clona el repo en el runner (sin esto, la máquina está vacía). fetch-depth: 1 traería solo el último commit (más rápido, y es lo que usan smoke, sanity y regression). build.yml usa fetch-depth: 0, el historial completo, porque el step Documentation Contracts compara el rango del PR y lee los mensajes de sus commits. El siguiente step (Setup Bun, omitido aquí) es oven-sh/setup-bun@v2, que instala Bun.
@v4 es una etiqueta de versión mayor. Para máxima seguridad, GitHub recomienda fijar un SHA de commit completo en acciones de terceros: una etiqueta se puede mover, un SHA no.
Cada run es un comando de shell en el runner. Aquí corren los mismos scripts de package.json que corres en local: lint:check y docs:check / skills:check (además de types:check y otros steps, omitidos por espacio). Los dos últimos son los gates de documentación: el PR queda rojo si hay links o paths muertos, o si rompe un contrato de documentación. playwright test --list compila todos los specs sin ejecutarlos: valida que la suite ES ejecutable.
Regla del repo: el YAML nunca inventa comandos. Llama a los scripts de package.json, así local y CI hacen exactamente lo mismo.
quiz 01: vocabulario y anatomía
E2ETests instala Bun y los browsers aunque IntegrationTests ya lo hizo un minuto antes. Por qué?needs borra el estado del job anterioroutputs). 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.env: del YAML «porque total el repo es privado». Qué le decís?${{ secrets.X }}; el YAML queda en el historial de git para siempreCapítulo 02
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 ${{ }}simulador: elige un evento y mira qué workflows despiertan
on: pull_request: branches: [main]on: push (cualquier rama, sin tags) · pull_request de forks · workflow_dispatchbun 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.schedule 02:00 · workflow_dispatchworkflow_dispatchschedule 00:00 · workflow_dispatchpush a main con paths · dispatch · squash mensualpackages/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.paths: del propio workflow (kata-academy, pages-home, decks, docs y el workflow mismo; la fuente es ese bloque). Un push que cambia un test no publica nada.TEST_ENV cae en 'staging' por el || 'staging' del bloque env, y STP_EXECUTION_KEY cae en el secret. Dos horas antes que smoke para no pisarse.workflow_dispatch, uno a la vez: vos elegís cuál en la UI (o con gh workflow run <archivo>). El formulario se genera de los inputs. build.yml NO tiene botón: solo PR.cron '0 4 1 * *'): reescribe gh-pages a un único commit para que la rama generada no crezca sin límite. Un cron de mantenimiento, no de testing.formulario: cambia los valores y mira el comando que genera
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
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.
cron: haz clic en cada campo
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 = en punto. El intervalo mínimo que GitHub acepta es 5 minutos. Consejo de la doc: evita el minuto 0, es cuando más carga hay y los runs se retrasan; 17 2 * * * arranca más puntual que 0 2 * * *.
2 = 02:00 UTC. En Buenos Aires son las 23:00 del día anterior; en Madrid las 04:00. Regression va a las 00:00 y smoke a las 02:00: dos horas de margen para que la regresión (hasta 60 min) no se solape con el smoke en gh-pages.
* = todos. 1 = solo el primero: así pages-squash.yml corre mensual.
* = todos los meses. Casi nunca se usa otra cosa en testing.
* = todos. 1-5 correría solo de lunes a viernes: útil si el entorno de staging se apaga los fines de semana y no querés ruido rojo el sábado.
main.|| 'default'. Y por eso execution_key cae al secret STP_EXECUTION_KEY en la regresión nocturna.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.
el lenguaje dentro de ${{ }}
| Contexto | Qué contiene | Ejemplo real en el repo |
|---|---|---|
| github | Metadatos del run: quién, qué rama, qué evento, número de run. | ${{ github.actor }} · github.ref_name · github.run_number (smoke.yml) |
| github.event | El payload del evento que disparó el run. Con dispatch, trae inputs. | ${{ github.event.inputs.environment || 'staging' }} (bloque env) |
| inputs | Los inputs tipados (booleanos siguen siendo booleanos). Sirve para dispatch y para workflow_call. | ${{ inputs.environment }} (no usado en este repo) |
| secrets | Valores cifrados, enmascarados en logs. Solo en env, with e if de step. | ${{ secrets.XRAY_CLIENT_ID }} (regression.yml, bloque env) |
| vars | Configuració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) |
| env | Las variables de entorno definidas con env:. NO disponible en if de job. | ${{ env.TEST_ENV }} (smoke.yml, step Run Smoke Tests) |
| steps | Outputs de steps anteriores (mismo job) por su id. | steps.check_allure.outputs.has_results (smoke.yml, job allure-report) |
| needs | Resultado y outputs de los jobs de los que dependés. | needs.SmokeTest.result (en el job Slack comentado) |
# 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'
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.
quiz 02: triggers, inputs y contextos
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?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'.if: env.TMS_PROVIDER == 'xray' a nivel de job. Qué pasa?if de job no puede leer env; hay que usar vars.TMS_PROVIDERenv 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.Capítulo 03
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 summarysimulador: pulsa ▶ y mira el orden real de ejecución
needs: IntegrationTests · timeout 60needs: [IntegrationTests, E2ETests]if: always() && …needs: [IntegrationTests, E2ETests]if: always() && vars…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.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ó.needs: [IntegrationTests, E2ETests] arranca cuando ambos terminaron (o se saltaron). needs.X.result te dice cómo: success / failure / cancelled / skipped.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.
los cinco steps que toda suite repite
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 actionCó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 comandoShell 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.
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.simulador: cambia cómo terminó la suite y mira qué steps corren
| Función | Devuelve true cuando… | Uso típico |
|---|---|---|
| success() | ningún step/job anterior falló (es el default implícito) | casi nunca se escribe |
| always() | siempre, incluso cancelado | uploads, cleanup, reporte |
| failure() | algún step anterior falló | evidencia de fallo, alertas |
| cancelled() | el run fue cancelado | limpieza específica |
cómo un step le habla al siguiente, y al humano
- 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
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.
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.
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.
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.
quiz 03: ejecución
if: always() del step «Upload Allure Results» para «simplificar». La suite falla esa noche. Qué encontrás por la mañana?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».allure-report quiere saber si el job SmokeTest dejó resultados. Cómo puede enterarse?allure-results/ del disco: los jobs comparten el filesystemsteps.check_allure: los outputs de steps son globalesdownload-artifact, o leyendo needs.SmokeTest.outputs.* si el job los declaró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).Capítulo 04
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ínimosclasificador: dónde va cada valor? un clic por fila
| Nivel | Para qué | Propiedades |
|---|---|---|
env: literal | Constantes del workflow | Visible en el YAML y en logs. CI: true. |
${{ vars.X }} | Configuración que cambia por repo/entorno pero no es secreta | Se define en Settings → Variables. Legible en if de job. Vacío si no existe. |
${{ secrets.X }} | Credenciales | Cifrado, enmascarado en logs, no llega a PRs de forks, no legible en if de job. |
environment: name: = un cajón de secrets con reglasCada 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.
simulador: dos runs del mismo grupo se solapan
# 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
group define quién compite. Puede usar expresiones (github, inputs, vars). build-${{ github.ref }}: un grupo por rama. Los nombres no distinguen mayúsculas.queue: max podés encolar hasta 100, pero no combinado con cancel-in-progress: true.pages-docs-hub) porque siempre publica al mismo sitio.«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).
el token que GitHub le presta a cada job
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.
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.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.GITHUB_TOKEN no dispara otros workflows (para que no se llamen en bucle), salvo workflow_dispatch y repository_dispatch.quiz 04: configuración y seguridad operativa
cancel-in-progress: falseregression-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).${{ secrets.STAGING_USER_PASSWORD }} dentro de ese run?pull_request desde forks*** pero funcionalenv 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.Capítulo 05
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únlo que sobrevive al runner
- 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
| Artifact | Contenido | Cuándo | Quién lo consume |
|---|---|---|---|
| *-allure-results | JSON de Allure por test | always() | job allure-report → reporte publicado |
| *-test-results | junit.xml, results.json, traces | always() | vos con gh run download |
| *-atc-results | reports/atc_results.json: un veredicto por ATC | always() (regression) | job XrayImport: una entrada por clave de Test, nunca crea un Test |
| *-playwright-report | HTML de Playwright | always() (sanity, e2e) | humanos: abrir y ver el trace viewer |
| *-failure-evidence | test-results + playwright-report | failure() | triage del fallo, adjuntar al bug |
| merged-allure-results-{env} | Integration + E2E fusionados | si hay resultados | análisis del agente (/regression-testing Fase 2) |
Snapshot: la lista viva son los steps Upload … de cada workflow.
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.explorador: el mismo esqueleto en smoke, sanity y regression
# ==== 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
El comentario de las primeras líneas dice qué pregunta contesta la suite («Daily health check running only @critical tagged tests»). Es la primera línea que lee quien abre el archivo en seis meses. run-name con actor + entorno hace legible la lista de runs.
El cron da la rutina; el dispatch da el control manual con tres inputs que se repiten en las tres suites: environment, generate_allure, execution_key. Sanity añade test_type, grep, test_file; regression añade video_record (la lista viva es el bloque inputs: de cada workflow).
Un grupo por suite y rama. cancel-in-progress: false porque el job final publica a gh-pages y dos publicaciones solapadas se pisan.
Qué entorno, qué credenciales (por prefijo de entorno: STAGING_USER_EMAIL), qué proveedor de TMS y si la sincronización está activa. Todo lo sensible por secrets, lo configurable por vars, lo derivado por expresión.
runs-on: ubuntu-latest, timeout-minutes acorde al tamaño de la suite, y environment: name: igual al input, para heredar los secrets y reglas de ese environment de GitHub.
Siempre los mismos, siempre en este orden: código → runtime → dependencias → validación de variables (falla temprano) → browsers. Copiar y pegar es correcto aquí: un job es una máquina nueva.
Un solo run con el script de package.json. Después, con always(), el paso que escribe en el TMS: lee reports/atc_results.json, que KataReporter escribe al salir Playwright (por eso es un step aparte y no parte del test). Detalle en la Parte II, Cap 8.
Tres uploads con always() (resultados) y uno con failure() (evidencia extra). Nombres con el prefijo de la suite. Retención la que fije retention-days. Cleanup al final.
Otro runner: vuelve a hacer checkout (necesita allurerc.mjs y el devDep allure), baja la rama gh-pages para la historia de tendencias, baja el artifact, comprueba que hay resultados y publica con el mismo script que usás en local. Parte II, Cap 7.
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.
quiz 05: artifacts y esqueleto
name: e2e-allure-results. Qué pasa en el primer run?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.e2e-failure-evidence del run, disponible hasta el día 10: gh run download <RUN_ID> -n e2e-failure-evidenceupload-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.cierre de la parte I: qué sabés ahora y qué sigue
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.
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.
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.
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.