dfcb3c46a7
Auto-init step skipped any dir with .git present, even when it lacked an origin remote. Such dirs fell through to push step and failed with "'origin' does not appear to be a git repository". Now skip only when .git AND origin exist; otherwise run ensure_repo_synced to create the Gitea repo + add origin + push. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
298 lines
12 KiB
Bash
298 lines
12 KiB
Bash
#!/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
|
|
local registry_root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
|
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/<p>/apps/, analysis/, projects/<p>/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 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) <noreply@anthropic.com>" \
|
|
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")"
|
|
local status_line
|
|
status_line=$(git_push_if_ahead "$repo" 2>&1 || true)
|
|
|
|
if [[ "$status_line" == *"non-fast-forward"* || "$status_line" == *"Updates were rejected"* || "$status_line" == "[error]"* ]]; 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
|
|
if git -C "$repo" merge --no-ff --no-edit "$upstream" 2>&1 | tail -3 >&2; then
|
|
status_line=$(git_push_if_ahead "$repo" 2>&1 || true)
|
|
else
|
|
git -C "$repo" merge --abort 2>/dev/null || true
|
|
status_line="[error] $repo_name: merge auto fallo, 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 "$@"
|