En Windows el stdout de Python defaultea a cp1252 y crashea con
UnicodeEncodeError al imprimir literales no-ASCII (e.g. el ✓ del print
de éxito de pr-create.sh). El sintoma era que el PR se creaba OK pero
el script terminaba con exit 1 por el crash del print posterior.
Agrego export PYTHONIOENCODING=utf-8 después de set -euo pipefail en
los 10 scripts que invocan python (todos menos query.sh). Cubre tanto
las invocaciones inline como futuras sin tener que acordarse caso por
caso. Mantiene los literales unicode existentes (✓, →, ─, ⚠️).
292 lines
9.9 KiB
Bash
292 lines
9.9 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
|
|
|
|
# Forzar UTF-8 en stdout de Python (Windows defaultea a cp1252 y crashea con
|
|
# literales no-ASCII). Ver memoria feedback_api_utf8_encoding.md.
|
|
export PYTHONIOENCODING=utf-8
|
|
|
|
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
|