Files
herdr-issue/skills/herdr-issue/scripts/lib.sh
T

134 lines
5.9 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 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']))"
}