#!/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 # resumen (auto-tail si falló) # actions-logs.sh --tail N # actions-logs.sh --head N # actions-logs.sh --lines X-Y # actions-logs.sh --grep PAT [-C N] # actions-logs.sh --errors [-C N] # actions-logs.sh --count # actions-logs.sh --save /tmp/log.txt # actions-logs.sh --full [--i-mean-it] # actions-logs.sh --job ... # skip probe # # 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 </ [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á / " >&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 directo. Listá jobs candidatos con:" >&2 echo " for j in \$(seq ); 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