Files
skill-gitea/scripts/pr-create.sh
claudecode0 d5e1cf3696 fix: forzar PYTHONIOENCODING=utf-8 en scripts con python -c
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 (✓, →, ─, ⚠️).
2026-04-26 15:00:37 -06:00

169 lines
4.9 KiB
Bash

#!/usr/bin/env bash
# gitea skill — crear un PR.
#
# Uso:
# pr-create.sh <owner>/<repo> --head <branch> [--base main] --title "..." \
# (--body "..." | --body-file <path>) [--draft]
#
# Garantías:
# - Body en UTF-8 puro (vía Python json.dumps + curl --data-binary @file)
# - Anti-AI-attribution guard sobre title + body (rechaza si encuentra
# "Co-Authored-By: Claude", "🤖", "Generated with Claude", "Anthropic" en
# contexto de atribución)
# - Trap EXIT borra el temp dir con el body al terminar
set -euo pipefail
# Forzar UTF-8 en stdout de Python. Windows defaultea a cp1252 y crashea
# con literales no-ASCII (e.g. el `OK`/`->` aquí abajo o un PR title con ñ).
# 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=""
head=""
base="main"
title=""
body=""
body_file=""
draft=0
while [[ $# -gt 0 ]]; do
case "$1" in
--head) head="$2"; shift 2 ;;
--base) base="$2"; shift 2 ;;
--title) title="$2"; shift 2 ;;
--body) body="$2"; shift 2 ;;
--body-file) body_file="$2"; shift 2 ;;
--draft) draft=1; shift ;;
-h|--help)
cat <<EOF
Uso: pr-create.sh <owner>/<repo> --head <branch> [--base main] \\
--title "..." (--body "..." | --body-file <path>) [--draft]
Ejemplos:
pr-create.sh NucleOS/nucleo-infra --head fix/foo --title "fix(x): ..." \\
--body-file /tmp/pr-body.md
pr-create.sh nucleo-infra --head feat/y --title "..." --body "linea 1"
EOF
exit 0
;;
-*) echo "ERROR: flag desconocida: $1" >&2; exit 2 ;;
*) repo_arg="$1"; shift ;;
esac
done
if [[ -z "$repo_arg" || -z "$head" || -z "$title" ]]; then
echo "ERROR: faltan args. Necesitás: <owner>/<repo> --head <branch> --title \"...\"" >&2
exit 2
fi
# Resolver owner/repo
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
# Cargar body desde archivo si corresponde
if [[ -n "$body_file" ]]; then
if [[ -n "$body" ]]; then
echo "ERROR: pasá --body o --body-file, no ambos." >&2
exit 2
fi
if [[ ! -f "$body_file" ]]; then
echo "ERROR: --body-file '$body_file' no existe." >&2
exit 2
fi
body="$(cat "$body_file")"
fi
# ─── Anti-AI-attribution guard ──────────────────────────────────────────
# Regla dura. Ver memoria feedback_no_ai_attribution.md.
combined="$title
$body"
ai_patterns=(
"Co-Authored-By:[[:space:]]*Claude"
"Co-Authored-By:[[:space:]]*Anthropic"
"🤖"
"Generated with[[:space:]]*\[?Claude"
"Generated with[[:space:]]*Anthropic"
"Created with[[:space:]]*\[?Claude"
"Powered by[[:space:]]*Claude"
"Made with[[:space:]]*Claude"
)
for pat in "${ai_patterns[@]}"; do
if echo "$combined" | grep -iqE "$pat"; then
cat >&2 <<EOF
ERROR: el title o body contiene marker de AI attribution:
pattern: $pat
Esta skill bloquea esos markers (regla dura, ver memoria
feedback_no_ai_attribution.md). El usuario considera deshonesto darle crédito
a Claude/Anthropic por trabajo del usuario. Removelos y volvé a correr.
Líneas que matchean:
EOF
echo "$combined" | grep -inE "$pat" | head -5 >&2
exit 4
fi
done
# ─── Build body JSON con Python (UTF-8 safe) ────────────────────────────
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
BODY_JSON="$TMP_DIR/body.json"
PY_TITLE="$title" PY_BODY="$body" PY_HEAD="$head" PY_BASE="$base" PY_DRAFT="$draft" \
python -c '
import json, os
data = {
"title": os.environ["PY_TITLE"],
"body": os.environ["PY_BODY"],
"head": os.environ["PY_HEAD"],
"base": os.environ["PY_BASE"],
}
if os.environ.get("PY_DRAFT") == "1":
# Gitea no tiene draft API field, pero podés prefijar el title
data["title"] = "[WIP] " + data["title"]
# json.dumps con ensure_ascii=True (default) escapa todo non-ASCII a \uXXXX,
# eliminando cualquier riesgo de encoding raro en transit.
print(json.dumps(data))
' > "$BODY_JSON"
# Sanity check
if [[ ! -s "$BODY_JSON" ]]; then
echo "ERROR: body JSON vacío después de python. Bug del script." >&2
exit 5
fi
echo "→ POST /repos/${owner}/${repo}/pulls" >&2
echo " head: $head → base: $base" >&2
echo " title: $title" >&2
echo " body: ${#body} chars" >&2
resp="$("$QUERY" -X POST \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @"$BODY_JSON" \
"/repos/${owner}/${repo}/pulls")"
echo "$resp" | python -c "
import json, sys
d = json.load(sys.stdin)
if d.get('number'):
print(f'✓ PR #{d[\"number\"]} creado')
print(f' url: {d.get(\"html_url\")}')
else:
print('ERROR:', d.get('message') or d, file=sys.stderr)
sys.exit(1)
"