AGENTIC QA · MINI-CURSO · CI PARA TESTING
Parte II · ArquitecturaPor qué cuatro suites y no una. Qué gatea cada una y cuándo dispara. regression.yml job por job. Dónde aparecen los resultados y cómo vuelven al TMS. Cómo se conectará todo esto al pipeline de la app. Y cómo escribís el tuyo.
→ avanzar · O overview · N notas · F pantalla completa · clic en los bloques interactivos
mapa: haz clic en un capítulo para saltar
La Parte I te enseñó a leer un workflow. Esta parte explica por qué están escritos así, qué producen y cómo encajan con el resto del equipo.
Build · Smoke · Sanity · Regression. Qué gatea cada una, cuándo dispara, por qué no se mezclan.
Cuatro jobs, merge de resultados, publicación Allure con historia, una noche completa en línea de tiempo.
Dónde miro qué. El RTR/STR: execution_key, AUTO_SYNC, TMS_PROVIDER. La matriz de decisión real.
repository_dispatch · workflow_call · deployment_status. Que producción espere a las suites.
Constructor interactivo sobre nuestra plantilla + checklist de calidad antes del PR.
El veredicto GO / CAUTION / NO-GO y la clasificación de fallos (REGRESSION vs FLAKY vs ENVIRONMENT) no se repiten aquí: viven en regression-testing/how-it-works.es.html, que arranca justo donde este deck termina.
Capítulo 06
Un solo workflow que corra «todos los tests» contesta todo tarde y mal. Cada suite existe porque hay una pregunta distinta, con una urgencia distinta y un costo distinto de respuesta.
una tabla que vale por el capítulo
| Suite | Pregunta | Trigger | Alcance | Dura (orden de magnitud) | Gatea |
|---|---|---|---|---|---|
| build.yml | El framework compila, pasa lint y su documentación sigue consistente? | pull_request → main | types · lint · docs gates · contratos de documentación · evals · playwright test --list. Cero tests ejecutados. | ~3 min | el merge del PR |
| unit-tests.yml | Las herramientas del propio repo (cli/, scripts/) siguen andando? | push (cualquier rama) · PR de fork · manual | bun test (sin las suites de Playwright) + los gates de repo que build.yml no corre. Inerte fuera del repo del boilerplate. | minutos | cada push, también el directo a main |
| smoke.yml | La app está viva tras el deploy? | cron 02:00 · manual | solo @critical (proyectos smoke-ui + smoke-api) | 2-5 min | «seguimos o avisamos?» post-deploy |
| sanity.yml | Funciona ESTE cambio / feature? | solo manual, con inputs | un archivo, un --grep, o e2e/integration | 1-10 min | validar un test nuevo en CI antes del PR |
| regression.yml | Se rompió algo más? | cron 00:00 · manual | integration completo → e2e completo | 20-60 min | el release: GO / CAUTION / NO-GO |
Smoke habla del build de la app; sanity habla del cambio; regression habla del producto entero. Tres pass-rates distintos con tres SLOs distintos: nunca los sumes en un número (regla R6 de /regression-testing). Y build no habla de la app en absoluto: habla de nosotros.
La secuencia clásica de la industria es smoke → sanity → regression como gates sucesivos: si smoke falla, no gastás la regresión. En este repo los gates son temporales (cron) y manuales; conectarlos al deploy de la app es el Capítulo 9.
suite 0: no prueba la app, prueba el repo de pruebas
- name: Validate env schema (varlock) uses: dmno-dev/varlock-action@v1.0.5 with: fail-on-error: true show-summary: true # First gate, before any dependency is installed: loads .env.schema + # .env.core.schema against the job's env block above and writes a # REDACTED summary to the job page. The only required item is TEST_ENV # (it has a default); project credentials are optional typed examples, # so a PR from a fork, which has no secrets, passes here and at # test:env:check below. … # … Setup Bun · Install Dependencies - name: Validate Test Environment Variables run: bun run test:env:check - name: TypeScript Check run: bun run types:check - name: ESLint Check run: bun run lint:check - name: Docs Gates run: | bun run docs:check bun run skills:check - name: Documentation Contracts run: bun scripts/lint-doc-contracts.ts --base "$BASE_SHA" --head "$HEAD_SHA" - name: Skill Activation Evals Check run: bun run skills:evals - name: Playwright Compile Check run: bunx playwright test --list
playwright test --list es el step clave: compila cada spec sin ejecutarlo y sin credenciales de app.bun install: valida que el bloque env del job cumpla .env.schema y deja un summary redactado en la página del job. Lo único requerido es TEST_ENV (tiene default); el par de test-user es opcional, así que un PR de fork, que no tiene secrets, pasa aquí y en test:env:check.Docs Gates corre docs:check y skills:check (links y paths muertos, scripts citados que package.json no declara, Rule #17, router de skills y capa de instrucciones). Documentation Contracts falla el PR si cambia una región marcada con LINT.IfChange sin que cambien las páginas que su LINT.ThenChange nombra (o sin un trailer Docs-Checked: en un commit del PR). Por eso el checkout de build.yml trae el historial completo (fetch-depth: 0).cancel-in-progress: true): un push nuevo invalida al anterior y nada quedó publicado.timeout-minutes en build.yml): si tarda más, algo está mal en la máquina, no en el código.No corre ni un test contra staging. Correr la suite en cada PR costaría minutos y credenciales por cada push (incluidos forks, que no las tienen) y mezclaría dos preguntas: «compila?» con «la app funciona?». La segunda se contesta con sanity, a demanda.
suite 1: la pregunta más barata, la más frecuente
# smoke.yml - name: Run Smoke Tests (@critical) on ${{ env.TEST_ENV }} run: bun run test:smoke # package.json "test:smoke": "… playwright test --project=smoke-ui --project=smoke-api" # playwright.config.ts (comentario del bloque smoke, resumido) # Smoke Tests - @critical tagged tests, ONE PROJECT PER SURFACE # smoke-ui == e2e + @critical # smoke-api == integration + @critical { name: 'smoke-ui', grep: /@critical/, … } { name: 'smoke-api', grep: /@critical/, … }
El comentario del config lo cuenta: un solo proyecto smoke que abarcara e2e + integration tenía que elegir UN bloque use, y eligió el de UI, que le pasaba una sesión de navegador (con cookie) a los tests de API. Un test que esperaba 401 sin token recibía 200. Un proyecto no puede cargar el auth de dos superficies. Se partió en dos que reflejan exactamente a sus suites completas más el grep.
@critical en su título, no por estar en una carpeta. El mismo tag alimenta el label suite de Allure: una sola taxonomía para CI y reporte.STP_EXECUTION_KEY no se lee aquí (bloque env del workflow): un smoke que importara al RTR voltearía Test Runs que pertenecen a un veredicto de regresión completo. Solo importa si le pasás una execution_key explícita.suite 2: la única que siempre necesita que le digas qué
- name: Run Sanity Tests on ${{ env.TEST_ENV }} shell: bash env: GREP_INPUT: ${{ github.event.inputs.grep }} TEST_FILE: ${{ github.event.inputs.test_file }} TEST_TYPE: ${{ github.event.inputs.test_type }} run: | grep_args=() if [ -n "$GREP_INPUT" ]; then grep_args=(--grep "$GREP_INPUT") fi # Determine which test command to run if [ -n "$TEST_FILE" ]; then echo "Running specific test file: $TEST_FILE" bun run test "$TEST_FILE" "${grep_args[@]}" elif [ "$TEST_TYPE" == "e2e" ]; then echo "Running E2E tests" bun run test:e2e "${grep_args[@]}" elif [ "$TEST_TYPE" == "integration" ]; then echo "Running Integration tests" bun run test:integration "${grep_args[@]}" else echo "Running all tests" bun run test "${grep_args[@]}" fi
| Input | Tipo | Efecto |
|---|---|---|
| test_type | choice all / e2e / integration | elige el script de package.json |
| grep | string | --grep "…": por tag o por texto del título |
| test_file | string | una ruta: gana sobre test_type |
| custom_allure_url | string, default sanity | carpeta del reporte: /{env}/{custom}/ |
grep y test_file a la vez funciona, pero el skill lo desaconseja: elegí uno para que el reporte diga qué probaste.gh workflow run sanity.yml -f environment=staging -f test_file=tests/e2e/auth/login.test.ts.Este step interpolaba grep y test_file directamente dentro de run: y los pasaba por eval. Un input es texto controlado por quien dispara, y la guía de seguridad de GitHub pide pasarlo primero a una variable de env: y leerlo entre comillas. Eso es lo que ves arriba: los tres inputs entran por env:, se leen como "$GREP_INPUT" y "$TEST_FILE", y el eval desapareció: el comando se arma como un array de bash. Mismas cuatro ramas, mismos mensajes. Al escribir el tuyo, seguí este patrón (Cap 10).
decisor: elige la situación y mira qué suite corresponde
gh workflow run smoke.yml -f environment=staging. Verde = seguimos. Rojo = avisar antes de que el equipo pierda la tarde.-f test_file=… para tu test nuevo; -f grep="@checkout" para el área del fix. Pequeño, dirigido, con reporte propio./regression-testing crea el RTR y dispara con -f execution_key=<RTR>. Un re-run por ENVIRONMENT antes del veredicto reutiliza el MISMO RTR./regression-testing: crea el RTR, dispara con execution_key, persiste el RUN_ID y luego clasifica. Deck how-it-works para el resto.-f test_type=e2e -f grep="@checkout" corre solo los tests etiquetados. Reporte en /staging/sanity/ (o el custom_allure_url que elijas, p.ej. checkout-fix).execution_key. Un RTR nuevo (#2) solo después de un veredicto ya escrito.quiz 06: estrategia
playwright test --list en vez de playwright test?--list carga y compila cada spec: si un import está roto o un tipo no cierra, falla ahí, sin credenciales de app ni browsers. Ejecutar la suite en cada PR mezclaría el estado del framework con el estado de staging, y haría que un PR de fork (sin secrets) fallara por motivos que no son suyos.Capítulo 07
Cuatro jobs en regression.yml, y cada decisión con su comentario. Es el workflow mejor documentado del repo: vamos a leerlo como lo leería quien tiene que mantenerlo.
simulador: la misma animación de la Parte I, ahora con el porqué de cada job
| Job | Sube | Corre si… |
|---|---|---|
| IntegrationTests | integration-allure-results · integration-test-results | siempre (raíz del grafo) |
| E2ETests | e2e-allure-results · e2e-test-results · e2e-playwright-report · e2e-failure-evidence | Integration terminó bien |
| allure-report | merged-allure-results-{env} | ambos terminaron (como sea) y generate_allure != 'false' |
| XrayImport | nada: escribe en Xray | ambos terminaron (como sea) y vars.TMS_PROVIDER es xray |
Los dos primeros ejecutan Playwright y suben evidencia. Los dos últimos no prueban nada: consumen los artifacts para producir el reporte (humanos) y la importación (TMS). Separarlos permite que corran en paralelo, que corran aunque la suite haya fallado, y que cada uno se pueda apagar solo: generate_allure=false apaga uno; TMS_PROVIDER=jira apaga el otro.
un reporte por run, no uno por job
- name: Download Integration Allure Results uses: actions/download-artifact@v4 with: name: integration-allure-results path: regression-artifacts/integration-allure-results continue-on-error: true - name: Download E2E Allure Results … (idéntico, e2e-allure-results) - name: Merge Allure Results id: merge_results run: | mkdir -p merged-allure-results if [ -d "regression-artifacts/integration-allure-results" ] && [ -n "$(ls -A …)" ]; then cp -r regression-artifacts/integration-allure-results/. merged-allure-results/ … (lo mismo para e2e) - name: Check Allure Results id: check_allure echo "has_results=true" >> $GITHUB_OUTPUT - name: Upload Merged Allure Results if: steps.check_allure.outputs.has_results == 'true' name: merged-allure-results-${{ env.TEST_ENV }}
cp -r. Allure genera después UN reporte con las dos suites como suites distintas dentro.continue-on-error en cada descarga: si E2E se saltó (porque Integration falló), su artifact no existe y la descarga fallaría. El reporte igual debe salir, con lo que haya./regression-testing Fase 2 para analizar: gh run download <RUN_ID> -n merged-allure-results-staging.download-artifact@v4 con pattern: '*-allure-results' + merge-multiple: true hace el merge en un step. El repo lo hace a mano para poder loguear qué faltó.El veredicto es sobre el producto, no sobre un job. Un PM que abre dos reportes tiene que sumar mentalmente; uno solo con la pestaña Graphs (pirámide de testing, severidades, tendencia) contesta solo. Y la historia de tendencias (history.jsonl) se lleva por {env}/{suite}: dos reportes serían dos historias.
del artifact a una URL con historia
- name: Get Allure History uses: actions/checkout@v4 with: ref: gh-pages # la rama publicada, no main path: gh-pages fetch-depth: 1 continue-on-error: true # primer run: la rama no existe - name: Build & Publish Allure Report (Allure 3) if: steps.check_allure.outputs.has_results == 'true' env: PORTAL_URL: ${{ secrets.PORTAL_URL }} # + PORTAL_*, R2_* (modo privado) run: | if [ -n "$PORTAL_URL" ]; then bun scripts/ci/publish-allure-portal.ts … # portal privado (R2) else bun scripts/ci/publish-allure-pages.ts \ --env "${{ env.TEST_ENV }}" --suite regression \ --run "${{ github.run_number }}" \ --results merged-allure-results --pages gh-pages --keep 10 echo "## Allure Report" >> $GITHUB_STEP_SUMMARY echo "View the report: https://${{ github.repository_owner }}.github.io/${{ … }}/${{ env.TEST_ENV }}/regression/" >> $GITHUB_STEP_SUMMARY fi
{env}/{suite}/
index.html → redirect relativo al último run
history.jsonl → tendencia Allure 3 (restaurada antes de
generar, guardada después)
{runNumber}/ → un reporte completo por run
(se conservan los últimos 10, --keep 10)allurerc.mjs y el devDep allure: lo que ves en bun allure:open es byte a byte lo publicado. Un solo plugin (Awesome) → el index.html ES el reporte, con Report | Graphs | Timeline.https://upex-galaxy.github.io/agentic-qa-boilerplate/staging/regression/. Solo existe si el job allure-report terminó: si falló, analizás desde el artifact.PORTAL_URL el reporte va a un portal con login (R2 + Vercel) en vez de a Pages. Runbook: regression-testing/references/private-hosting-setup.md.pages-squash.yml la aplana a un commit el 1° de cada mes.línea de tiempo: pulsa ▶
timeout-minutes de cada joblos artifacts de la tabla del Cap 7, con su retención · el reporte en /staging/regression/<run>/ (últimos 10) · el summary del run con el link · los Test Runs actualizados en el RTR de Xray.
Nadie clasifica los fallos ni emite GO / NO-GO: eso lo hace el agente con /regression-testing, leyendo el merged y el --log-failed. El CI produce evidencia; el veredicto es humano-asistido.
El job Slack está escrito y comentado al final del archivo: un webhook con resultado por job y botones al run y al reporte. Descomentarlo es una decisión de equipo, no un cambio técnico.
quiz 07: regression.yml
if: always() y la descarga de E2E lleva continue-on-errorneeds falló y no tiene always()). allure-report sí arranca (if: always() && …), la descarga de e2e-allure-results falla sin romper el job, el merge copia lo que hay y publica. El agente verá una suite sola y sabrá por qué.gh-pages, que no existe. Qué pasa?continue-on-error: true); el reporte nace sin historia y el script crea la ramacontinue-on-error justamente para el primer run. Los gráficos de tendencia aparecen del segundo reporte en adelante, cuando ya hay un history.jsonl que restaurar. Es el mismo mecanismo que en local con .allure/history.jsonl.Capítulo 08
Dónde aparece cada cosa cuando el run termina, y cómo los resultados vuelven a Jira/Xray sin que nadie los copie a mano: el RTR, la execution_key y la matriz de decisión que gobierna cada import.
el mapa de los resultados
| Quiero saber… | Dónde | Cómo llego | Qué veo |
|---|---|---|---|
| Pasó o falló, y qué job? | GitHub → Actions → el run | gh run view <RUN_ID> --json status,conclusion,jobs | grafo de jobs con ✓/✗/⊘, duración por job, el summary con el link al reporte |
| Qué línea falló? | Logs del job | gh run view <RUN_ID> --log-failed | solo los steps rojos (el --log completo puede pesar >50 MB) |
| Qué tests, con qué tendencia? | Reporte Allure publicado | https://…github.io/…/{env}/{suite}/ | Report (drill-down, tags) · Graphs (status, severidades, pirámide) · Timeline |
| Qué pasó en pantalla? | Playwright HTML report + traces | gh run download <RUN_ID> -n e2e-playwright-report | trace viewer: DOM, red, consola paso a paso. trace: 'retain-on-failure' en el config |
| Evidencia para el bug | artifact de fallo | gh run download <RUN_ID> -n e2e-failure-evidence -D ./analysis/evidence/ | screenshots, traces, videos (si video_record=true) |
| Los datos crudos para analizar | artifact merged | gh run download <RUN_ID> -n merged-allure-results-staging | un JSON por test: status, message, labels (testId = ATC, suite, severity) |
| Qué quedó en el TMS? | Jira / Xray | el RTR (Test Execution) del run | un Test Run por ATC con PASSED / FAILED, importado desde reports/atc_results.json con la clave de cada Test |
/regression-testing fija la prioridad: Allure results JSON > Playwright results.json > logs. Los logs son el último recurso, no el primero. Y el label suite de Allure deriva del tag Playwright que también eligió el alcance en CI: una sola taxonomía, cero reconciliación.
un RTR por veredicto
RTR: staging-2026-09-23: Regression Testing, padre «QA Test Artifacts», testPlan → RTP, environment + assignee al crear.gh workflow run regression.yml -f environment=staging -f execution_key=UPEX-482STP_EXECUTION_KEY: ${{ github.event.inputs.execution_key || secrets.STP_EXECUTION_KEY }}KataReporter → reports/atc_results.json en cada job; subido como *-atc-results.bun run xray:payload (una entrada por ATC, sin testInfo) y bun xray import xray en ${STP_EXECUTION_KEY}. Nunca crea un Test.#2). Nunca se reabre uno cerrado.STP_EXECUTION_KEY como secret le da destino. El nombre quedó por compatibilidad; la semántica cambió.env de smoke.yml): importar el subconjunto @critical en el RTR voltearía Test Runs que pertenecen a un veredicto completo. Solo importan con key explícita.simulador: mueve los tres interruptores y mira qué steps corren de verdad
Todo es opt-in y no bloqueante: un repo sin TMS ve un aviso, nunca un rojo. La suite es verde o roja por los tests; el write-back nunca cambia ese color (continue-on-error en el import).
Las expresiones son las del archivo. Nota el detalle de diseño: en xray, los steps «Sync … to TMS» de cada job se saltan porque XrayImport ya importa el mismo reports/atc_results.json (comentario del step: «two imports of the same runs is a double-import, not a safety net»). El paso test:sync lee reports/atc_results.json, que KataReporter escribe al salir Playwright: por eso es un step separado y nunca un teardown.
quiz 08: resultados y write-back
execution_key. AUTO_SYNC=true, TMS_PROVIDER=xray. Qué llega a Xray?STP_EXECUTION_KEY: ${{ github.event.inputs.execution_key }} sin fallback al secret, y el if del step Sync Results to TMS exige key cuando el proveedor es xray. Importar el @critical dentro del RTR voltearía Test Runs de un veredicto completo (comentario del bloque env). C tampoco: el import sin destino se evita justamente porque acuñaría huérfanas.…#2): el veredicto del jueves ya se escribió y cerró; nunca se reabreenv de regression.yml).Capítulo 09
Sin esta integración, nuestras suites corren por reloj o por botón. El objetivo: que el deploy de la app a staging dispare las suites, y que producción espere a que pasen. GitHub ofrece tres mecanismos; ninguno está cableado aún. Todo YAML de este capítulo es ilustrativo.
repository_dispatchworkflow_calldeployment_statusel gate a producciónel problema y las tres formas de resolverlo
La app despliega a staging a las 22:00 y nuestra regresión corre por cron a las 00:00 (horas ilustrativas; la real es el schedule de regression.yml). Si el deploy fue a las 00:30, probamos la versión vieja. Y producción no espera a nadie: el deploy a prod es una decisión humana sin gate automatizado de QA.
Deploy a staging → smoke (inmediato) y regression o sanity del área → el resultado vuelve al commit de la app → deploy-production solo arranca si el gate está verde, con un revisor humano vía environment: production.
El GITHUB_TOKEN de un workflow no dispara nada en OTRO repo, y los eventos de la app ocurren en la app. Cruzar la frontera exige uno de los tres mecanismos, y un token con alcance (PAT fine-grained o GitHub App) en los dos primeros.
| Mecanismo | Quién dispara | Qué recibe el repo QA | Feedback a la app | Coste |
|---|---|---|---|---|
| repository_dispatch | la app, con POST /repos/{qa}/dispatches (token cross-repo) | event_type (≤100 chars) + client_payload (≤10 claves: env, sha, url) | manual: escribir un commit status / check en la app | medio: token + dos workflows |
| workflow_call | la app, como un job: uses: upex-galaxy/qa/.github/workflows/regression.yml@main | inputs y secrets declarados; secrets: inherit si misma org | nativo: el job sale ✓/✗ en el run de la app; needs es el gate | bajo si misma org; el repo QA declara on: workflow_call |
| deployment_status | la plataforma de hosting (Vercel, Render…) al terminar el deploy | state (success/failure/pending…), environment, environment_url | indirecto: no bloquea el deploy que ya ocurrió | bajo, pero solo llega al repo donde ocurrió el deployment |
Regla de las docs que hace posible todo esto: los eventos provocados con GITHUB_TOKEN no disparan otros workflows (anti-bucle) salvo workflow_dispatch y repository_dispatch. Por eso el segundo existe.
diseñador: elige mecanismo, suite y dureza del gate
cómo se vería el pipeline completo, con la recomendación
environment: staging. Al terminar, invoca QA.needs: [qa] + environment: production con required reviewers.upex-galaxy), así que secrets: inherit funciona y el gate es un needs: nativo, visible, sin token extra.on: workflow_call a smoke.yml y regression.yml sin quitar el dispatch ni el cron: un mismo archivo, tres formas de entrar. inputs.environment sirve para los tres.needs) y un humano (protection rule «required reviewers» en el environment production). El veredicto GO sigue siendo humano-asistido; el CI solo impide que se olvide.Un run disparado por la app no pasa por /regression-testing, así que nadie creó el RTR antes. Opciones: que la app no pase key (el import se salta con ::warning, el reporte Allure igual se publica), o que un step previo cree el RTR con bun xray y lo pase como input. La segunda es la que preserva «un RTR por veredicto».
Ni el esqueleto de la suite ni el reporte ni la clasificación de fallos. El trigger es la única línea que se mueve. Por eso valía la pena aprender el esqueleto primero.
Fuentes: docs GitHub reuse-workflows, events-that-trigger-workflows (repository_dispatch, deployment_status), deploy-to-environment; patrones de la industria: E2E post-deploy con dispatch (oneuptime, qaskills) y repos cross-trigger (AST-LW/math-operations-e2e).
quiz 09: integración con el pipeline de desarrollo
deploy-production no arranque si la regresión falla. Mecanismo más simple?needs en deploy-productionsecrets: inherit vale; el job QA aparece en el run de la app y needs es el gate, sin tokens ni statuses a mano. A funciona pero cuesta más (token cross-repo + feedback manual). C no llega: deployment_status se emite en el repo donde ocurrió el deployment, que es la app, no QA.on: workflow_call a regression.yml y leés el entorno con github.event.inputs.environment. Desde la app llega vacío. Por qué?github.event.inputs es del dispatch; los inputs de workflow_call llegan por el contexto inputs, que sirve para ambossecrets: inheritgithub.event.inputs no trae nada. El contexto inputs unifica ambos casos: es el que las docs recomiendan y el que habría que adoptar en los tres || 'staging' del archivo al hacer el cambio. B afecta a secrets, no a inputs.Capítulo 10
Todo lo anterior cabe en una plantilla. Ahora la armás con interruptores, la leés completa y la pasás por el checklist antes de abrir el PR. Sin copiar a ciegas: cada bloque ya sabés qué hace.
constructor: cada interruptor añade o quita un bloque; el YAML sigue el esqueleto del repo
Guardalo como .github/workflows/<suite>.yml, corré bun run format:check (prettier valida YAML) y pasá el checklist de la siguiente slide. Los inputs se leen con github.event.inputs para ser coherente con las suites actuales; si añadís workflow_call, migrá a inputs.*.
checklist: marca cada punto; se guarda en este navegador
bun run test:smoke, nunca npx playwright test … escrito a mano (Rule #11).|| 'default' donde un cron pueda dispararlo.${{ secrets.X }}. Los nombres siguen el prefijo de entorno: STAGING_USER_EMAIL.run:. Pasalo por env: y leelo como "$VAR" (docs: secure-use).timeout-minutes en cada job. Sin él, un cuelgue factura hasta 6 horas.if: always() en cada upload de resultados y failure() en la evidencia extra.concurrency decidida: publica → cancel-in-progress: false; solo valida → true.permissions mínimos si el workflow escribe en GitHub; nada declarado si solo lee.@v4) y, para terceros críticos, SHA completo con comentario de versión.workflow_dispatch antes de confiar en el cron. Un cron nuevo no corre hasta estar en main; el botón sí (gh workflow run <file> --ref <tu-rama> una vez que el archivo también exista en main).Cierre del ciclo: el PR dispara build.yml, que valida el repo pero no el YAML nuevo. El YAML lo valida bun run format:check en local (prettier) y, en última instancia, el primer run. Por eso el punto 12 no es opcional.
quiz 10: el tuyo
grep y lo usa así: run: bun run test --grep="${{ github.event.inputs.grep }}". Un compañero de seguridad lo marca. Qué cambiás?env: GREP: ${{ github.event.inputs.grep }} y en el run --grep="$GREP": el valor no participa en la generación del scriptrun: se sustituye ANTES de que bash lea el script: un input con "; rm -rf … se convierte en código. Con env: el valor llega como variable de entorno y bash lo trata como dato. Es la recomendación literal de la guía de seguridad de GitHub, y el patrón que aplica el step Run Sanity Tests de sanity.yml: inputs por env:, leídos entre comillas, sin eval.permissions: write-allgh workflow run tampoco lo arranca, el problema es el YAML, no el relojcierre: el mapa completo y qué sigue
Leer y escribir un workflow de GitHub Actions. Por qué existen cuatro suites y qué gatea cada una. Cómo regression.yml fusiona, publica e importa. Dónde vive cada resultado. Cómo funciona el write-back RTR/STR. Y qué haría falta para que producción espere a las suites.
El run terminó y dejó evidencia. Clasificar cada fallo (REGRESSION · FLAKY · ENVIRONMENT · KNOWN · NEW TEST · KNOWN-BLOCKED), puntuar y emitir el veredicto es /regression-testing: how-it-works.es.html. Escribir los tests que corren aquí es /test-automation.
Repo: .github/workflows/*.yml (leelos con este deck al lado) · .agents/skills/regression-testing/references/ci-cd-integration.md (sharding, caché, path filters, troubleshooting) · references/github-pages-setup.md · references/private-hosting-setup.md. GitHub Docs: docs.github.com/en/actions. Playwright: playwright.dev/docs/test-sharding.
Tomá el YAML del constructor para una suite visual por tag @visual. Pasá los 12 controles. Abrí un PR de prueba (sin mergear) y mirá cómo build.yml reacciona. Después borrá la rama. Si entendiste cada línea que escribiste, este mini-curso cumplió.
Un pipeline de testing no es una lista de comandos: es la respuesta escrita a «qué queremos saber, cuándo, y qué pasa si la respuesta es no?». Ahora sabés escribirla.