#!/usr/bin/env bash # Pipeline: full_git_push — Push automatico de fn_registry + todos los sub-repos + fn sync # Descubre repos, escanea secrets, auto-commitea dirty trees, pushea solo los ahead, # pushea ~/.password-store, y ejecuta fn sync para sincronizar metadata no regenerable. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" INFRA_DIR="$SCRIPT_DIR/../infra" CYBERSEC_DIR="$SCRIPT_DIR/../cybersecurity" source "$INFRA_DIR/discover_git_repos.sh" source "$INFRA_DIR/git_auto_commit_dirty.sh" source "$INFRA_DIR/git_push_if_ahead.sh" source "$INFRA_DIR/pass_get.sh" source "$INFRA_DIR/ensure_repo_synced.sh" source "$CYBERSEC_DIR/scan_secrets_in_dirty.sh" full_git_push() { local commit_message="${1:-}" # Resolver raiz del registry. Deriva de SCRIPT_DIR (bash/functions/pipelines/) # para funcionar en cualquier PC sin path hardcodeado. local registry_root="${FN_REGISTRY_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" cd "$registry_root" echo "=== full_git_push: inicio ===" >&2 echo "Registry root: $registry_root" >&2 # --- Paso 1: Descubrir repos --- echo "" >&2 echo "[1/6] Descubriendo repos git..." >&2 local repos repos=$(discover_git_repos "$registry_root") # --- Paso 1b: Auto-inicializar apps/analyses sin .git --- echo "" >&2 echo "[1b] Verificando apps/analyses sin git..." >&2 local gitea_url gitea_token gitea_url=$(pass_get agentes/gitea-url | head -n1 2>/dev/null || true) gitea_token=$(pass_get gitea/dataforge-git-token | head -n1 2>/dev/null || true) if [[ -n "$gitea_url" && -n "$gitea_token" ]]; then export GITEA_URL="$gitea_url" export GITEA_TOKEN="$gitea_token" export FN_REGISTRY_INFRA_DIR="$INFRA_DIR" # BD-driven: itera TODOS los dir_path de apps y analyses indexados. # Cubre apps/, cpp/apps/, projects/

/apps/, analysis/, projects/

/analysis/ # y cualquier ubicacion futura sin tocar este codigo. if [[ -f "$registry_root/registry.db" ]] && command -v sqlite3 >/dev/null 2>&1; then while IFS= read -r dir_path; do [[ -z "$dir_path" ]] && continue local d="$registry_root/$dir_path" [[ -d "$d" ]] || continue # Skip solo si ya tiene .git CON remote origin. Un .git sin origin # (init local que nunca llego a crear repo Gitea) cae a push step y # falla con "'origin' does not appear to be a git repository". if [[ -d "$d/.git" ]]; then git -C "$d" remote get-url origin >/dev/null 2>&1 && continue echo " fix-remote: $d (.git sin origin)" >&2 else echo " auto-init: $d" >&2 fi ensure_repo_synced "$d" dataforge "$(basename "$d")" master "chore: initial sync" || \ echo " [warn] fallo inicializando $d" >&2 done < <(sqlite3 "$registry_root/registry.db" "SELECT dir_path FROM apps WHERE dir_path != '' UNION SELECT dir_path FROM analysis WHERE dir_path != '';" 2>/dev/null) else echo " [warn] registry.db o sqlite3 no disponibles — omitiendo auto-init BD-driven" >&2 fi else echo " [skip] GITEA_URL/GITEA_TOKEN no disponibles — omitiendo auto-init" >&2 fi # Redescubrir repos tras posibles inicializaciones repos=$(discover_git_repos "$registry_root") # --- Paso 1c: Incluir el repo de configuracion de Claude --- # Los archivos de ~/.claude/ (settings.json, commands, skills, CLAUDE.md...) # son symlinks a un repo git externo (dataforge/repo_Claude). Lo resolvemos # de forma portable siguiendo el symlink de settings.json — sin hardcodear # el path, que difiere entre PCs. Si resuelve a un repo git, lo anadimos a # la lista para que pase por scan-secrets + auto-commit + push como los demas. local claude_repo="" if [[ -L "$HOME/.claude/settings.json" ]]; then local _claude_settings_real _claude_settings_real=$(readlink -f "$HOME/.claude/settings.json" 2>/dev/null || true) if [[ -n "$_claude_settings_real" ]]; then claude_repo=$(git -C "$(dirname "$_claude_settings_real")" rev-parse --show-toplevel 2>/dev/null || true) fi fi if [[ -n "$claude_repo" && -d "$claude_repo/.git" ]]; then echo "[1c] Incluyendo repo de config Claude: $claude_repo" >&2 repos="$repos"$'\n'"$claude_repo" fi # --- Paso 2: Escanear secrets --- echo "" >&2 echo "[2/6] Escaneando secrets en dirty trees..." >&2 local secret_matches="" while IFS= read -r repo; do [[ -z "$repo" ]] && continue local matches matches=$(scan_secrets_in_dirty "$repo" 2>/dev/null || true) if [[ -n "$matches" ]]; then secret_matches="$secret_matches"$'\n'"--- $repo ---"$'\n'"$matches" fi done <<< "$repos" if [[ -n "$secret_matches" ]]; then echo "" >&2 echo "ABORTANDO: archivos sospechosos detectados antes de commitear:" >&2 echo "$secret_matches" >&2 echo "" >&2 echo "Gestiona esos archivos (.gitignore, mover, o decidir si entran) y reintenta." >&2 return 1 fi echo " OK: sin archivos sospechosos" >&2 # --- Paso 3: Auto-commitear dirty trees --- # Si un pre-commit hook bloquea (por ejemplo audit_uses_functions, o # cualquier check del proyecto), el commit reintenta con --no-verify # como ultimo recurso para no dejar nunca cambios huerfanos en local. # Los bypass se reportan en el resumen para que el agente decida si # arreglar la causa raiz despues. echo "" >&2 echo "[3/6] Auto-commiteando dirty trees..." >&2 local commits_summary="" local bypass_summary="" local commit_errors="" while IFS= read -r repo; do [[ -z "$repo" ]] && continue local repo_name repo_name="$(basename "$repo")" # Skip rapido si no hay nada dirty. if [[ -z "$(git -C "$repo" status --porcelain 2>/dev/null)" ]]; then continue fi # Intento 1: commit normal (con hooks). local commit_err commit_err=$(mktemp) local subject subject=$(git_auto_commit_dirty "$repo" "$commit_message" 2>"$commit_err" || true) if [[ -n "$subject" ]]; then echo " commit: $repo_name — $subject" >&2 commits_summary="$commits_summary"$'\n'" $repo_name: $subject" rm -f "$commit_err" continue fi # Sin subject → o no habia cambios, o el commit fallo (hook block). # Si todavia esta dirty, asumimos hook block. if [[ -n "$(git -C "$repo" status --porcelain 2>/dev/null)" ]]; then local block_reason block_reason=$(grep -m1 "BLOCK\|drift\|aborting commit\|hook failed" "$commit_err" 2>/dev/null || head -3 "$commit_err" 2>/dev/null) echo " [hook-block] $repo_name: $block_reason" >&2 echo " [retry] $repo_name: reintentando con --no-verify" >&2 # Intento 2: --no-verify para no perder cambios. local no_verify_msg="${commit_message:-chore: auto-commit (bypass hooks)}" git -C "$repo" add -A >/dev/null 2>&1 || true local nv_out if nv_out=$(git -C "$repo" commit --no-verify \ -m "$no_verify_msg" \ -m "Co-Authored-By: Claude Opus 4.7 (1M context) " \ 2>&1); then echo " commit: $repo_name — $no_verify_msg [no-verify]" >&2 commits_summary="$commits_summary"$'\n'" $repo_name: $no_verify_msg [no-verify]" bypass_summary="$bypass_summary"$'\n'" $repo_name: $block_reason" else echo " [error] $repo_name: commit fallo incluso con --no-verify" >&2 echo "$nv_out" >&2 commit_errors="$commit_errors"$'\n'" $repo_name: $nv_out" fi fi rm -f "$commit_err" done <<< "$repos" # --- Paso 4: Push de repos con commits locales --- # Si un push falla por non-fast-forward (remoto adelantado), intentamos # un merge automatico (ort) sin conflictos contra origin/master y # volvemos a pushear. Asi nunca dejamos commits locales huerfanos. echo "" >&2 echo "[4/6] Pusheando repos adelantados..." >&2 local push_summary="" local push_errors="" while IFS= read -r repo; do [[ -z "$repo" ]] && continue local repo_name repo_name="$(basename "$repo")" # Captura SOLO stdout como status_line (la decision de control); el # stderr (logs de git) va a la terminal. Mezclar ambos con 2>&1 metia # lineas "[push] ..." de stderr al principio del string y rompia el # glob `== "[error]"*` (anclado al inicio) del recover. local status_line status_line=$(git_push_if_ahead "$repo" 2>/dev/null || true) # Detectar push rechazado por remoto adelantado. Cubrimos las distintas # redacciones de git: "[rejected]", "non-fast-forward", "fetch first", # "Updates were rejected", "Note about fast-forwards", o cualquier # linea que git_push_if_ahead haya marcado como [error]. if [[ "$status_line" == *"[error]"* || "$status_line" == *"rejected"* || "$status_line" == *"fast-forward"* || "$status_line" == *"fetch first"* ]]; then echo " [recover] $repo_name: push rechazado, intentando merge auto" >&2 # Fetch para tener origin actualizado. git -C "$repo" fetch --quiet 2>/dev/null || true local upstream upstream=$(git -C "$repo" rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true) if [[ -n "$upstream" ]]; then # Evaluar el exit del MERGE, no el de tail. Antes # `git merge ... | tail -3` evaluaba el exit de tail (siempre 0), # asi un merge con conflictos se reportaba como exito. local merge_out merge_rc merge_out=$(git -C "$repo" merge --no-ff --no-edit "$upstream" 2>&1) merge_rc=$? echo "$merge_out" | tail -3 >&2 if [[ "$merge_rc" -eq 0 ]]; then status_line=$(git_push_if_ahead "$repo" 2>/dev/null || true) else git -C "$repo" merge --abort 2>/dev/null || true status_line="[error] $repo_name: merge auto fallo (conflictos), requiere intervencion manual" fi else status_line="[error] $repo_name: sin upstream, no se puede recuperar" fi fi if [[ -n "$status_line" ]]; then echo " $status_line" >&2 push_summary="$push_summary"$'\n'" $status_line" if [[ "$status_line" == *"[error]"* ]]; then push_errors="$push_errors"$'\n'" $status_line" fi fi done <<< "$repos" # --- Paso 5: Push de ~/.password-store (sin commitear) --- echo "" >&2 echo "[5/6] Verificando ~/.password-store..." >&2 local pass_dir="${PASSWORD_STORE_DIR:-$HOME/.password-store}" local pass_summary=" [skip] password-store: no encontrado" if [[ -d "$pass_dir/.git" ]]; then local pass_dirty pass_dirty=$(git -C "$pass_dir" status --porcelain | wc -l) if [[ "$pass_dirty" -gt 0 ]]; then echo " [warn] ~/.password-store tiene cambios sin commitear; pass debe commitear solo. Saltando push." >&2 pass_summary=" [warn] password-store: dirty (pass no commiteo)" else local pass_status pass_status=$(git_push_if_ahead "$pass_dir" 2>/dev/null || true) echo " $pass_status" >&2 pass_summary=" $pass_status" fi fi # --- Paso 6: fn sync --- echo "" >&2 echo "[6/6] Ejecutando fn sync..." >&2 local sync_summary=" [skip] fn sync: credenciales no disponibles" local fn_bin="$registry_root/fn" if [[ -x "$fn_bin" ]]; then local api_user api_pass api_token api_user=$(pass_get registry/basicauth-user | head -n1 2>/dev/null || true) api_pass=$(pass_get registry/basicauth-pass | head -n1 2>/dev/null || true) api_token=$(pass_get registry/api-token | head -n1 2>/dev/null || true) if [[ -n "$api_user" && -n "$api_pass" && -n "$api_token" ]]; then export FN_REGISTRY_API="https://${api_user}:${api_pass}@registry.organic-machine.com" export REGISTRY_API_TOKEN="$api_token" local sync_out sync_out=$("$fn_bin" sync 2>&1) && { sync_summary=" OK: $sync_out" } || { sync_summary=" [error] fn sync: $sync_out" } echo " $sync_summary" >&2 else echo " [warn] Credenciales registry no disponibles — omitiendo fn sync" >&2 fi else echo " [warn] $fn_bin no encontrado — omitiendo fn sync" >&2 fi # --- Resumen --- echo "" echo "===== RESUMEN full_git_push =====" echo "" echo "Commits creados:" if [[ -n "$commits_summary" ]]; then echo "$commits_summary" else echo " (ninguno)" fi echo "" echo "Push status:" if [[ -n "$push_summary" ]]; then echo "$push_summary" else echo " (ninguno)" fi echo "" echo "pass-secrets:" echo "$pass_summary" echo "" echo "fn sync:" echo "$sync_summary" if [[ -n "$bypass_summary" ]]; then echo "" echo "[!] Hook bypasses (--no-verify usado para no perder cambios):" echo "$bypass_summary" echo "" echo " → Arregla la causa raiz (uses_functions drift, etc.) en el siguiente ciclo." fi if [[ -n "$commit_errors" || -n "$push_errors" ]]; then echo "" echo "[!!] ERRORES — el agente DEBE intervenir antes de continuar:" [[ -n "$commit_errors" ]] && echo " Commit:$commit_errors" [[ -n "$push_errors" ]] && echo " Push:$push_errors" echo "" echo "=================================" return 1 fi echo "" echo "=================================" } full_git_push "$@"