Files
jiantw83 28a52c9f0c fix(gitea): 認證失敗不再被讀成頁面不存在
金鑰失效以前會偽裝成別的結果。指令把請求直接接進管線,管線的結束狀態
取自後段的解析程式,前段的失敗就被吃掉。wiki 頁清單因此看起來是空的,
PR 留言看起來像沒有任何審查意見。wiki 讀取更把每一種失敗都翻成
「頁面不存在」。

技能組寫 wiki 的語意是附加、不覆蓋,判斷依據是先把舊內容讀回來。呼叫端
一旦把認證失敗當成一張新頁,就會整份蓋上去,舊紀錄直接消失。

現在失敗成因分開回報:找不到、金鑰失效或權限不足、其他 API 失敗,各給
一個結束碼。每條管線先接進變數,先看結束碼,再解析內容。議題工具的同一
類缺陷一併修掉。wiki 技能也把「只有找不到才可以建新頁」寫成獨立規則,
涵蓋每一條「不存在就建立」的路徑。
2026-08-31 11:11:55 +08:00

220 lines
9.3 KiB
Bash
Executable File
Raw Permalink 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
# issue.sh — Gitea 議題的讀取與建立(供 jsc-gitea:wiki-to-issue、html-export 使用)。
#
# 為什麼要有這支腳本:建議題要組 JSON、要把標籤名換成標籤 id、要把回應裡的編號與網址挑出來,
# 全是固定的輸入輸出。交給模型每次自己拼 curl,跳脫字元一錯就把議題內容寫壞,而議題是對外的。
#
# 用法:
# issue.sh labels <owner>/<repo> 列出既有標籤:id<TAB>name
# issue.sh label-ids <owner>/<repo> <name>[,<name>] 標籤名換成 id(逗號分隔);有名字對不到就 exit 4
# issue.sh projects <owner>/<repo> 列出專案看板:id<TAB>title;站台沒有這個 API 就 exit 3
# issue.sh title <owner>/<repo> <index> 印出議題標題
# issue.sh body <owner>/<repo> <index> 印出議題正文(markdown)
# issue.sh labels-of <owner>/<repo> <index> 印出議題目前的標籤名,一行一個
# issue.sh show <owner>/<repo> <index> 一次印出標題、標籤與正文(只打一次 API)
# 輸出格式固定四段,正文放最後,因為只有它會多行:
# 第 1 行 title<TAB>{標題}
# 第 2 行 labels<TAB>{標籤名,逗號分隔;沒有標籤時這一欄留空}
# 第 3 行 body (單獨一個字,當正文的起始標記)
# 第 4 行起 正文原文,一直到檔尾
# 呼叫端取標題用 head -n1 | cut -f2-,取標籤用 sed -n 2p | cut -f2-,取正文用 tail -n +4。
# 同一筆議題要標題、正文又要標籤時一律用 show,不要連打 title、body、labels-of 三次。
# issue.sh create <owner>/<repo> <title> <body-file> [--labels <id>[,<id>]] [--milestone <id>]
# 建立議題。輸出兩行: index=<編號>、url=<議題網址>
#
# 環境變數: 同 gitea.sh(GITEA_HOST、GITEA_TOKEN;token 缺或被拒時退回 tea 登入金鑰)。
# 結束碼: 0=成功 1=API 失敗 2=用法錯誤 3=站台不支援該 API 4=名稱對不到 id
#
# 陷阱:
# - 正文一律用檔案傳進來,內容維持 UTF-8 與真實換行;不要在參數裡塞 \n。
# - Gitea 1.27 沒有公開的專案看板 API,projects 會 exit 3。呼叫端要據實回報「請手動把議題拖到看板」,
# 不要假裝已經關聯好——關聯不上跟關聯成功看起來一樣,是最容易被當成完成的一種失敗。
# exit 3 的兩條路徑(端點回錯、端點回了但不是清單)都會印出看板網址,呼叫端一定拿得到那條連結。
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
GITEA="$script_dir/gitea.sh"
usage() {
cat >&2 <<'EOF'
用法:
issue.sh labels <owner>/<repo> 列出既有標籤:id<TAB>name
issue.sh label-ids <owner>/<repo> <name>[,<name>] 標籤名換成 id
issue.sh projects <owner>/<repo> 列出專案看板;站台不支援就 exit 3
issue.sh title|body|labels-of|show <owner>/<repo> <index>
issue.sh create <owner>/<repo> <title> <body-file> [--labels <id>[,<id>]] [--milestone <id>]
結束碼: 0=成功 1=API 失敗 2=用法錯誤 3=站台不支援 4=名稱對不到 id
EOF
exit 2
}
[ -f "$GITEA" ] || { echo "[jsc][議題][ERR]:找不到 $GITEA。" >&2; exit 1; }
valid_repo() {
case "${1:-}" in
*/*/*|/*|*/) return 1 ;;
*/*) return 0 ;;
*) return 1 ;;
esac
}
board_url() { # 看板頁的人工網址。站台沒有看板 API 時,呼叫端要靠這條連結手動處理
_host="${GITEA_HOST:-}"
case "$_host" in http://*|https://*) : ;; *) _host="https://$_host" ;; esac
printf '%s/%s/projects\n' "${_host%/}" "$repo"
}
cmd="${1:-}"; [ -n "$cmd" ] || usage
shift || true
repo="${1:-}"
valid_repo "$repo" || { echo "[jsc][議題][ERR]:存取庫須為 {owner}/{repo},收到「${repo:-空值}」。" >&2; exit 2; }
shift
# gitea.sh 的輸出一律先接進變數再解析,不要寫成 "$GITEA" api ... | python3。
# 管線的結束狀態取自 python,會把 gitea.sh 的失敗整個吃掉:金鑰失效時,回應是一份
# 錯誤 JSON,python 從裡面挑不到 labels 就印出空清單並回 0,看起來像「這個議題沒有標籤」。
api_get() { # $1=API 路徑 -> 回應內容;失敗回 1
"$GITEA" api GET "$1"
}
case "$cmd" in
labels)
out=$(api_get "/repos/$repo/labels?limit=100") \
|| { echo "[jsc][議題][ERR]:讀不到 $repo 的標籤清單。" >&2; exit 1; }
printf '%s' "$out" | python3 -c '
import json,sys
for l in json.load(sys.stdin):
print("%s\t%s" % (l["id"], l["name"]))
' ;;
label-ids)
names="${1:-}"; [ -n "$names" ] || usage
out=$(api_get "/repos/$repo/labels?limit=100") \
|| { echo "[jsc][議題][ERR]:讀不到 $repo 的標籤清單。" >&2; exit 1; }
printf '%s' "$out" | python3 -c '
import json,sys
want=[n.strip() for n in sys.argv[1].split(",") if n.strip()]
have={l["name"]: l["id"] for l in json.load(sys.stdin)}
miss=[n for n in want if n not in have]
if miss:
sys.stderr.write("[jsc][議題][ERR]:這些標籤在存取庫裡找不到:" + "、".join(miss) + "\n")
sys.exit(4)
print(",".join(str(have[n]) for n in want))
' "$names" ;;
projects)
manual=0
if out=$(api_get "/repos/$repo/projects" 2>/dev/null); then
printf '%s' "$out" | python3 -c '
import json,sys
try:
d=json.load(sys.stdin)
except Exception:
sys.exit(3)
if not isinstance(d, list):
sys.exit(3)
for p in d:
print("%s\t%s" % (p.get("id",""), p.get("title") or p.get("name","")))
' || manual=1
else
manual=1
fi
if [ "$manual" = 1 ]; then
# 兩條 exit 3 的路徑都要印出看板網址:呼叫端的技能被要求把這條連結交給使用者,
# 少印一次,使用者就只收到「關聯不上」而不知道要去哪裡手動拖。
echo "[jsc][議題][WARN]:這個 Gitea 站台沒有專案看板 API,程式關聯不了。請開 $(board_url) 手動把議題拖進看板。" >&2
exit 3
fi ;;
title)
idx="${1:-}"; [ -n "$idx" ] || usage
out=$(api_get "/repos/$repo/issues/$idx") \
|| { echo "[jsc][議題][ERR]:讀不到議題 $repo#$idx。" >&2; exit 1; }
printf '%s' "$out" | python3 -c '
import json,sys
print(json.load(sys.stdin).get("title",""))
' ;;
body)
idx="${1:-}"; [ -n "$idx" ] || usage
out=$(api_get "/repos/$repo/issues/$idx") \
|| { echo "[jsc][議題][ERR]:讀不到議題 $repo#$idx。" >&2; exit 1; }
printf '%s' "$out" | python3 -c '
import json,sys
sys.stdout.write(json.load(sys.stdin).get("body","") or "")
' ;;
labels-of)
idx="${1:-}"; [ -n "$idx" ] || usage
out=$(api_get "/repos/$repo/issues/$idx") \
|| { echo "[jsc][議題][ERR]:讀不到議題 $repo#$idx 的標籤。" >&2; exit 1; }
printf '%s' "$out" | python3 -c '
import json,sys
for l in json.load(sys.stdin).get("labels") or []:
print(l.get("name",""))
' ;;
show)
# 標題、標籤、正文都在同一份 API 回應裡。分成 title、body、labels-of 三次呼叫,
# 打的是同一支端點三遍,還可能取到三個不同時間點的版本。
idx="${1:-}"; [ -n "$idx" ] || usage
out=$(api_get "/repos/$repo/issues/$idx") \
|| { echo "[jsc][議題][ERR]:讀不到議題 $repo#$idx。" >&2; exit 1; }
printf '%s' "$out" | python3 -c '
import json,sys
try:
d=json.load(sys.stdin)
except Exception:
sys.exit(1)
if not isinstance(d, dict) or not d.get("title"):
sys.exit(1)
print("title\t%s" % d.get("title"))
print("labels\t%s" % ",".join((l.get("name") or "") for l in (d.get("labels") or [])))
print("body")
sys.stdout.write(d.get("body") or "")
' || { echo "[jsc][議題][ERR]:議題 $repo#$idx 的回應解不出標題。" >&2; exit 1; } ;;
create)
title="${1:-}"; body_file="${2:-}"
[ -n "$title" ] || usage
[ -n "$body_file" ] || usage
[ -f "$body_file" ] || { echo "[jsc][議題][ERR]:找不到正文檔「$body_file」。" >&2; exit 2; }
shift 2
labels=''
milestone=''
while [ "$#" -gt 0 ]; do
case "$1" in
--labels) [ "$#" -ge 2 ] || usage; labels="$2"; shift 2 ;;
--milestone) [ "$#" -ge 2 ] || usage; milestone="$2"; shift 2 ;;
*) echo "[jsc][議題][ERR]:不認得的選項「$1」。" >&2; usage ;;
esac
done
confirm_target="議題「$title」"
[ -n "$labels" ] && confirm_target="$confirm_target,標籤:$labels"
[ -n "$milestone" ] && confirm_target="$confirm_target,里程碑:$milestone"
sh "$script_dir/write-confirm.sh" "建立議題" "$confirm_target"
payload=$(mktemp)
trap 'rm -f "$payload"' EXIT
python3 -c '
import json,sys
title, body_file, labels, milestone, out = sys.argv[1:6]
d={"title": title, "body": open(body_file, encoding="utf-8").read()}
if labels:
d["labels"]=[int(x) for x in labels.split(",") if x.strip()]
if milestone:
d["milestone"]=int(milestone)
with open(out, "w", encoding="utf-8") as f:
json.dump(d, f, ensure_ascii=False)
' "$title" "$body_file" "$labels" "$milestone" "$payload"
out=$("$GITEA" api POST "/repos/$repo/issues" "$payload") \
|| { echo "[jsc][議題][ERR]:建不出議題到 $repo。" >&2; exit 1; }
printf '%s' "$out" | python3 -c '
import json,sys
d=json.load(sys.stdin)
print("index=%s" % (d.get("number") or d.get("index") or ""))
print("url=%s" % (d.get("html_url") or ""))
' ;;
*) usage ;;
esac