Files
gitea/tools/gitea.sh
T
jiantw83andClaude Fable 5 684d3f4f31 feat(gitea): gitea.sh 認證失敗時自動退回 tea CLI 登入 token
What(改了什麼)
- tools/gitea.sh 新增 tea_token():解析 ~/.config/tea/config.yml,優先取 default: true 的登入 token,否則取第一個。
- 啟動時 GITEA_TOKEN 未設定或為空,改用 tea token;兩者皆無才維持原本的必填錯誤。
- req() 遇 HTTP 401/403 且 tea token 存在又與當前 token 不同時,換用 tea token 重試一次,之後不再重試。
- 檔頭用法註解補上退回機制的說明。

Why(為什麼改)
- 使用者規則:api token 401/403 時,若 tea CLI 已登入就取其 token 重試,避免因 GITEA_TOKEN 過期或未設定就直接中斷流程。

How(怎麼改)
- 以 awk 解析 tea 設定檔的 logins 清單,抓各登入的 token 與 default 旗標。
- req() 改為 while 迴圈包住 curl,retried 旗標保證最多重試一次;HTTP >= 400 仍以 exit 4 收場。
- 實測:sh -n 語法檢查通過;GITEA_TOKEN 為空時經 tea token 成功;GITEA_TOKEN=deadbeef 時走 401 重試路徑成功;無 tea 設定且 token 為空時維持原錯誤訊息。

Who(影響哪個功能/使用者)
- 所有經 tools/gitea.sh 呼叫 Gitea API 的技能(wiki、repo-sync、jsc-git pr 等)與使用者:token 缺失或失效時多一層自動救援,行為只在原本會失敗的情境改變。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 08:23:27 +00:00

185 lines
7.5 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# gitea.sh — Gitea API 工具(curl + GITEA_TOKEN)。
# 用法:
# gitea.sh owners # 列出可讀取的 owner(自己 + 組織)
# gitea.sh repos <owner> # 列出 owner 底下可讀取的 repo(全名)
# gitea.sh default-branch <owner>/<repo> # 印出預設分支
# gitea.sh clone-url <owner>/<repo> # 印出 clone URL
# gitea.sh wiki-repo <TYPE> # 解析頁面類型的 wiki 位置:
# TYPE = QUESTION|PLAN|ANALYZE|MAINTAIN|REPO|LOG(即頁名前綴)
# 依序取 JSC_WIKI_REPO_{TYPE} > JSC_WIKI_REPO;都未設定 exit 3(呼叫端須詢問使用者)
# gitea.sh wiki-list <owner>/<repo> # 列出 wiki 頁名
# gitea.sh wiki-get <owner>/<repo> <page> # 印出 wiki 頁 markdown;不存在時 exit 4
# gitea.sh wiki-put <owner>/<repo> <page> <file> # 建立或更新 wiki 頁(內容取自檔案)
# gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file> # 建立 PR,印出 PR URL
# gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index>
# 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併
# gitea.sh repo-set <owner>/<repo> <description> [website] # 設定 repo 描述與網頁
# gitea.sh api <METHOD> <path> [json-file] # 原始 API 呼叫(path 以 /repos/... 起始)
# 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN
# GITEA_TOKEN 未設定,或請求遇 401/403 時,自動退回 tea CLI 的登入 token
# (~/.config/tea/config.yml,優先取 default: true 的登入);兩者皆無才失敗。
set -eu
tea_token() { # 取 tea CLI 設定檔的 token(優先 default: true 的登入,否則第一個)
cfg="${HOME}/.config/tea/config.yml"
[ -f "$cfg" ] || return 1
t=$(awk '
/^ *- / { if (def && tok != "") { print tok; found=1; exit } tok=""; def=0 }
/^ *token: */ { line=$0; sub(/^ *token: */,"",line); gsub(/"/,"",line)
tok=line; if (first=="") first=line }
/^ *default: *true/ { def=1 }
END { if (!found) { if (def && tok != "") print tok; else print first } }
' "$cfg")
[ -n "$t" ] && printf '%s\n' "$t"
}
: "${GITEA_HOST:?GITEA_HOST is required}"
TEA_TOKEN=$(tea_token 2>/dev/null || true)
if [ -z "${GITEA_TOKEN:-}" ]; then
if [ -n "$TEA_TOKEN" ]; then GITEA_TOKEN="$TEA_TOKEN"; else
: "${GITEA_TOKEN:?GITEA_TOKEN is required}"
fi
fi
case "$GITEA_HOST" in http://*|https://*) HOST="$GITEA_HOST" ;; *) HOST="https://$GITEA_HOST" ;; esac
API="${HOST%/}/api/v1"
req() { # METHOD path [json-file] -> body(HTTP >= 400 時 exit 4;401/403 以 tea token 重試一次)
method="$1"; path="$2"; body_file="${3:-}"
retried=0
while :; do
if [ -n "$body_file" ]; then
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
-H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
--data-binary "@$body_file" "$API$path")
else
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
-H "Authorization: token $GITEA_TOKEN" "$API$path")
fi
code=$(printf '%s' "$out" | tail -n1)
if [ "$retried" -eq 0 ] && { [ "$code" = 401 ] || [ "$code" = 403 ]; } \
&& [ -n "$TEA_TOKEN" ] && [ "$TEA_TOKEN" != "$GITEA_TOKEN" ]; then
GITEA_TOKEN="$TEA_TOKEN"; retried=1; continue
fi
break
done
printf '%s\n' "$out" | sed '$d'
[ "$code" -lt 400 ]
}
json_field() { # stdin JSON -> 每個物件的指定欄位一行(陣列或單一物件皆可)
python3 -c '
import json,sys
d=json.load(sys.stdin); k=sys.argv[1]
items=d if isinstance(d,list) else [d]
for i in items:
v=i.get(k,"")
if v!="" and v is not None: print(v)
' "$1"
}
cmd="${1:?usage: gitea.sh <command> ...}"; shift
case "$cmd" in
owners)
{ req GET "/user" | json_field login
req GET "/user/orgs?limit=50" | json_field username; } | sort -u ;;
repos)
owner="${1:?owner required}"
# 分頁抓 owner 的 repo(org 與 user 端點擇一成功)
page=1
while :; do
out=$(req GET "/orgs/$owner/repos?limit=50&page=$page" 2>/dev/null) \
|| out=$(req GET "/users/$owner/repos?limit=50&page=$page")
names=$(printf '%s' "$out" | json_field full_name)
[ -n "$names" ] || break
printf '%s\n' "$names"
page=$((page+1))
done ;;
default-branch)
or="${1:?owner/repo required}"
req GET "/repos/$or" | json_field default_branch ;;
clone-url)
or="${1:?owner/repo required}"
req GET "/repos/$or" | json_field clone_url ;;
wiki-repo)
type=$(printf '%s' "${1:?TYPE required}" | tr a-z A-Z)
eval "v=\${JSC_WIKI_REPO_$type:-}"
[ -n "$v" ] || v="${JSC_WIKI_REPO:-}"
if [ -n "$v" ]; then printf '%s\n' "$v"; else
echo "no wiki repo configured for $type (set JSC_WIKI_REPO_$type or JSC_WIKI_REPO)" >&2; exit 3
fi ;;
wiki-list)
or="${1:?owner/repo required}"
req GET "/repos/$or/wiki/pages?limit=200" | json_field title ;;
wiki-get)
or="${1:?owner/repo required}"; page="${2:?page required}"
if ! out=$(req GET "/repos/$or/wiki/page/$page" 2>/dev/null); then
echo "wiki page not found: $page" >&2; exit 4
fi
printf '%s' "$out" | python3 -c '
import json,sys,base64
d=json.load(sys.stdin)
sys.stdout.write(base64.b64decode(d.get("content_base64","")).decode("utf-8"))
' ;;
wiki-put)
or="${1:?owner/repo required}"; page="${2:?page required}"; file="${3:?content file required}"
tmp=$(mktemp)
python3 -c '
import json,sys,base64
title,path=sys.argv[1],sys.argv[2]
content=open(path,"rb").read()
print(json.dumps({"title":title,"content_base64":base64.b64encode(content).decode()}))
' "$page" "$file" > "$tmp"
if req GET "/repos/$or/wiki/page/$page" >/dev/null 2>&1; then
req PATCH "/repos/$or/wiki/page/$page" "$tmp" >/dev/null
else
req POST "/repos/$or/wiki/new" "$tmp" >/dev/null
fi
rm -f "$tmp"
echo "OK $page" ;;
pr-create)
or="${1:?owner/repo required}"; head="${2:?head required}"; base="${3:?base required}"
title="${4:?title required}"; body_file="${5:?body file required}"
tmp=$(mktemp)
python3 -c '
import json,sys
print(json.dumps({"head":sys.argv[1],"base":sys.argv[2],"title":sys.argv[3],
"body":open(sys.argv[4],encoding="utf-8").read()}))
' "$head" "$base" "$title" "$body_file" > "$tmp"
req POST "/repos/$or/pulls" "$tmp" | json_field html_url
rm -f "$tmp" ;;
pr-depend)
or="${1:?owner/repo required}"; idx="${2:?pr index required}"
dep_or="${3:?dep owner/repo required}"; dep_idx="${4:?dep index required}"
tmp=$(mktemp)
python3 -c '
import json,sys
o,r=sys.argv[1].split("/",1)
print(json.dumps({"owner":o,"repo":r,"index":int(sys.argv[2])}))
' "$dep_or" "$dep_idx" > "$tmp"
if ! out=$(req POST "/repos/$or/issues/$idx/dependencies" "$tmp"); then
rm -f "$tmp"; printf '%s\n' "$out" >&2; exit 4
fi
rm -f "$tmp"
echo "OK $or#$idx depends on $dep_or#$dep_idx" ;;
repo-set)
or="${1:?owner/repo required}"; desc="${2:?description required}"; site="${3:-}"
tmp=$(mktemp)
python3 -c '
import json,sys
b={"description":sys.argv[1]}
if len(sys.argv)>2 and sys.argv[2]: b["website"]=sys.argv[2]
print(json.dumps(b))
' "$desc" "$site" > "$tmp"
if ! out=$(req PATCH "/repos/$or" "$tmp"); then
rm -f "$tmp"; printf '%s\n' "$out" >&2; exit 4
fi
rm -f "$tmp"
echo "OK $or" ;;
api)
method="${1:?METHOD}"; path="${2:?path}"; body="${3:-}"
req "$method" "$path" $body ;;
*)
echo "unknown command: $cmd" >&2; exit 2 ;;
esac