164 lines
4.7 KiB
Bash
164 lines
4.7 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
|
|
|
|
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)
|
|
"
|