Files
skill-gitea/scripts/actions-logs.sh

288 lines
9.7 KiB
Bash

#!/usr/bin/env bash
# gitea skill — leer logs de un run de Actions con filtros precisos.
#
# Diseñado para no saturar la ventana de contexto de Claude. Nunca devuelve
# un log entero por default. Modos:
#
# actions-logs.sh <repo> <run_ref> # resumen (auto-tail si falló)
# actions-logs.sh <repo> <run_ref> --tail N
# actions-logs.sh <repo> <run_ref> --head N
# actions-logs.sh <repo> <run_ref> --lines X-Y
# actions-logs.sh <repo> <run_ref> --grep PAT [-C N]
# actions-logs.sh <repo> <run_ref> --errors [-C N]
# actions-logs.sh <repo> <run_ref> --count
# actions-logs.sh <repo> <run_ref> --save /tmp/log.txt
# actions-logs.sh <repo> <run_ref> --full [--i-mean-it]
# actions-logs.sh <repo> <run_ref> --job <job_id> ... # skip probe
#
# <run_ref> puede ser run_number (lo que el usuario ve, ej. 11) o task_id
# (numérico interno). El script matchea por cualquiera de los dos.
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
QUERY="$SKILL_DIR/scripts/query.sh"
repo_arg=""
run_ref=""
job_id=""
mode="auto"
tail_n=""
head_n=""
lines_range=""
grep_pat=""
context_n="0"
grep_case_sensitive=0
save_path=""
i_mean_it=0
raw_output=0
while [[ $# -gt 0 ]]; do
case "$1" in
--job) job_id="$2"; shift 2 ;;
--tail) tail_n="$2"; mode="tail"; shift 2 ;;
--head) head_n="$2"; mode="head"; shift 2 ;;
--lines) lines_range="$2"; mode="lines"; shift 2 ;;
--grep) grep_pat="$2"; mode="grep"; shift 2 ;;
--grep-case) grep_case_sensitive=1; shift ;;
--errors) grep_pat='(error|fail|fatal|exception|panic|unhealthy|denied|refused|cannot|not found|missing)'; mode="grep"; shift ;;
--context|-C) context_n="$2"; shift 2 ;;
--count) mode="count"; shift ;;
--save) save_path="$2"; mode="save"; shift 2 ;;
--full) mode="full"; shift ;;
--i-mean-it) i_mean_it=1; shift ;;
--raw) raw_output=1; shift ;;
-h|--help)
cat <<EOF
Uso: actions-logs.sh <owner>/<repo> <run_ref> [opciones]
run_ref: run_number (visible en URL) o task_id (numérico interno).
Modos (default: auto-summary basado en status del run):
--tail N últimas N líneas
--head N primeras N líneas
--lines X-Y rango (1-indexed inclusive)
--grep PAT [-C N] regex case-insensitive con N líneas de contexto
--grep-case con --grep, hacer case-sensitive
--errors [-C N] shortcut: regex de errores comunes
--count solo número de líneas
--save PATH escribir log entero a archivo (no a stdout, sin sanitizar)
--full volcado completo (aborta si > 1000 lineas)
--i-mean-it con --full sobre log grande, confirma intención
--job JOB_ID usar este job_id directo (saltea el probe)
--raw no sanitizar (default: stripear ANSI escapes y truncar
líneas > 500 chars; necesario porque docker stack deploy
vuelca progress bars con mucho \\x1b[2K)
EOF
exit 0
;;
-*) echo "ERROR: flag desconocida: $1" >&2; exit 2 ;;
*)
if [[ -z "$repo_arg" ]]; then repo_arg="$1"
elif [[ -z "$run_ref" ]]; then run_ref="$1"
else echo "ERROR: argumento extra: $1" >&2; exit 2
fi
shift
;;
esac
done
if [[ -z "$repo_arg" || -z "$run_ref" ]]; then
echo "ERROR: pasá <owner>/<repo> <run_ref>" >&2
exit 2
fi
if [[ "$repo_arg" == */* ]]; then
owner="${repo_arg%%/*}"
repo="${repo_arg##*/}"
else
set -a; source "$SKILL_DIR/.env"; set +a
owner="${GITEA_DEFAULT_OWNER:?owner no especificado y GITEA_DEFAULT_OWNER vacío}"
repo="$repo_arg"
fi
# ─── Resolver task_id y status ──────────────────────────────────────────
resp="$("$QUERY" "/repos/${owner}/${repo}/actions/tasks?limit=50")"
read task_id task_status <<< "$(echo "$resp" | PY_REF="$run_ref" python -c '
import json, os, sys
ref = int(os.environ["PY_REF"])
d = json.load(sys.stdin)
for r in d.get("workflow_runs", []):
if r.get("id") == ref or r.get("run_number") == ref:
print(r.get("id"), r.get("status"))
sys.exit(0)
' | tr -d '\r')"
if [[ -z "${task_id:-}" ]]; then
echo "ERROR: no encontré ningún run con id o run_number = $run_ref" >&2
exit 3
fi
# ─── Resolver job_id (probe si no se pasó) ──────────────────────────────
probe_job_id() {
local target_task="$1"
local found=""
set +o pipefail
for delta in 0 1 2 3 4 5 -1 -2 -3 6 7 8 9 10; do
local jid=$((target_task + delta))
if [[ $jid -lt 1 ]]; then continue; fi
local first
first="$("$QUERY" "/repos/${owner}/${repo}/actions/jobs/${jid}/logs" 2>/dev/null | head -1 || true)"
if echo "$first" | grep -q "received task ${target_task} "; then
found="$jid"
break
fi
done
set -o pipefail
printf '%s' "$found"
}
if [[ -z "$job_id" ]]; then
echo "→ Probing job_id para task $task_id..." >&2
job_id="$(probe_job_id "$task_id")"
if [[ -z "$job_id" ]]; then
echo "ERROR: no pude resolver job_id (probé task_id±10)." >&2
echo " Probá con --job <jid> directo. Listá jobs candidatos con:" >&2
echo " for j in \$(seq <range>); do query.sh /repos/$owner/$repo/actions/jobs/\$j/logs | head -1; done" >&2
exit 4
fi
echo "→ job_id resuelto: $job_id" >&2
fi
LOG_PATH="/repos/${owner}/${repo}/actions/jobs/${job_id}/logs"
# ─── Helper: ejecutar la query y devolver el log a stdout o a temp file ─
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
LOG_FILE="$TMP_DIR/log.txt"
fetch_log() {
"$QUERY" "$LOG_PATH" > "$LOG_FILE"
if [[ "$raw_output" -eq 0 ]]; then
# Sanitize: strip ANSI escape sequences (CSI, OSC, simple ESC[...m, etc.)
# y truncar líneas > 500 chars (docker stack deploy las hace de 30KB con
# progress bars). El reemplazo lo hace Python para encoding-safe.
PY_LOG_IN="$LOG_FILE" python -c '
import re, os
ansi = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-_]")
maxlen = 500
fp_in = os.environ["PY_LOG_IN"]
with open(fp_in, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
with open(fp_in, "w", encoding="utf-8") as f:
for line in lines:
line = ansi.sub("", line.rstrip("\n"))
if len(line) > maxlen:
line = line[:maxlen] + f"... [truncated, +{len(line)-maxlen} chars]"
f.write(line + "\n")
'
fi
}
# ─── Modo: count ────────────────────────────────────────────────────────
if [[ "$mode" == "count" ]]; then
fetch_log
wc -l < "$LOG_FILE"
exit 0
fi
# ─── Modo: save (siempre RAW, sin sanitizar — preserva el log original) ─
if [[ "$mode" == "save" ]]; then
"$QUERY" "$LOG_PATH" > "$save_path"
size="$(wc -c < "$save_path")"
lines="$(wc -l < "$save_path")"
echo "→ guardado en $save_path ($lines líneas, $size bytes, job_id=$job_id)"
exit 0
fi
# ─── Modo: full (con guardrail) ─────────────────────────────────────────
if [[ "$mode" == "full" ]]; then
fetch_log
total="$(wc -l < "$LOG_FILE")"
if [[ "$total" -gt 1000 && "$i_mean_it" -ne 1 ]]; then
echo "ERROR: el log tiene $total líneas (> 1000). Volcar entero te llena la" >&2
echo " ventana. Si REALMENTE querés todo, agregá --i-mean-it" >&2
echo " o usá --tail/--head/--grep/--save." >&2
exit 5
fi
cat "$LOG_FILE"
exit 0
fi
# ─── Modo: tail / head / lines / grep ──────────────────────────────────
if [[ "$mode" == "tail" ]]; then
fetch_log
tail -n "$tail_n" "$LOG_FILE"
exit 0
fi
if [[ "$mode" == "head" ]]; then
fetch_log
head -n "$head_n" "$LOG_FILE"
exit 0
fi
if [[ "$mode" == "lines" ]]; then
fetch_log
start="${lines_range%-*}"
end="${lines_range#*-}"
if [[ -z "$start" || -z "$end" ]]; then
echo "ERROR: --lines necesita formato X-Y (ej. 100-150)." >&2
exit 2
fi
sed -n "${start},${end}p" "$LOG_FILE"
exit 0
fi
if [[ "$mode" == "grep" ]]; then
fetch_log
flags="-nE"
[[ "$grep_case_sensitive" -eq 0 ]] && flags="${flags}i"
if [[ "$context_n" != "0" ]]; then
flags="$flags -C $context_n"
fi
# No usar set -e short-circuit si grep no matchea
set +e
grep $flags "$grep_pat" "$LOG_FILE"
rc=$?
set -e
if [[ $rc -eq 1 ]]; then
echo "(sin matches para: $grep_pat)" >&2
elif [[ $rc -ne 0 ]]; then
exit $rc
fi
exit 0
fi
# ─── Modo: auto (default) ───────────────────────────────────────────────
# Resumen inteligente:
# - run failed/cancelled → tail 40 + header
# - run success → header solo + último step (tail 5)
# - run in_progress → tail 20
#
# task_status posibles en Gitea: success, failure, running, waiting, cancelled,
# blocked, skipped, unknown.
fetch_log
total="$(wc -l < "$LOG_FILE")"
case "$task_status" in
failure|cancelled|skipped)
echo "─── job $job_id status=$task_status ($total líneas total) ───"
echo "─── últimas 40 líneas ───"
tail -n 40 "$LOG_FILE"
;;
running|waiting|blocked)
echo "─── job $job_id status=$task_status ($total líneas hasta ahora) ───"
echo "─── últimas 20 líneas (en progreso) ───"
tail -n 20 "$LOG_FILE"
;;
success|*)
echo "─── job $job_id status=$task_status ($total líneas total) ───"
echo "─── últimas 5 líneas (resumen) ───"
tail -n 5 "$LOG_FILE"
echo
echo "Para ver más: --tail N | --grep PAT | --errors | --lines X-Y | --full"
;;
esac