Skip to content

English · Español

Lab 04 — Gate de deploy en CI: bloquear regresiones del tutor de gramática

Objetivo: cablear .github/workflows/deploy-grammar-tutor.yml. Verificar que promueve un candidato limpio; verificar que rechaza un candidato deliberadamente regresado.

Tiempo estimado: 3–5 horas.

Prerequisito: labs 00–03 hechos. eval_baseline.json commiteado en la raíz del repo con precisión por bucket de la entrada activa del registry de producción. Tracking server de MLflow alcanzable desde el runner de GitHub Actions (para el currículo: un MLflow público de solo lectura en el laptop, o MLflow in-CI con los artefactos pusheados como workflow artifact).


Lo que produces

experiments/38-ci-gate/ conteniendo:

  • degraded_model/ — una copia de un checkpoint registrado existente con una regresión por bucket deliberada inyectada (p. ej., pesos del head de participio pasado perturbados; o un wrapper que aleatoriza 5% de las salidas de participio pasado).
  • register_degraded.py — sube el modelo degradado a MLflow, captura el run_id.
  • pr_clean.md — narrativa + screenshot de la corrida CI que pasó y promovió un candidato limpio.
  • pr_regressed.md — narrativa + screenshot de la corrida CI que falló sobre el candidato regresado.
  • manifest.json.

Además, fuera del directorio del experimento:

  • .github/workflows/deploy-grammar-tutor.yml — el workflow CI.
  • scripts/mlops/compare_baseline.py — la lógica de comparación invocada por el workflow.
  • eval_baseline.json — baseline commiteada (puede ya existir tras el lab 00).

TODOs

Bloque A — escribe el workflow

  • Crea .github/workflows/deploy-grammar-tutor.yml. Trigger: PR etiquetada deploy-candidate con un comentario conteniendo mlflow_run_id=<id> semver=v0.X.Y.
  • Seis etapas per theory/05 Parte 1:
  • actions/checkout@v4 + astral-sh/setup-uv@v3 + uv sync --frozen.
  • dvc pull data/eval/phase-20.jsonl.dvc (y el corpus si es necesario). Afirma que dvc hash coincide con eval_baseline.json["eval_set_dvc_hash"].
  • mlflow artifacts download --run-id ${{ inputs.run_id }} --dst-path ./candidate/.
  • python -m minieval --bundle ./candidate/ --eval-set data/eval/phase-20.jsonl --output candidate_eval.json.
  • python -m scripts.mlops.compare_baseline --candidate candidate_eval.json --baseline eval_baseline.json --tolerance 0.02. Exit 0 = pass, no-cero = fail.
  • En pass: python -m scripts.mlops.registry promote --canonical-sha <derived from candidate> --semver <from PR comment>. Luego gh pr comment con el SHA promovido + semver. En fail: gh pr comment con los buckets que fallaron y sal no-cero.
  • Pinea cada acción a un SHA, no un tag (@v4@<sha>). Higiene de cadena de suministro (cross-reference security/supply-chain.md).

Bloque B — escribe la lógica de comparación

  • scripts/mlops/compare_baseline.py:
  • Carga candidate_eval.json y eval_baseline.json.
  • Para cada bucket en la baseline, chequea candidate.bucket.accuracy >= baseline.bucket.accuracy - tolerance_pp.
  • Si algún bucket falla, imprime una tabla formateada en markdown de los buckets que fallaron a stdout y sale con código 2.
  • Si todos los buckets pasan, imprime un resumen de una línea y sale 0.
  • Test-unitea: JSONs sintéticos de candidato + baseline, ambos casos de pass y fail. Los tests viven en tests/mlops/test_compare_baseline.py.

Bloque C — dry run con candidato limpio

  • Abre una PR titulada chore: deploy v0.3.1 (rerun of LoRA grammar tutor, no model change). Añade la etiqueta deploy-candidate. Comenta mlflow_run_id=<existing_lora_run_id> semver=v0.3.1.
  • Observa la corrida del workflow. Comportamiento esperado: la etapa 5 pasa (sin regresión vs baseline), la etapa 6 promueve. Confirma que el index.jsonl del registry tiene una nueva línea y que tags.json tiene el nuevo semver.
  • Captura la corrida verde de CI + el comentario-promoción del bot.
  • Guarda como pr_clean.md en el directorio del experimento.

Bloque D — inyección de regresión

  • Toma un modelo registrado existente (el tutor de gramática LoRA es buen candidato). Aplica una perturbación focalizada:
  • Opción 1 (más simple): envuelve el modelo en degraded_model/wrapper.py que machaca aleatoriamente el 10% de las salidas de participio pasado. Es más fácil que perturbar pesos pero es observable como regresión por el gate de evaluación.
  • Opción 2 (más profunda): añade ruido gaussiano a la matriz B del adaptador LoRA para el slice del decoder de participio pasado. Más realista pero más lento de inyectar.
  • Corre la evaluación localmente para confirmar: la precisión agregada puede mantenerse cercana al baseline, pero el bucket de participio pasado cae > 2pp.
  • Sube a MLflow como una nueva run; captura el run_id.

Bloque E — dry run con candidato regresado

  • Abre una PR titulada chore: deploy v0.3.2 (degraded — should fail CI). Añade la etiqueta deploy-candidate. Comenta mlflow_run_id=<degraded_run_id> semver=v0.3.2.
  • Observa la corrida del workflow. Comportamiento esperado: la etapa 5 falla porque tense.past_participle regresa más de 2pp. El bot comenta en la PR con el bucket que falló. La etapa 6 no se ejecuta. El registry no cambia.
  • Captura la corrida roja de CI + el comentario del bucket que falló.
  • Guarda como pr_regressed.md en el directorio del experimento.
  • Cierra la PR sin mergear — el punto es que el gate funcionó, no que queramos los cambios de esta PR en main.

Bloque F — recetas Justfile + manifest

  • Añade just register-model <run-id> <semver> para invocar el path de promoción local (modo dev) con un aviso de que el path de producción es CI.
  • Añade just compare-baseline <candidate.json> para dry-runear la comparación localmente.
  • manifest.json lista: el run_id limpio usado para el Bloque C, el run_id degradado usado para el Bloque E, el SHA de eval_baseline.json en el momento de cada PR.

Restricciones

  • Sin flag --force en el code path de producción. Si te encuentras añadiendo uno, para. El dev local tiene modo LYNX_ENV=dev; producción tiene CI. No hay tercer path.
  • GitHub Actions pineadas. Cada acción usada en el workflow está pineada por commit SHA, no por tag. Regla de cadena de suministro.
  • Sin secrets en el workflow. El acceso a MLflow usa secrets de GitHub Actions a nivel repo (configurados en los settings del repo, no commiteados). Documenta los nombres de los secrets en pr_clean.md.
  • La baseline está commiteada. eval_baseline.json es un fichero tracked. Actualizarlo es su propia PR.
  • Sin nuevo src/<module>/. compare_baseline.py y registry promote viven en scripts/mlops/.

Condiciones de parada

Hecho cuando:

  1. El fichero del workflow existe y es sintácticamente válido (gh workflow list lo muestra).
  2. La PR de candidato limpio cerró con corrida CI exitosa y una nueva entrada en el registry.
  3. La PR de candidato regresado cerró con corrida CI fallida y sin cambio en el registry.
  4. Ambas PRs tienen screenshots guardadas en el directorio del experimento.
  5. scripts/mlops/compare_baseline.py tiene tests unitarios, todos pasando.

Pitfalls

  • CI corre contra main, no la branch de la PR. GitHub Actions checkea la branch de la PR por defecto para eventos pull_request pero main para workflow_dispatch. Usa pull_request_target con cuidado — puede dar permisos excesivos. Default: trigger en pull_request: labeled y corre con permissions: { contents: read, pull-requests: write }.
  • Las descargas MLflow son lentas en cold cache. Cachea la descarga del artefacto con actions/cache keyado por run_id. Ahorra minutos por corrida.
  • DVC necesita auth. Si el remote DVC es privado (p. ej., S3), el workflow necesita credenciales cloud. Para el currículo, el remote es local; CI solo corre dvc fetch --remote local contra una copia bundleada con el workflow artifact. Documenta esto en el README del lab.
  • La baseline puede romperse silenciosamente. Si el hash DVC del eval set en eval_baseline.json no coincide con el corpus jalado, todas las comparaciones se hacen contra el eval equivocado. El workflow afirma este hash en la etapa 2 — verifica con un mismatch deliberado (commitea una baseline apuntando a un hash de eval obsoleto; confirma que CI falla rápido).
  • La promoción no es atómica entre el registry + MLflow. promote() actualiza tags.json y añade un tag MLflow. Si la mitad falla, tienes medio-estado. Implementa: escribe tags.json primero (local, atómico vía tmp+rename), luego añade el tag MLflow; ante fallo de MLflow, rollback del fichero local y reintenta. Documenta el modo-fallo en el README del lab.
  • Spam del bot de promoción en reintentos. Un workflow fallido re-corrido no debería promover dos veces. promote() es idempotente sobre (canonical_sha, semver) — verifica con un re-run.

Cuándo consultar solutions/

Tras los seis bloques. solutions/04-ci-deploy-gate-ref.md (apertura de fase) revisa tu diseño de workflow, los edge cases de la lógica de comparación, el pineado de acciones y la historia de rollback ante fallo.


Este es el último lab de la Fase 38. Tras completar los cinco (00–04), tienes:

  • Un registry con SHAs canónicos estables sobre MLflow + DVC.
  • Routing shadow + A/B cableado en el stack de servicio (sin nuevo módulo src).
  • Un detector de drift con umbrales calibrados.
  • Una tabla CpQU por modelo registrado.
  • Un gate de CI que rechaza promover una regresión.

Juntos: la columna vertebral MLOps para el capstone de la Fase 39.

Siguiente: reporte de Fase 38 → capstone de Fase 39.