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>
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
# 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 |
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: my_analysis
|
||||
lang: py
|
||||
domain: datascience
|
||||
description: "Descripcion breve del analisis."
|
||||
tags: []
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
framework: "jupyterlab"
|
||||
entry_point: "notebooks/main.ipynb"
|
||||
dir_path: "analysis/my_analysis"
|
||||
repo_url: ""
|
||||
---
|
||||
|
||||
## Notas
|
||||
|
||||
Notas adicionales sobre el analisis.
|
||||
Reference in New Issue
Block a user