134 lines
5.9 KiB
Bash
Executable File
134 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# herdr-issue 共用函式。每個腳本都 source 這個檔。
|
||
set -euo pipefail
|
||
|
||
LABEL="in-progress"
|
||
LABEL_COLOR="0e8a16"
|
||
LABEL_DESC="Herdr 的 agent 正在實作"
|
||
INTERVAL="${HERDR_ISSUE_INTERVAL:-300}"
|
||
MODEL="${HERDR_ISSUE_MODEL:-claude-sonnet-5-5}"
|
||
SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
die() { echo "herdr-issue: $*" >&2; exit 1; }
|
||
|
||
# tea 的預設使用者。在 clone 裡 tea 會依 remote URL 挑第一個符合的 login(可能不是預設的),
|
||
# 所以明確取 default 那個 login,所有 tea 呼叫都加 --login。可用 HERDR_ISSUE_LOGIN 覆寫。
|
||
if [ -z "${HERDR_ISSUE_LOGIN:-}" ]; then
|
||
HERDR_ISSUE_LOGIN="$(tea login list --output json | python3 -c "import sys,json
|
||
print(next((l['name'] for l in json.load(sys.stdin) if str(l.get('default')).lower()=='true'),''))")"
|
||
fi
|
||
[ -n "$HERDR_ISSUE_LOGIN" ] || { echo "herdr-issue: tea 沒有預設 login,請先 tea login default <name>" >&2; exit 1; }
|
||
export HERDR_ISSUE_LOGIN
|
||
|
||
# 忽略所有 TLS 檢查:tea 的 insecure 只能寫在設定檔(沒有環境變數),所以把所有 login 設成 insecure: true(冪等);
|
||
# git 用 GIT_SSL_NO_VERIFY。agent 的 tea/git 也會吃到同一份設定。
|
||
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/tea/config.yml"
|
||
if [ -f "$TEA_CONFIG" ] && grep -qE '^\s*insecure:\s*false' "$TEA_CONFIG"; then
|
||
sed -i -E 's/^(\s*insecure:\s*)false/\1true/' "$TEA_CONFIG"
|
||
fi
|
||
export GIT_SSL_NO_VERIFY=true
|
||
|
||
# git 走 HTTPS 時沒有帳密又不能互動輸入會失敗,所以腳本內的 git 一律用 tea 預設 login 的 token(git-credential.sh)。
|
||
export GIT_TERMINAL_PROMPT=0
|
||
git() { command git -c http.sslVerify=false -c credential.helper= -c "credential.helper=$SCRIPTS/git-credential.sh" "$@"; }
|
||
# TLS/憑證錯誤多半是暫時的(連線被重置、代理、時鐘),遇到就重試幾次,不要讓輪詢中的腳本因此中斷;
|
||
# 其他錯誤原樣回傳。(憑證驗證已關閉,仍保留重試以處理連線被重置等暫時性錯誤。)重試次數 HERDR_ISSUE_TLS_RETRIES(預設 5),間隔 3 秒。
|
||
tea() {
|
||
local out err rc=0 try max="${HERDR_ISSUE_TLS_RETRIES:-5}" ef; ef="$(mktemp)"
|
||
for ((try = 1; try <= max; try++)); do
|
||
out="$(command tea "$1" --login "$HERDR_ISSUE_LOGIN" "${@:2}" 2>"$ef")" && rc=0 || rc=$?
|
||
err="$(cat "$ef")"
|
||
if [ "$rc" -eq 0 ] || ! grep -qiE 'tls|x509|certificate|handshake' <<<"$out$err"; then break; fi
|
||
echo "herdr-issue: tea 遇到 TLS/憑證錯誤(第 $try/$max 次),3 秒後重試" >&2
|
||
rc=${rc/#0/1}; sleep 3
|
||
done
|
||
rm -f "$ef"
|
||
[ -z "$err" ] || printf '%s\n' "$err" >&2
|
||
printf '%s\n' "$out"
|
||
return "$rc"
|
||
}
|
||
|
||
# 從 stdin 讀 JSON,執行 python 片段,d 是解析後的物件。
|
||
jq_py() { python3 -c "import sys,json
|
||
def find(o,k):
|
||
if isinstance(o,dict):
|
||
if k in o: return o[k]
|
||
for v in o.values():
|
||
r=find(v,k)
|
||
if r is not None: return r
|
||
elif isinstance(o,list):
|
||
for v in o:
|
||
r=find(v,k)
|
||
if r is not None: return r
|
||
return None
|
||
d=json.load(sys.stdin)
|
||
$1"; }
|
||
|
||
# 議題的完整 JSON(state、labels、body、title、closedAt)。
|
||
issue_json() { tea issues "$2" --repo "$1" --output json; }
|
||
|
||
issue_state() { issue_json "$1" "$2" | jq_py 'print(d["state"])'; }
|
||
|
||
issue_labels() { issue_json "$1" "$2" | jq_py 'print("\n".join(l["name"] for l in d.get("labels") or []))'; }
|
||
|
||
# 某個標籤底下還有幾個 open 議題。
|
||
open_count_with_label() {
|
||
tea issues list --repo "$1" --state open --labels "$2" --limit 500 --output json --fields index \
|
||
| jq_py 'print(len(d))'
|
||
}
|
||
|
||
ensure_label() {
|
||
if ! tea labels list --repo "$1" --output json | jq_py "sys.exit(0 if any(l['name']=='$LABEL' for l in d) else 1)"; then
|
||
tea labels create --repo "$1" --name "$LABEL" --color "$LABEL_COLOR" --description "$LABEL_DESC" >/dev/null
|
||
fi
|
||
}
|
||
|
||
# agent 狀態:idle|working|blocked|done|unknown|missing
|
||
agent_status() {
|
||
herdr agent get "issue-$1" 2>/dev/null | jq_py 'print(find(d,"agent_status") or "unknown")' 2>/dev/null || echo missing
|
||
}
|
||
|
||
# 標籤為 #N 的 tab ID,沒有就空字串。
|
||
tab_for_issue() {
|
||
herdr tab list --workspace "${HERDR_WORKSPACE_ID:?}" \
|
||
| jq_py "print(next((t['tab_id'] for t in find(d,'tabs') or [] if t.get('label')=='#$1'),''))"
|
||
}
|
||
|
||
# worktree 路徑:clone 同層的 <repo>-issue-N
|
||
worktree_path() { echo "$(dirname "$2")/$(basename "$2")-issue-$1"; }
|
||
|
||
notify() { herdr notification show "$1" --body "$2" --sound "${3:-request}" >/dev/null 2>&1 || true; }
|
||
|
||
# 目前目錄必須是 owner/repo 的 clone;印出 clone 根目錄。
|
||
clone_root_for() {
|
||
local root url
|
||
root="$(git rev-parse --show-toplevel 2>/dev/null)" || die "目前目錄不是 git repo,請在 $1 的 clone 裡執行"
|
||
url="$(git -C "$root" remote get-url origin 2>/dev/null || true)"
|
||
case "$url" in
|
||
*"$1"|*"$1.git"|*"$1/") echo "$root" ;;
|
||
*) die "目前 clone 的 origin 是 $url,不是 $1" ;;
|
||
esac
|
||
}
|
||
|
||
# head 分支為 issue-N 的最新 PR:印出「index state」或空字串。
|
||
pr_for_issue() {
|
||
tea pulls list --repo "$1" --state all --limit 500 --output json --fields index,state,mergeable,head \
|
||
| jq_py "m=[p for p in d if p.get('head')=='issue-$2']
|
||
print(' '.join([m[0]['index'],m[0]['state'],str(m[0].get('mergeable'))]) if m else '')"
|
||
}
|
||
|
||
# 擋住議題 N 的 open 依賴(Gitea 原生 issue dependencies),一行一個:同 repo 印 #M,跨 repo 印 owner/repo#M。
|
||
# tea api 遇到 HTTP 錯誤仍 exit 0,所以用回應是否為 JSON 陣列判斷成敗;失敗就 die。
|
||
open_blockers() {
|
||
tea api --repo "$1" "/repos/{owner}/{repo}/issues/$2/dependencies" | python3 -c "import sys,json
|
||
raw=sys.stdin.read()
|
||
try: d=json.loads(raw)
|
||
except Exception: d=None
|
||
if not isinstance(d,list):
|
||
sys.stderr.write('herdr-issue: 讀取 #$2 的依賴失敗:'+raw.strip()[:300]+'\n'); sys.exit(1)
|
||
for i in d:
|
||
if i.get('state')=='open':
|
||
r=(i.get('repository') or {}).get('full_name','')
|
||
print(('#' if r in ('','$1') else r+'#')+str(i['number']))"
|
||
}
|