From a8f3c356d2d6c4fd9f078993ab380e4b30c5ddd7 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 13 Apr 2026 01:17:14 +0200 Subject: [PATCH 1/3] feat: funcion docker_compose_remote_deploy bash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Funcion bash que despliega un stack Docker Compose en un host remoto via SSH. Flujo: verificar SSH → git pull → docker-compose pull → docker-compose up -d → listar containers. Soporta compose files adicionales y retorna JSON con status, containers y duracion. --- .../infra/docker_compose_remote_deploy.md | 64 +++++++++++++++ .../infra/docker_compose_remote_deploy.sh | 81 +++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 bash/functions/infra/docker_compose_remote_deploy.md create mode 100644 bash/functions/infra/docker_compose_remote_deploy.sh diff --git a/bash/functions/infra/docker_compose_remote_deploy.md b/bash/functions/infra/docker_compose_remote_deploy.md new file mode 100644 index 00000000..52f02847 --- /dev/null +++ b/bash/functions/infra/docker_compose_remote_deploy.md @@ -0,0 +1,64 @@ +--- +name: docker_compose_remote_deploy +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "docker_compose_remote_deploy(host: string, remote_dir: string, branch: string, compose_files: string) -> json" +description: "Despliega un stack Docker Compose en un host remoto via SSH. Verifica conectividad, hace git pull del branch indicado, actualiza imagenes con docker-compose pull y levanta/recrea los servicios modificados con docker-compose up -d. Soporta compose files adicionales. Retorna JSON con status, containers corriendo y duracion." +tags: [docker, compose, deploy, ssh, remote, git, infra, cicd] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: host + desc: "alias SSH del host remoto definido en ~/.ssh/config (ej: prod-server)" + - name: remote_dir + desc: "ruta absoluta en el host donde esta el repo con docker-compose.yml (ej: /opt/apps/element)" + - name: branch + desc: "branch de git a hacer pull; default 'main'" + - name: compose_files + desc: "archivos compose adicionales separados por coma (ej: 'docker-compose.livekit.yml,docker-compose.monitoring.yml'); si vacio usa solo docker-compose.yml" +output: "JSON con status ('ok'), host, remote_dir, branch, containers (array de nombres corriendo tras el deploy), duration_ms" +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/docker_compose_remote_deploy.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/docker_compose_remote_deploy.sh + +# Deploy basico (solo docker-compose.yml, branch main) +result=$(docker_compose_remote_deploy "prod-server" "/opt/apps/element") +echo "$result" +# {"status":"ok","host":"prod-server","remote_dir":"/opt/apps/element","branch":"main","containers":["element-web","synapse","postgres"],"duration_ms":4200} + +# Deploy con compose files adicionales y branch especifico +result=$(docker_compose_remote_deploy "prod-server" "/opt/apps/element" "release" "docker-compose.livekit.yml,docker-compose.monitoring.yml") +echo "$result" +# {"status":"ok","host":"prod-server","remote_dir":"/opt/apps/element","branch":"release","containers":[...],"duration_ms":8100} + +# Uso desde un pipeline CI/CD +source bash/functions/infra/docker_compose_remote_deploy.sh +docker_compose_remote_deploy "$SSH_HOST" "$REMOTE_DIR" "$GIT_BRANCH" "$EXTRA_COMPOSE" || exit 1 +``` + +## Notas + +- Flujo: verificar SSH → git pull → docker-compose pull → docker-compose up -d → listar containers. +- La verificacion SSH usa `-o BatchMode=yes -o ConnectTimeout=5` para fallar rapido sin pedir password. +- Los compose files adicionales se pasan como `-f file1.yml -f file2.yml` a todos los subcomandos compose. +- `docker-compose up -d` solo recrea los servicios cuya imagen o config cambio (comportamiento nativo de compose). +- La lista de containers al final incluye TODOS los containers corriendo en el host, no solo los del stack. +- Requiere `jq` instalado en el host remoto para serializar la lista de containers. Si no esta, `containers` sera `[]`. +- Los mensajes de progreso van a stderr; el JSON final va a stdout. +- Exit code 1 en cualquier fallo (SSH, git pull, compose pull, compose up); el JSON de error NO se emite — el caller debe manejar el exit code. +- El `host` se resuelve con `~/.ssh/config` incluyendo host, user, identityfile y puerto. +- Diferencia con `rsync_deploy`: este flujo asume que el codigo ya esta en el remoto (via git) y usa compose. `rsync_deploy` sube archivos locales sin git. diff --git a/bash/functions/infra/docker_compose_remote_deploy.sh b/bash/functions/infra/docker_compose_remote_deploy.sh new file mode 100644 index 00000000..abf10c40 --- /dev/null +++ b/bash/functions/infra/docker_compose_remote_deploy.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# docker_compose_remote_deploy — Despliega un stack Docker Compose en un host remoto via SSH +set -euo pipefail + +docker_compose_remote_deploy() { + local host="$1" + local remote_dir="$2" + local branch="${3:-main}" + local compose_files="${4:-}" + + if [[ -z "$host" || -z "$remote_dir" ]]; then + echo "docker_compose_remote_deploy: se requieren host y remote_dir" >&2 + return 1 + fi + + local start_ts + start_ts=$(date +%s) + + # 1. Verificar conectividad SSH + echo "docker_compose_remote_deploy: verificando conectividad SSH a '$host'..." >&2 + if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" true 2>/dev/null; then + echo "docker_compose_remote_deploy: no se puede conectar a '$host' via SSH" >&2 + return 1 + fi + + # 2. Git pull en el host remoto + echo "docker_compose_remote_deploy: git pull origin $branch en '$remote_dir'..." >&2 + if ! ssh "$host" "cd '$remote_dir' && git pull origin '$branch'" >&2; then + echo "docker_compose_remote_deploy: git pull falló en '$host:$remote_dir'" >&2 + return 1 + fi + + # 3. Construir los argumentos -f para docker-compose + local compose_args="-f docker-compose.yml" + if [[ -n "$compose_files" ]]; then + local IFS="," + local extra_file + for extra_file in $compose_files; do + extra_file="${extra_file// /}" # trim spaces + if [[ -n "$extra_file" ]]; then + compose_args="$compose_args -f $extra_file" + fi + done + unset IFS + fi + + # 4. docker-compose pull + echo "docker_compose_remote_deploy: actualizando imagenes ($compose_args)..." >&2 + if ! ssh "$host" "cd '$remote_dir' && docker-compose $compose_args pull" >&2; then + echo "docker_compose_remote_deploy: docker-compose pull falló en '$host:$remote_dir'" >&2 + return 1 + fi + + # 5. docker-compose up -d + echo "docker_compose_remote_deploy: levantando servicios ($compose_args)..." >&2 + if ! ssh "$host" "cd '$remote_dir' && docker-compose $compose_args up -d" >&2; then + echo "docker_compose_remote_deploy: docker-compose up -d falló en '$host:$remote_dir'" >&2 + return 1 + fi + + # 6. Recopilar containers corriendo tras el deploy + local containers_json + containers_json=$(ssh "$host" \ + "docker ps --format '{{.Names}}' 2>/dev/null | jq -R . | jq -sc ." 2>/dev/null || echo '[]') + + local end_ts + end_ts=$(date +%s) + local duration_ms=$(( (end_ts - start_ts) * 1000 )) + + # Emitir JSON a stdout + printf '{"status":"ok","host":"%s","remote_dir":"%s","branch":"%s","containers":%s,"duration_ms":%d}\n' \ + "$host" \ + "$remote_dir" \ + "$branch" \ + "$containers_json" \ + "$duration_ms" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + docker_compose_remote_deploy "$@" +fi From 1a0f9b5f86045e5338d1d2162206fc9781da867b Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 13 Apr 2026 01:17:19 +0200 Subject: [PATCH 2/3] fix: mejoras en jupyter launcher y kernel startup write_jupyter_launcher ahora exporta IPYTHONDIR al directorio local .ipython/ para que el kernel cargue el startup correcto cuando se ejecuta desde projects/. write_jupyter_registry_kernel usa descubrimiento inteligente de FN_REGISTRY_ROOT: prioriza env var, luego path hardcoded, luego sube desde CWD buscando registry.db. Esto permite que analyses dentro de projects/ encuentren el registry automaticamente. --- .../functions/infra/write_jupyter_launcher.sh | 5 +++++ .../infra/write_jupyter_registry_kernel.sh | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bash/functions/infra/write_jupyter_launcher.sh b/bash/functions/infra/write_jupyter_launcher.sh index afcb0aef..08880300 100644 --- a/bash/functions/infra/write_jupyter_launcher.sh +++ b/bash/functions/infra/write_jupyter_launcher.sh @@ -34,6 +34,11 @@ echo $PORT > .jupyter-port source .venv/bin/activate 2>/dev/null || true +# IPython startup: cargar .ipython/ local (FN_REGISTRY_ROOT, helpers, sys.path) +if [ -d "$(pwd)/.ipython" ]; then + export IPYTHONDIR="$(pwd)/.ipython" +fi + if ! python -c "import jupyter_collaboration" 2>/dev/null; then echo "ERROR: jupyter-collaboration no esta instalado" echo "Instala con: uv add jupyter-collaboration" diff --git a/bash/functions/infra/write_jupyter_registry_kernel.sh b/bash/functions/infra/write_jupyter_registry_kernel.sh index 79d46b83..e7a85379 100644 --- a/bash/functions/infra/write_jupyter_registry_kernel.sh +++ b/bash/functions/infra/write_jupyter_registry_kernel.sh @@ -33,7 +33,24 @@ import sqlite3 from pathlib import Path # ── FN_REGISTRY_ROOT ──────────────────────────────────────── -FN_REGISTRY_ROOT = Path("${registry_root}") +# Prioridad: env var > path hardcoded > descubrimiento automatico +def _discover_registry_root(): + if os.environ.get("FN_REGISTRY_ROOT"): + return Path(os.environ["FN_REGISTRY_ROOT"]).resolve() + hardcoded = Path("${registry_root}") + if (hardcoded / "registry.db").exists(): + return hardcoded + # Subir desde CWD hasta encontrar registry.db + p = Path.cwd() + for _ in range(10): + if (p / "registry.db").exists(): + return p + if p.parent == p: + break + p = p.parent + return hardcoded + +FN_REGISTRY_ROOT = _discover_registry_root() os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT) # ── sys.path: importar funciones Python del registry ──────── From 2c7edba69369d0cc43e15007d26b36807e741c22 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 13 Apr 2026 01:17:25 +0200 Subject: [PATCH 3/3] docs: regla projects, estructura projects/vaults, registry.db Nueva regla projects.md que documenta como agrupar apps, analyses y vaults bajo un tema comun en projects/{nombre}/. Actualiza INDEX.md con la entrada #15. Crea directorios projects/ y vaults/ con .gitkeep (contenido real gitignored). registry.db regenerado con los cambios del indice. --- .claude/rules/INDEX.md | 1 + .claude/rules/projects.md | 90 +++++++++++++++++++++++++++++++++++++++ projects/.gitkeep | 0 vaults/.gitkeep | 0 4 files changed, 91 insertions(+) create mode 100644 .claude/rules/projects.md create mode 100644 projects/.gitkeep create mode 100644 vaults/.gitkeep diff --git a/.claude/rules/INDEX.md b/.claude/rules/INDEX.md index 3182630a..d5f982e9 100644 --- a/.claude/rules/INDEX.md +++ b/.claude/rules/INDEX.md @@ -18,3 +18,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente. | 12 | [notebook_collaboration.md](notebook_collaboration.md) | Colaboración en notebooks Jupyter via funciones del registry | | 13 | [frontend_theming.md](frontend_theming.md) | Componentes propios y sistema de temas en frontends | | 14 | [deploy.md](deploy.md) | Deploy de apps a VPS remotos via SSH + systemd + rsync | +| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema | diff --git a/.claude/rules/projects.md b/.claude/rules/projects.md new file mode 100644 index 00000000..1835f238 --- /dev/null +++ b/.claude/rules/projects.md @@ -0,0 +1,90 @@ +## Projects: apps, analysis y vaults bajo un tema comun + +Un project agrupa apps, analyses y vaults relacionados. Vive en `projects/{nombre}/` con esta estructura: + +``` +projects/{nombre}/ + project.md # Frontmatter obligatorio (name, description, tags) + apps/ # Apps del proyecto (cada una con app.md) + {app_name}/ + app.md + ... + analysis/ # Analyses del proyecto (cada uno con analysis.md) + {analysis_name}/ + analysis.md + .venv/ + notebooks/ + run-jupyter-lab.sh + ... + vaults/ # Datos del proyecto + vault.yaml # Manifest de vaults (nombre, descripcion, path, tags) + {vault_name} -> /abs/path # Symlinks a directorios reales de datos +``` + +### Reglas + +- `project.md` sigue el template de `docs/templates/project.md` — campos: `name`, `description`, `tags`, `repo_url` +- `analysis.md` sigue el template de `docs/templates/analysis.md` — `dir_path` debe apuntar a `projects/{nombre}/analysis/{tema}/` +- `vault.yaml` lista los vaults con nombre, descripcion, path absoluto y tags +- Los vaults reales viven fuera del repo (ej: `~/vaults/{nombre}/`) con symlinks en el proyecto +- `fn index` escanea `projects/*/` y setea `project_id` automaticamente en apps, analyses y vaults +- Apps y analyses sueltos (sin proyecto) siguen en `apps/` y `analysis/` en la raiz + +### Raiz vs proyecto + +| Ubicacion | Para que | +|-----------|---------| +| `apps/` | Apps independientes que no pertenecen a ningun proyecto | +| `analysis/` | Analyses independientes | +| `projects/{nombre}/apps/` | Apps de un proyecto — `project_id` se setea automaticamente | +| `projects/{nombre}/analysis/` | Analyses de un proyecto — `project_id` se setea automaticamente | + +### Crear un proyecto nuevo + +```bash +# 1. Crear estructura +mkdir -p projects/{nombre}/{apps,analysis,vaults} + +# 2. Crear project.md con frontmatter +fn add -k project # genera template + +# 3. Crear vault (datos fuera del repo, symlink dentro) +mkdir -p ~/vaults/{vault_name}/{raw,processed,exports} +ln -s ~/vaults/{vault_name} projects/{nombre}/vaults/{vault_name} +# Crear vault.yaml con la entrada + +# 4. Crear analysis dentro del proyecto +fn run init_jupyter_analysis {nombre_analysis} [paquetes...] +mv analysis/{nombre_analysis} projects/{nombre}/analysis/ +# Crear analysis.md con dir_path correcto +# Regenerar launcher y kernel startup: +source bash/functions/infra/write_jupyter_launcher.sh && write_jupyter_launcher projects/{nombre}/analysis/{tema} +source bash/functions/infra/write_jupyter_registry_kernel.sh && write_jupyter_registry_kernel projects/{nombre}/analysis/{tema} + +# 5. Indexar +fn index +fn show {nombre} # verifica el project y sus componentes +``` + +### Consultas utiles + +```sql +-- Listar proyectos +SELECT id, description FROM projects; + +-- Analysis de un proyecto +SELECT id, name, dir_path FROM analysis WHERE project_id = 'app_turismo'; + +-- Vaults de un proyecto +SELECT id, name, path, symlink FROM vaults WHERE project_id = 'app_turismo'; + +-- Apps de un proyecto +SELECT id, name, dir_path FROM apps WHERE project_id = 'app_turismo'; + +-- Todo lo que pertenece a un proyecto +SELECT 'analysis' as tipo, id, name FROM analysis WHERE project_id = ? +UNION ALL +SELECT 'vault', id, name FROM vaults WHERE project_id = ? +UNION ALL +SELECT 'app', id, name FROM apps WHERE project_id = ?; +``` diff --git a/projects/.gitkeep b/projects/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/vaults/.gitkeep b/vaults/.gitkeep new file mode 100644 index 00000000..e69de29b