Files
fn_registry/docs/execution_standard.md
egutierrez d7f2c00d7b feat: externalize apps/analysis to Gitea repos, add analysis table
- Migration 007: repo_url on apps table + analysis table with FTS5
- Analysis struct, parser, CRUD, validation, hash computation
- Selective purge: remote-only apps/analysis preserved across fn index
- CLI: fn app list/clone/pull, fn analysis list/clone/pull
- search/show/list now include analysis results
- Apps removed from git tracking (content lives in Gitea repos)
- .gitkeep for apps/ and analysis/ dirs
- Bash functions: jupyter analysis pipeline, shell utilities
- Browser domain: CDP functions moved from infra to browser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 04:23:51 +02:00

385 lines
12 KiB
Markdown

# Estandar de Ejecucion: YAML + Exit Codes + Hooks
Contrato comun para apps, pipelines y automatizaciones del fn-registry. Define como declarar flujos en YAML, que exit codes devolver, como reportar resultados y como integrarse con Dagu y operations.db.
---
## Motivacion
Tenemos dos patrones que ya funcionan:
- **Dagu**: orquesta DAGs con `command`/`script`, usa exit codes para decidir flujo, tiene handlers globales y scheduling cron.
- **script_navegador**: ejecuta pasos tipados (CDP actions) desde YAML, registra todo en operations.db, sale con 0/1.
Ambos comparten la misma idea: **YAML declara el flujo, exit code señala el resultado, logs estructurados permiten analisis posterior**. Este documento formaliza ese contrato para que cualquier app nueva lo siga desde el inicio.
---
## 1. Estructura YAML
Toda app que ejecute un flujo debe leer un YAML con esta estructura minima:
```yaml
# Cabecera obligatoria
name: nombre_del_flujo
description: "Que hace este flujo" # opcional pero recomendado
# Variables de entorno disponibles para todos los pasos
env: # opcional
KEY: value
# Pasos del flujo
steps:
- name: paso_1 # identificador unico dentro del flujo
action: tipo_de_accion # dispatch key (navigate, command, query, etc.)
# ... parametros especificos del action
continue_on_error: false # default: false
timeout_ms: 30000 # default: 30s, 0 = sin timeout
depends: [] # IDs de pasos previos (vacio = secuencial)
# Hooks opcionales
hooks:
on_success: "comando o script" # se ejecuta si exit code = 0
on_failure: "comando o script" # se ejecuta si exit code = 1
on_partial: "comando o script" # se ejecuta si exit code = 2
```
### Campos de la cabecera
| Campo | Requerido | Descripcion |
|-------|-----------|-------------|
| `name` | si | Identificador del flujo (snake_case) |
| `description` | no | Descripcion legible |
| `env` | no | Variables de entorno (mapa key-value) |
| `steps` | si | Lista de pasos a ejecutar |
| `hooks` | no | Comandos a ejecutar segun resultado |
### Campos de cada step
| Campo | Requerido | Default | Descripcion |
|-------|-----------|---------|-------------|
| `name` | si | — | ID unico del paso dentro del flujo |
| `action` | si | — | Tipo de accion (cada app define sus propios actions) |
| `continue_on_error` | no | `false` | Si `true`, un fallo no detiene el flujo |
| `timeout_ms` | no | `30000` | Timeout del paso en ms (0 = sin timeout) |
| `depends` | no | `[]` | Pasos que deben completarse antes |
Los campos adicionales dependen del `action` y los define cada app/dominio.
### Actions por dominio
Cada app registra sus actions validos. Ejemplos:
| Dominio | Actions | App de referencia |
|---------|---------|-------------------|
| navegacion | `navigate`, `click`, `type`, `wait`, `screenshot`, `evaluate`, `get_html`, `wait_load`, `sleep` | script_navegador |
| shell | `command`, `script` | Dagu / apps genericas |
| database | `query`, `migrate`, `backup` | futuras apps |
| http | `request`, `assert_status`, `extract` | futuras apps |
---
## 2. Exit Codes
Toda app/script que siga este estandar DEBE salir con uno de estos tres codigos:
| Codigo | Constante | Significado |
|--------|-----------|-------------|
| `0` | `EXIT_SUCCESS` | Todos los pasos completaron sin error |
| `1` | `EXIT_FAILURE` | Al menos un paso fallo y el flujo aborto |
| `2` | `EXIT_PARTIAL` | Algunos pasos fallaron pero el flujo continuo (continue_on_error) |
### Reglas
- Si todos los pasos son exitosos → `0`
- Si algun paso falla y tiene `continue_on_error: false``1` (aborta)
- Si algun paso falla pero todos los fallos tenian `continue_on_error: true``2`
- Un timeout agotado cuenta como fallo del paso
### Compatibilidad con Dagu
Dagu interpreta exit code != 0 como fallo. Para que `2` (partial) no dispare el handler de failure en Dagu, el DAG que llama a la app puede usar:
```yaml
steps:
- name: run_app
command: ./mi_app --script flujo.yaml
continue_on:
failure: true # no abortar el DAG por parcial
- name: check_result
command: test $? -le 1 # falla solo si fue error fatal
depends: [run_app]
```
O alternativamente, la app puede configurarse con `--strict` para mapear partial → failure (exit 1).
---
## 3. Output Estructurado
Toda app DEBE escribir un **resumen JSON en stdout** al finalizar (despues de su output humano). El formato:
```json
{
"name": "nombre_del_flujo",
"status": "success|failure|partial",
"exit_code": 0,
"started_at": "2026-04-01T10:00:00Z",
"ended_at": "2026-04-01T10:00:05Z",
"duration_ms": 5000,
"steps_total": 5,
"steps_ok": 4,
"steps_failed": 1,
"steps": [
{
"name": "paso_1",
"action": "navigate",
"status": "ok",
"elapsed_ms": 1200,
"output": "optional output"
},
{
"name": "paso_2",
"action": "click",
"status": "error",
"elapsed_ms": 500,
"error": "elemento no encontrado: #btn-submit"
}
]
}
```
### Separacion humano / maquina
- **stderr**: logs legibles para humanos (progreso, warnings)
- **stdout**: output JSON estructurado al final (para Dagu, hooks, pipes)
- Flag `--json` o `--quiet`: suprime output humano, solo JSON en stdout
Esto permite que Dagu capture el JSON:
```yaml
steps:
- name: navegacion
command: ./script_navegador --script busqueda.yaml --json
output: RESULT
- name: analizar
command: echo "$RESULT" | jq '.steps[] | select(.status=="error")'
depends: [navegacion]
```
---
## 4. Hooks
Los hooks se ejecutan **despues** de todos los pasos, segun el exit code resultante.
### En el YAML del flujo
```yaml
hooks:
on_success: "notify-send 'Flujo completado'"
on_failure: "echo 'FALLO: {{.Name}}' >> /tmp/failures.log"
on_partial: "echo 'PARCIAL: {{.StepsFailed}} pasos fallaron' >> /tmp/warnings.log"
on_step_error: "echo 'Step {{.StepName}} fallo: {{.Error}}'" # se ejecuta por cada paso fallido
```
### Variables disponibles en hooks
| Variable | Tipo | Descripcion |
|----------|------|-------------|
| `{{.Name}}` | string | Nombre del flujo |
| `{{.Status}}` | string | success, failure, partial |
| `{{.ExitCode}}` | int | 0, 1, 2 |
| `{{.DurationMs}}` | int | Duracion total en ms |
| `{{.StepsTotal}}` | int | Total de pasos |
| `{{.StepsOk}}` | int | Pasos exitosos |
| `{{.StepsFailed}}` | int | Pasos fallidos |
| `{{.StepName}}` | string | Nombre del paso (solo en on_step_error) |
| `{{.Error}}` | string | Mensaje de error (solo en on_step_error/on_failure) |
### En Dagu (handlers globales)
Los hooks del YAML son locales al flujo. Para orquestacion, Dagu maneja sus propios handlers:
```yaml
# dags/mi_automatizacion.yaml
name: mi_automatizacion
handlers:
failure:
- name: alerta
command: echo "Fallo en mi_automatizacion" >> ~/dagu/logs/failures.log
success:
- name: registrar
command: echo "OK $(date)" >> ~/dagu/logs/success.log
steps:
- name: ejecutar
command: ./fn run mi_pipeline --script flujo.yaml
```
---
## 5. Integracion con operations.db
Las apps que siguen el bucle reactivo DEBEN registrar la ejecucion en operations.db:
### Mapping del estandar a operations.db
| Concepto estandar | Tabla operations.db | Campo |
|-------------------|---------------------|-------|
| Flujo completo | `executions` | `status`: success/failure/partial |
| Cada paso | `logs` | `level`: info/error, `message`: resultado |
| Exit code | `executions.metrics` | `{"exit_code": N}` |
| Output JSON | `executions.metrics` | `{"steps": [...]}` |
| Duracion | `executions.duration_ms` | milisegundos |
| Pasos in/out | `executions.records_in/out` | total/exitosos |
### Patron de registro (referencia: script_navegador/ops.go)
```
1. initOpsDB() → abrir/crear operations.db
2. EnsureEntities() → registrar recursos involucrados
3. EnsureRelations() → registrar como se conectan
4. [ejecutar flujo]
5. RecordRun() → insertar execution con metricas
6. LogStep() por paso → insertar log por cada paso
7. UpdateRelation() → status final de la relation
```
Este patron ya esta implementado en `script_navegador/ops.go` y sirve como referencia para nuevas apps.
---
## 6. Flujo completo: App → Dagu → Hooks
```
Dagu (scheduler)
cron / manual
┌────▼────┐
│ DAG │ dags/mi_dag.yaml
│ step │ command: ./mi_app --script flujo.yaml --json
└────┬────┘
┌────▼────────────────┐
│ App (mi_app) │
│ │
│ 1. Load YAML │
│ 2. Validate steps │
│ 3. Init ops.db │
│ 4. Execute steps │
│ 5. Run hooks │
│ 6. Record to ops │
│ 7. JSON → stdout │
│ 8. Exit code │
└────┬────────────────┘
┌──────────┼──────────┐
│ │ │
exit 0 exit 1 exit 2
success failure partial
│ │ │
▼ ▼ ▼
Dagu OK Dagu FAIL Dagu FAIL*
handler handler (configurable)
```
*Con `continue_on: failure: true` en el step de Dagu, exit 2 no aborta el DAG.
---
## 7. Implementacion: funciones del registry
### Existentes (ya en uso por script_navegador)
Todas las funciones CDP del registry (`cdp_*_go_browser`, `chrome_launch_go_browser`) ya siguen el patron de retornar error. El runner de script_navegador las compone.
### Nuevas (por construir)
| Funcion | Lang | Domain | Descripcion |
|---------|------|--------|-------------|
| `run_steps` | bash | shell | Ejecuta pasos de un YAML generico (action=command/script), reporta JSON y sale con 0/1/2 |
| `report_execution_json` | bash | shell | Genera el JSON de salida estandar dado los resultados de pasos |
| `exit_with_status` | bash | shell | Calcula exit code (0/1/2) a partir de contadores ok/fail/partial |
Estas funciones Bash permiten que cualquier script nuevo siga el estandar sin reimplementar la logica de reporte y exit codes.
---
## 8. Ejemplo completo
### Script YAML (flujo.yaml)
```yaml
name: backup_db
description: "Backup de operations.db y push a remoto"
steps:
- name: check_db
action: command
command: "test -f operations.db"
- name: backup
action: command
command: "cp operations.db backups/operations_$(date +%Y%m%d).db"
depends: [check_db]
- name: push
action: command
command: "git add backups/ && git commit -m 'backup' && git push"
depends: [backup]
continue_on_error: true
hooks:
on_failure: "echo 'Backup fallo' >> /tmp/backup_errors.log"
```
### Invocacion desde Dagu
```yaml
name: backup_diario
schedule: "0 2 * * *"
steps:
- name: backup
command: ./fn run backup_db --script flujo.yaml --json
working_dir: /home/lucas/fn_registry/apps/mi_app
handlers:
failure:
- name: alerta
command: echo "Backup fallo $(date)" >> ~/dagu/logs/failures.log
```
### Output esperado (stdout)
```json
{
"name": "backup_db",
"status": "partial",
"exit_code": 2,
"started_at": "2026-04-01T02:00:00Z",
"ended_at": "2026-04-01T02:00:03Z",
"duration_ms": 3000,
"steps_total": 3,
"steps_ok": 2,
"steps_failed": 1,
"steps": [
{"name": "check_db", "action": "command", "status": "ok", "elapsed_ms": 10},
{"name": "backup", "action": "command", "status": "ok", "elapsed_ms": 450},
{"name": "push", "action": "command", "status": "error", "elapsed_ms": 2540, "error": "remote rejected"}
]
}
```
Exit code: `2` (partial — push fallo pero tenia continue_on_error).
---
## Resumen del contrato
| Aspecto | Regla |
|---------|-------|
| Formato de flujo | YAML con `name` + `steps[]` |
| Exit codes | 0=success, 1=failure, 2=partial |
| Output maquina | JSON en stdout (con `--json`) |
| Output humano | stderr (progreso, warnings) |
| Error handling | `continue_on_error` por paso, hooks por resultado |
| Trazabilidad | operations.db (executions + logs) |
| Orquestacion | Dagu como scheduler, apps como ejecutores |
| Funciones | Reutilizar del registry, actions por dominio |