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 (✓, →, ─, ⚠️).
169 lines
4.9 KiB
Bash
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)
|
|
"
|