forked from Gitlink/gitlink-cli
199 lines
8.1 KiB
Bash
199 lines
8.1 KiB
Bash
#!/usr/bin/env bash
|
||
# ================================================================
|
||
# Scenario 1a: Real-Time Issue Triage (Linux Webhook 版)
|
||
# Flow: 接收 Issue 编号 → 拉取详情 → 分类 → 打 tags → 分配人
|
||
# ================================================================
|
||
set -eo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
source "$SCRIPT_DIR/lib/common.sh"
|
||
|
||
ISSUE_NUMBER=""
|
||
OWNER=""
|
||
REPO=""
|
||
ASSIGNEE=""
|
||
DRY_RUN=false
|
||
|
||
usage() {
|
||
echo "Usage: $0 --issue-number N [--owner OWNER] [--repo REPO] [--assignee USER] [--dry-run]"
|
||
exit 1
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--issue-number) ISSUE_NUMBER="$2"; shift 2 ;;
|
||
--owner) OWNER="$2"; shift 2 ;;
|
||
--repo) REPO="$2"; shift 2 ;;
|
||
--assignee) ASSIGNEE="$2"; shift 2 ;;
|
||
--dry-run) DRY_RUN="true"; shift ;;
|
||
--help|-h) usage ;;
|
||
*) log_err "Unknown arg: $1"; usage ;;
|
||
esac
|
||
done
|
||
|
||
[[ -z "$ISSUE_NUMBER" ]] && { log_err "--issue-number is required"; usage; }
|
||
|
||
check_auth
|
||
require_owner_repo
|
||
|
||
# 10 个标准中文分类标签(必须和仓库实际标签名一致)
|
||
STANDARD_LABELS="缺陷 功能 文档 任务 测试 支持 重复 疑问 协助 搁置"
|
||
|
||
# ================================================================
|
||
log_title "Issue Triage: #$ISSUE_NUMBER ($OWNER/$REPO)"
|
||
|
||
# ── Phase 1: 拉取 Issue 详情 ─────────────────────────────────────
|
||
log_step "Fetching issue #$ISSUE_NUMBER details..."
|
||
ISSUE_JSON=$(gl_check issue +view --owner "$OWNER" --repo "$REPO" --number "$ISSUE_NUMBER")
|
||
[[ -z "$ISSUE_JSON" ]] && { log_err "Failed to fetch issue #$ISSUE_NUMBER"; exit 1; }
|
||
|
||
ISSUE_TITLE=$(echo "$ISSUE_JSON" | jq -r '.data.subject // .data.title // "N/A"')
|
||
ISSUE_DESC=$(echo "$ISSUE_JSON" | jq -r '.data.description // ""' | head -c 2000)
|
||
ISSUE_AUTHOR=$(echo "$ISSUE_JSON" | jq -r '.data.author.login // .data.author.username // ""')
|
||
ISSUE_STATE=$(echo "$ISSUE_JSON" | jq -r '.data.status.name // .data.state.name // .data.state // "N/A"')
|
||
|
||
EXISTING_TAGS=$(echo "$ISSUE_JSON" | jq -r '[.data.tags[]?.name // ""] | join(", ")' 2>/dev/null || echo "")
|
||
[[ -n "$EXISTING_TAGS" ]] && log_info "Already tagged: $EXISTING_TAGS"
|
||
|
||
log_ok "Issue \"$ISSUE_TITLE\" by @$ISSUE_AUTHOR"
|
||
|
||
# ── Phase 2: 动态收集标签 ID 映射 ─────────────────────────────────
|
||
log_step "Discovering label IDs..."
|
||
|
||
ID_MAP_FILE=$(mktemp)
|
||
|
||
# 优先从项目 issue_tags API 获取
|
||
TAGS_JSON=$(gl_run api GET "/v1/$OWNER/$REPO/issue_tags" 2>/dev/null || true)
|
||
if [[ -n "$TAGS_JSON" ]]; then
|
||
echo "$TAGS_JSON" | jq -r '(.data.issue_tags // .data // [])[]? | "\(.id)|\(.name)"' 2>/dev/null >> "$ID_MAP_FILE" || true
|
||
fi
|
||
|
||
# 补充:从已有 issue 的 tags 字段收集(覆盖 issue_tags API 未返回的情况)
|
||
for state in open closed; do
|
||
SAMPLE=$(gl_run issue +list --owner "$OWNER" --repo "$REPO" --state "$state" --limit 50 2>/dev/null || true)
|
||
echo "$SAMPLE" | jq -r '(.data.issues // .data // [])[]?.tags[]? | "\(.id)|\(.name)"' 2>/dev/null >> "$ID_MAP_FILE" || true
|
||
done
|
||
|
||
declare -A TAG_ID
|
||
while IFS='|' read -r tid tname || [[ -n "$tid" ]]; do
|
||
[[ -n "$tid" && -n "$tname" ]] && TAG_ID["$tname"]="$tid"
|
||
done < "$ID_MAP_FILE"
|
||
rm -f "$ID_MAP_FILE"
|
||
|
||
FOUND=""; MISSING=""
|
||
for lbl in $STANDARD_LABELS; do
|
||
if [[ -n "${TAG_ID[$lbl]:-}" ]]; then
|
||
FOUND="$FOUND $lbl(${TAG_ID[$lbl]})"
|
||
else
|
||
MISSING="$MISSING $lbl"
|
||
fi
|
||
done
|
||
log_ok "Found tags:${FOUND:- (none)}"
|
||
[[ -n "$MISSING" ]] && log_warn "Not in repo yet:${MISSING}"
|
||
|
||
# ── Phase 3: 分类(关键词 → 中文标签)─────────────────────────────
|
||
log_step "Analyzing issue content..."
|
||
|
||
COMBINED=$(echo "$ISSUE_TITLE $ISSUE_DESC" | tr '[:upper:]' '[:lower:]')
|
||
CHOSEN_LABEL=""
|
||
CONFIDENCE=""
|
||
|
||
# 优先匹配具体意图,再匹配通用
|
||
if echo "$COMBINED" | grep -qiE 'bug|error|crash|fault|fix|错误|异常|崩溃|缺陷|修复|故障'; then
|
||
CHOSEN_LABEL="缺陷"
|
||
elif echo "$COMBINED" | grep -qiE 'feature|enhancement|add|新增|建议|功能|特性|新功能'; then
|
||
CHOSEN_LABEL="功能"
|
||
elif echo "$COMBINED" | grep -qiE 'doc|readme|guide|wiki|文档|说明|教程'; then
|
||
CHOSEN_LABEL="文档"
|
||
elif echo "$COMBINED" | grep -qiE 'test|测试|用例|覆盖'; then
|
||
CHOSEN_LABEL="测试"
|
||
elif echo "$COMBINED" | grep -qiE 'duplicate|重复|重复的|和.*重复'; then
|
||
CHOSEN_LABEL="重复"
|
||
elif echo "$COMBINED" | grep -qiE 'question|疑问|不确定|讨论|澄清'; then
|
||
CHOSEN_LABEL="疑问"
|
||
elif echo "$COMBINED" | grep -qiE 'help|协助|帮助|协作|请求帮助'; then
|
||
CHOSEN_LABEL="协助"
|
||
elif echo "$COMBINED" | grep -qiE 'postpone|wontfix|暂缓|搁置|低优|不重要|不紧急'; then
|
||
CHOSEN_LABEL="搁置"
|
||
elif echo "$COMBINED" | grep -qiE 'support|兼容|环境|依赖|支持'; then
|
||
CHOSEN_LABEL="支持"
|
||
elif echo "$COMBINED" | grep -qiE 'task|todo|任务|待办|计划'; then
|
||
CHOSEN_LABEL="任务"
|
||
elif echo "$COMBINED" | grep -qiE 'how|怎么|如何|求助|使用|用法'; then
|
||
CHOSEN_LABEL="疑问"
|
||
fi
|
||
|
||
if [[ -n "$CHOSEN_LABEL" ]]; then
|
||
CONFIDENCE="keyword"
|
||
log_ok "Classified: $CHOSEN_LABEL"
|
||
else
|
||
log_info "No matching label — skipping"
|
||
fi
|
||
|
||
# ── Phase 4: 打标签(raw API PATCH tags)─────────────────────────
|
||
if [[ -z "$CHOSEN_LABEL" ]]; then
|
||
log_info "No label assigned"
|
||
elif [[ "$DRY_RUN" == "true" ]]; then
|
||
log_warn "[DRY RUN] Would tag #$ISSUE_NUMBER with '$CHOSEN_LABEL'"
|
||
else
|
||
TGT_ID="${TAG_ID[$CHOSEN_LABEL]:-}"
|
||
if [[ -z "$TGT_ID" ]]; then
|
||
log_warn "Label '$CHOSEN_LABEL' not found in repo tags — create it on website first"
|
||
log_info " https://www.gitlink.org.cn/$OWNER/$REPO/settings/labels"
|
||
else
|
||
log_step "Tagging with '$CHOSEN_LABEL' (ID:$TGT_ID)..."
|
||
|
||
|
||
TAG_RESP=$(gl_run api PATCH "/v1/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||
--body "{\"issue_tag_ids\":[$TGT_ID]}" 2>&1) || true
|
||
|
||
if echo "$TAG_RESP" | jq -e '.ok == true' &>/dev/null; then
|
||
log_ok "Tagged: $CHOSEN_LABEL"
|
||
else
|
||
ERR=$(echo "$TAG_RESP" | jq -r '.error.message // "unknown"' 2>/dev/null || echo "unknown")
|
||
log_warn "Tag failed: $ERR"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── Phase 5: 分配责任人 ──────────────────────────────────────────
|
||
TARGET_ASSIGNEE="${ASSIGNEE:-$ISSUE_AUTHOR}"
|
||
|
||
log_step "Assigning..."
|
||
if [[ -z "$TARGET_ASSIGNEE" ]]; then
|
||
log_warn "No assignee, skipping"
|
||
elif [[ "$DRY_RUN" == "true" ]]; then
|
||
log_warn "[DRY RUN] Would assign @$TARGET_ASSIGNEE"
|
||
else
|
||
# Resolve login name to numeric user ID
|
||
ASSIGNEE_ID=""
|
||
if [[ "$TARGET_ASSIGNEE" =~ ^[0-9]+$ ]]; then
|
||
ASSIGNEE_ID="$TARGET_ASSIGNEE"
|
||
else
|
||
USER_JSON=$(gl_run api GET "/users/$TARGET_ASSIGNEE" 2>/dev/null || true)
|
||
if [[ -n "$USER_JSON" ]]; then
|
||
ASSIGNEE_ID=$(echo "$USER_JSON" | jq -r '.data.id // .data.user_id // ""' 2>/dev/null || echo "")
|
||
fi
|
||
fi
|
||
|
||
if [[ -z "$ASSIGNEE_ID" ]]; then
|
||
log_warn "Cannot resolve user ID for '$TARGET_ASSIGNEE' — skipping assign"
|
||
else
|
||
BODY="{\"assigner_ids\":[$ASSIGNEE_ID]}"
|
||
ARESP=$(gl_run api PATCH "/v1/$OWNER/$REPO/issues/$ISSUE_NUMBER" --body "$BODY" 2>&1) || true
|
||
if echo "$ARESP" | jq -e '.ok == true' &>/dev/null; then
|
||
log_ok "Assigned: @$TARGET_ASSIGNEE (ID:$ASSIGNEE_ID)"
|
||
else
|
||
ERR2=$(echo "$ARESP" | jq -r '.error.message // "unknown"' 2>/dev/null || echo "unknown")
|
||
log_warn "Assign failed: $ERR2"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── Complete ──────────────────────────────────────────────────────
|
||
log_title "Triage Complete"
|
||
echo -e "${GREEN} Issue: #$ISSUE_NUMBER - $ISSUE_TITLE${NC}"
|
||
echo -e "${GREEN} Author: @$ISSUE_AUTHOR${NC}"
|
||
echo -e "${GREEN} Label: ${CHOSEN_LABEL:-无}${NC}"
|
||
echo -e "${GREEN} Assignee: @$TARGET_ASSIGNEE${NC}"
|