diff --git a/workflows/01-community-ops.sh b/workflows/01-community-ops.sh new file mode 100644 index 00000000..db653833 --- /dev/null +++ b/workflows/01-community-ops.sh @@ -0,0 +1,295 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────── +# Scenario 1: Community Operations Automation +# Flow: Issue auto-classify → Assign responsible → Weekly report → Release notes +# +# Commands/Skills chained: +# 1. issue +list -- fetch open issues +# 2. issue +view -- read issue details +# 3. issue +batch-label -- add classification labels +# 4. issue +batch-assign -- assign responsible person +# 5. pr +list -- collect merged PRs for weekly report +# 6. wiki +create -- publish community weekly report +# 7. release +create -- publish release notes +# ───────────────────────────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +usage() { + echo "Usage: $0 --owner OWNER --repo REPO [--week WEEKS_AGO] [--dry-run]" + echo "" + echo " --owner OWNER Repository owner (org or user)" + echo " --repo REPO Repository name" + echo " --week N Generate report for N weeks ago (default: 0 = this week)" + echo " --dry-run Preview actions without executing" + exit 1 +} + +WEEKS_AGO=0 +DRY_RUN=false +OWNER="" +REPO="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --owner) OWNER="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --week) WEEKS_AGO="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + --help|-h) usage ;; + *) log_err "Unknown arg: $1"; usage ;; + esac +done + +check_auth +require_owner_repo + +# ───────────────────────────────────────────────────────────────────── +log_title "Phase 1: Issue Auto-Classification" +# ───────────────────────────────────────────────────────────────────── + +log_step "Fetching open issues..." +ISSUES_JSON=$(gl_check issue +list --owner "$OWNER" --repo "$REPO" --state open --limit 100) +ISSUE_COUNT=$(echo "$ISSUES_JSON" | jq '.data.issues | length') +log_ok "Found $ISSUE_COUNT open issues" + +if [[ "$ISSUE_COUNT" -gt 0 ]]; then + # Classify each issue by keywords in title/description + declare -A LABEL_MAP=() + BUG_IDS=() + FEATURE_IDS=() + QUESTION_IDS=() + DOCS_IDS=() + + log_step "Classifying issues by content..." + + for i in $(seq 0 $((ISSUE_COUNT - 1))); do + ISSUE_ID=$(echo "$ISSUES_JSON" | jq -r ".data.issues[$i].id") + ISSUE_TITLE=$(echo "$ISSUES_JSON" | jq -r ".data.issues[$i].subject // .data.issues[$i].title // \"\"") + ISSUE_DESC=$(echo "$ISSUES_JSON" | jq -r ".data.issues[$i].description // \"\"" | head -c 500) + COMBINED="$ISSUE_TITLE $ISSUE_DESC" + + # Keyword-based classification + if echo "$COMBINED" | grep -qiE 'bug|error|crash|fault|fix|修复|错误|异常|崩溃'; then + BUG_IDS+=("$ISSUE_ID") + log_info " #$ISSUE_ID → BUG: $ISSUE_TITLE" + elif echo "$COMBINED" | grep -qiE 'feature|新增|建议|enhancement|add|support|功能'; then + FEATURE_IDS+=("$ISSUE_ID") + log_info " #$ISSUE_ID → FEATURE: $ISSUE_TITLE" + elif echo "$COMBINED" | grep -qiE 'how|怎么|如何|question|help|\?|?'; then + QUESTION_IDS+=("$ISSUE_ID") + log_info " #$ISSUE_ID → QUESTION: $ISSUE_TITLE" + elif echo "$COMBINED" | grep -qiE 'doc|文档|readme|说明|guide'; then + DOCS_IDS+=("$ISSUE_ID") + log_info " #$ISSUE_ID → DOCS: $ISSUE_TITLE" + else + log_info " #$ISSUE_ID → UNCATEGORIZED: $ISSUE_TITLE" + fi + done + + divider + log_info "Classification summary:" + log_info " Bugs: ${#BUG_IDS[@]}" + log_info " Features: ${#FEATURE_IDS[@]}" + log_info " Questions: ${#QUESTION_IDS[@]}" + log_info " Docs: ${#DOCS_IDS[@]}" + + # Apply labels via batch-label + if [[ ${#BUG_IDS[@]} -gt 0 ]]; then + log_step "Labeling bug issues..." + for bid in "${BUG_IDS[@]}"; do + gl_run issue +label-add --owner "$OWNER" --repo "$REPO" --number "$bid" --labels "bug" > /dev/null 2>&1 || true + done + log_ok "Labeled ${#BUG_IDS[@]} bug issues" + fi + + if [[ ${#FEATURE_IDS[@]} -gt 0 ]]; then + log_step "Labeling feature issues..." + for fid in "${FEATURE_IDS[@]}"; do + gl_run issue +label-add --owner "$OWNER" --repo "$REPO" --number "$fid" --labels "feature" > /dev/null 2>&1 || true + done + log_ok "Labeled ${#FEATURE_IDS[@]} feature issues" + fi + + if [[ ${#QUESTION_IDS[@]} -gt 0 ]]; then + log_step "Labeling question issues..." + for qid in "${QUESTION_IDS[@]}"; do + gl_run issue +label-add --owner "$OWNER" --repo "$REPO" --number "$qid" --labels "question" > /dev/null 2>&1 || true + done + log_ok "Labeled ${#QUESTION_IDS[@]} question issues" + fi + + if [[ ${#DOCS_IDS[@]} -gt 0 ]]; then + log_step "Labeling docs issues..." + for did in "${DOCS_IDS[@]}"; do + gl_run issue +label-add --owner "$OWNER" --repo "$REPO" --number "$did" --labels "documentation" > /dev/null 2>&1 || true + done + log_ok "Labeled ${#DOCS_IDS[@]} docs issues" + fi +fi + +# ───────────────────────────────────────────────────────────────────── +log_title "Phase 2: Assign Responsible Persons" +# ───────────────────────────────────────────────────────────────────── + +log_step "Fetching repo members for assignment..." +MEMBERS_JSON=$(gl_run repo +members --owner "$OWNER" --repo "$REPO" --limit 50) +MEMBER_COUNT=$(echo "$MEMBERS_JSON" | jq '.data.members | length' 2>/dev/null || echo "0") + +if [[ "$MEMBER_COUNT" -gt 0 ]]; then + # Assign bugs to first member, features to second, etc. (round-robin) + MEMBER_LOGINS=() + for i in $(seq 0 $((MEMBER_COUNT - 1))); do + LOGIN=$(echo "$MEMBERS_JSON" | jq -r ".data.members[$i].login // .data.members[$i].username // empty") + [[ -n "$LOGIN" ]] && MEMBER_LOGINS+=("$LOGIN") + done + + if [[ ${#MEMBER_LOGINS[@]} -gt 0 ]]; then + assign_issues() { + local label="$1" + shift + local ids=("$@") + local member_idx=0 + for id in "${ids[@]}"; do + local assignee="${MEMBER_LOGINS[$((member_idx % ${#MEMBER_LOGINS[@]}))]}" + gl_run issue +update --owner "$OWNER" --repo "$REPO" --number "$id" --assignee "$assignee" > /dev/null 2>&1 || true + log_info " Assigned #$id → @$assignee" + ((member_idx++)) + done + } + + log_step "Assigning bug issues..." + assign_issues "bug" "${BUG_IDS[@]}" 2>/dev/null || true + log_step "Assigning feature issues..." + assign_issues "feature" "${FEATURE_IDS[@]}" 2>/dev/null || true + log_ok "Assignment complete" + fi +else + log_warn "No repo members found, skipping assignment" +fi + +# ───────────────────────────────────────────────────────────────────── +log_title "Phase 3: Generate Community Weekly Report" +# ───────────────────────────────────────────────────────────────────── + +WEEK_START=$(date -d "$((WEEKS_AGO * 7)) days ago" +%Y-%m-%d 2>/dev/null || date_today) +WEEK_END=$(date_today) + +log_step "Collecting weekly data (week of $WEEK_START)..." + +# Closed issues this week +CLOSED_ISSUES=$(gl_run issue +list --owner "$OWNER" --repo "$REPO" --state closed --limit 100) +CLOSED_COUNT=$(echo "$CLOSED_ISSUES" | jq '.data.issues | length' 2>/dev/null || echo "0") + +# Merged PRs this week +MERGED_PRS=$(gl_run pr +list --owner "$OWNER" --repo "$REPO" --state merged --limit 100) +MERGED_COUNT=$(echo "$MERGED_PRS" | jq '.data.issues | length' 2>/dev/null || echo "0") + +# New issues +NEW_ISSUES_COUNT=$ISSUE_COUNT + +# Build weekly report +REPORT_TITLE="Community Weekly Report: $WEEK_START ~ $WEEK_END" +REPORT_BODY="# $REPORT_TITLE + +## Summary +- New Issues: **$NEW_ISSUES_COUNT** +- Closed Issues: **$CLOSED_COUNT** +- Merged PRs: **$MERGED_COUNT** + +## Issue Classification +| Type | Count | +|------|-------| +| Bug | ${#BUG_IDS[@]} | +| Feature | ${#FEATURE_IDS[@]} | +| Question | ${#QUESTION_IDS[@]} | +| Docs | ${#DOCS_IDS[@]} | + +## Highlights +- Auto-classified and labeled $(( ${#BUG_IDS[@]} + ${#FEATURE_IDS[@]} + ${#QUESTION_IDS[@]} + ${#DOCS_IDS[@]} )) issues +- Assigned responsible persons for bug and feature issues + +--- +*Auto-generated by gitlink-cli community-ops workflow*" + +log_ok "Weekly report generated" +echo "" +echo "$REPORT_BODY" + +# Publish to Wiki +log_step "Publishing weekly report to Wiki..." +WIKI_RESULT=$(gl_run wiki +create --owner "$OWNER" --repo "$REPO" \ + --title "$REPORT_TITLE" \ + --content "$REPORT_BODY" 2>&1) || true + +if [[ "$(json_ok "$WIKI_RESULT")" == "true" ]]; then + log_ok "Weekly report published to Wiki" +else + log_warn "Wiki publish may have failed (wiki module might not be enabled)" +fi + +# ───────────────────────────────────────────────────────────────────── +log_title "Phase 4: Auto-Publish Release Notes" +# ───────────────────────────────────────────────────────────────────── + +log_step "Collecting recent changes for release notes..." + +# Get recent commits via compare API +TAG_NAME="weekly-$(date +%Y%m%d)" +RELEASE_NAME="Weekly Release $(date +%Y-%m-%d)" + +# Build release notes from closed issues and merged PRs +RELEASE_BODY="# Release Notes - $(date +%Y-%m-%d) + +## Merged PRs ($MERGED_COUNT)" + +if [[ "$MERGED_COUNT" -gt 0 ]]; then + for i in $(seq 0 $((MERGED_COUNT > 10 ? 9 : MERGED_COUNT - 1))); do + PR_TITLE=$(echo "$MERGED_PRS" | jq -r ".data.issues[$i].subject // .data.issues[$i].title // \"\"") + PR_NUM=$(echo "$MERGED_PRS" | jq -r ".data.issues[$i].id // .data.issues[$i].number // \"\"") + RELEASE_BODY+=$'\n'"- #$PR_NUM $PR_TITLE" + done +fi + +RELEASE_BODY+=" + +## Closed Issues ($CLOSED_COUNT)" + +if [[ "$CLOSED_COUNT" -gt 0 ]]; then + for i in $(seq 0 $((CLOSED_COUNT > 10 ? 9 : CLOSED_COUNT - 1))); do + ISSUE_TITLE=$(echo "$CLOSED_ISSUES" | jq -r ".data.issues[$i].subject // .data.issues[$i].title // \"\"") + ISSUE_NUM=$(echo "$CLOSED_ISSUES" | jq -r ".data.issues[$i].id // .data.issues[$i].number // \"\"") + RELEASE_BODY+=$'\n'"- #$ISSUE_NUM $ISSUE_TITLE" + done +fi + +RELEASE_BODY+=" + +--- +*Auto-generated by gitlink-cli community-ops workflow*" + +log_step "Creating release: $TAG_NAME..." +RELEASE_RESULT=$(gl_run release +create --owner "$OWNER" --repo "$REPO" \ + --tag "$TAG_NAME" \ + --name "$RELEASE_NAME" \ + --body "$RELEASE_BODY" 2>&1) || true + +if [[ "$(json_ok "$RELEASE_RESULT")" == "true" ]]; then + log_ok "Release $TAG_NAME created successfully" +else + log_warn "Release creation may have failed (tag might already exist)" + log_info "You can manually create with: gitlink-cli release +create --owner $OWNER --repo $REPO --tag $TAG_NAME --name '$RELEASE_NAME'" +fi + +# ───────────────────────────────────────────────────────────────────── +log_title "Community Operations Complete" +# ───────────────────────────────────────────────────────────────────── + +echo -e "${GREEN}Summary:${NC}" +echo " Issues classified: $(( ${#BUG_IDS[@]} + ${#FEATURE_IDS[@]} + ${#QUESTION_IDS[@]} + ${#DOCS_IDS[@]} ))" +echo " Closed this week: $CLOSED_COUNT" +echo " Merged PRs: $MERGED_COUNT" +echo " Weekly report: Published to Wiki" +echo " Release notes: $TAG_NAME" +echo "" diff --git a/workflows/02-code-quality-gatekeeper.sh b/workflows/02-code-quality-gatekeeper.sh new file mode 100644 index 00000000..53c4a0c6 --- /dev/null +++ b/workflows/02-code-quality-gatekeeper.sh @@ -0,0 +1,484 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────── +# Scenario 2: Code Quality Gatekeeper +# Flow: PR submit → Load Skill → Auto Review → Check CI → Auto-merge +# +# Commands/Skills chained: +# 1. pr +list -- list open PRs +# 2. pr +view -- get PR details +# 3. pr +files -- get changed files +# 4. pr +diff -- get diff content +# 5. gitlink-code-review -- AI code review (skill-driven) +# 6. api POST .../reviews -- post review comment with scores +# 7. ci +builds -- check CI build status +# 8. pr +merge -- auto-merge if quality passes threshold +# ───────────────────────────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +usage() { + echo "Usage: $0 --owner OWNER --repo REPO [--pr-id ID] [--threshold SCORE] [--dry-run]" + echo "" + echo " --owner OWNER Repository owner" + echo " --repo REPO Repository name" + echo " --pr-id ID Specific PR to review (default: all open PRs)" + echo " --threshold SCORE Min quality score to auto-merge (default: 80)" + echo " --dry-run Preview actions without executing" + exit 1 +} + +THRESHOLD=80 +DRY_RUN=false +OWNER="" +REPO="" +PR_ID="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --owner) OWNER="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --pr-id) PR_ID="$2"; shift 2 ;; + --threshold) THRESHOLD="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + --help|-h) usage ;; + *) log_err "Unknown arg: $1"; usage ;; + esac +done + +check_auth +require_owner_repo + +# ── Review a single PR ─────────────────────────────────────────────── +review_pr() { + local pr_id="$1" + + log_title "Reviewing PR #$pr_id" + + # Initialize arrays + ISSUES_FOUND=() + AI_POSITIVE=() + AI_RECOMMENDATIONS=() + + # Step 1: Get PR details + log_step "Fetching PR details..." + PR_JSON=$(gl_check pr +view --owner "$OWNER" --repo "$REPO" --id "$pr_id") + PR_TITLE=$(echo "$PR_JSON" | jq -r '.data.title // .data.subject // "N/A"') + PR_STATE=$(echo "$PR_JSON" | jq -r '.data.state // .data.status // "N/A"') + PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.data.author.login // .data.author.username // .data.user.login // "N/A"') + log_ok "PR #$pr_id: \"$PR_TITLE\" by @$PR_AUTHOR (state: $PR_STATE)" + + # Step 2: Get changed files + log_step "Fetching changed files..." + FILES_JSON=$(gl_run pr +files --owner "$OWNER" --repo "$REPO" --id "$pr_id") + FILE_COUNT=0 + if echo "$FILES_JSON" | jq empty 2>/dev/null; then + FILE_COUNT=$(echo "$FILES_JSON" | jq '.data.files | length' 2>/dev/null || echo "0") + fi + log_ok "Changed files: $FILE_COUNT" + + # List changed files + if [[ "$FILE_COUNT" -gt 0 ]] && [[ "$FILE_COUNT" != "null" ]]; then + for i in $(seq 0 $((FILE_COUNT - 1))); do + FNAME=$(echo "$FILES_JSON" | jq -r ".data.files[$i].name // .data.files[$i].filename // \"unknown\"" 2>/dev/null || echo "unknown") + echo " $FNAME" + done + fi + + # Step 3: Get diff (extract file names and content from diff response) + log_step "Fetching diff..." + DIFF_JSON=$(gl_run pr +diff --owner "$OWNER" --repo "$REPO" --id "$pr_id") + DIFF_CONTENT="" + if echo "$DIFF_JSON" | jq empty 2>/dev/null; then + DIFF_CONTENT=$(echo "$DIFF_JSON" | jq -r ' + [.data.files[]?.sections[]?.lines[]?.content // empty] | join("\n") + ' 2>/dev/null | head -c 5000 || true) + fi + DIFF_LINES=$(echo "$DIFF_CONTENT" | wc -l) + log_ok "Diff: $DIFF_LINES lines" + + # Step 4: AI-powered code review + log_step "AI analyzing code quality..." + + # Build file list string + FILE_LIST="" + if [[ "$FILE_COUNT" -gt 0 ]] && [[ "$FILE_COUNT" != "null" ]]; then + for i in $(seq 0 $((FILE_COUNT - 1))); do + FNAME=$(echo "$FILES_JSON" | jq -r ".data.files[$i].name // .data.files[$i].filename // \"unknown\"" 2>/dev/null || echo "unknown") + FILE_LIST+="- $FNAME"$'\n' + done + fi + + # Truncate diff to fit within context limits + DIFF_TRUNCATED=$(echo "$DIFF_CONTENT" | head -c 4000) + + # ── Load gitlink-code-review skill (concise version) ────────── + SKILL_DIR="$SCRIPT_DIR/../skills/gitlink-code-review" + SKILL_DIMENSIONS="" + if [[ -f "$SKILL_DIR/SKILL.md" ]]; then + SKILL_DIMENSIONS=$(sed -n '/^## 📊 审查维度/,/^## 🔧 使用方式/p' "$SKILL_DIR/SKILL.md" | grep '^\- \*\*' | head -20) + fi + + REVIEW_PROMPT="你是代码审查专家。请按 gitlink-code-review skill 的审查维度分析以下 PR。 + +## 审查维度与检查项 + +${SKILL_DIMENSIONS:-1. 代码质量: 复杂度、命名、注释、格式 +2. 安全性: SQL注入、XSS、敏感信息、认证、输入验证 +3. 性能: 循环效率、资源泄漏、N+1查询、内存 +4. 可维护性: 代码重复、职责单一、依赖耦合、测试覆盖} + +## 评分标准 +- 90-100: 优秀,可直接合并 +- 75-89: 良好,建议合并 +- 60-74: 一般,需要改进 +- <60: 较差,不建议合并 + +## 问题严重级别 +- CRITICAL: 阻止合并 +- HIGH: 强烈建议修复 +- MEDIUM: 建议修复 +- LOW: 可选修复 + +## PR 数据 + +PR 标题: $PR_TITLE +变更文件: +$FILE_LIST +代码差异: +$DIFF_TRUNCATED + +## 输出要求 + +请严格按以下 JSON 格式输出,不要输出其他内容: +{\"total\": <0-100>, \"quality\": <0-25>, \"security\": <0-25>, \"performance\": <0-25>, \"maintainability\": <0-25>, \"issues\": [{\"severity\": \"HIGH/MEDIUM/LOW\", \"category\": \"quality/security/performance/maintainability\", \"file\": \"文件路径\", \"rule\": \"规则名\", \"description\": \"问题描述\", \"suggestion\": \"修复建议\"}], \"positive_notes\": [{\"description\": \"优秀实践描述\"}], \"recommendations\": [\"改进建议1\"], \"verdict\": \"PASS或FAIL\"}" + + # Call Claude Code CLI for AI review + AI_AVAILABLE=false + if command -v claude &>/dev/null; then + log_info "Calling AI agent for code review (may take 30-60s)..." + PROMPT_FILE=$(mktemp) + AI_OUT_FILE=$(mktemp) + echo "$REVIEW_PROMPT" > "$PROMPT_FILE" + + # Run claude in a subshell to isolate from set -euo pipefail + AI_EXIT=0 + ( + timeout 120 claude -p --output-format json < "$PROMPT_FILE" > "$AI_OUT_FILE" 2>/dev/null + ) || AI_EXIT=$? + + if [[ $AI_EXIT -eq 0 ]] && [[ -s "$AI_OUT_FILE" ]]; then + # Parse Claude CLI JSON response + AI_RESULT=$(jq -r '.result // empty' "$AI_OUT_FILE" 2>/dev/null) + else + log_warn "AI call failed (exit: $AI_EXIT), falling back to keyword-based" + AI_RESULT="" + fi + rm -f "$PROMPT_FILE" "$AI_OUT_FILE" + + if [[ -n "$AI_RESULT" ]]; then + # Extract JSON block from AI response (may contain markdown wrapping) + # Use python for reliable JSON extraction from mixed content + AI_JSON="" + if command -v python3 &>/dev/null; then + AI_JSON=$(python3 -c " +import sys, json +text = sys.stdin.read() +# Find JSON by balanced brace matching +depth = 0 +start = -1 +results = [] +for i, c in enumerate(text): + if c == '{': + if depth == 0: + start = i + depth += 1 + elif c == '}': + depth -= 1 + if depth == 0 and start >= 0: + results.append(text[start:i+1]) + start = -1 +for m in reversed(results): + try: + obj = json.loads(m) + if 'total' in obj and 'verdict' in obj: + print(json.dumps(obj)) + break + except: pass +" <<< "$AI_RESULT" 2>/dev/null) + fi + # Fallback: simple grep extraction + if [[ -z "$AI_JSON" ]]; then + AI_JSON=$(echo "$AI_RESULT" | grep -oP '\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' | tail -1) + fi + + if [[ -n "$AI_JSON" ]] && echo "$AI_JSON" | jq empty 2>/dev/null; then + TOTAL_SCORE=$(echo "$AI_JSON" | jq -r '.total // 0' 2>/dev/null) + SCORE_QUALITY=$(echo "$AI_JSON" | jq -r '.quality // 0' 2>/dev/null) + SCORE_SECURITY=$(echo "$AI_JSON" | jq -r '.security // 0' 2>/dev/null) + SCORE_PERFORMANCE=$(echo "$AI_JSON" | jq -r '.performance // 0' 2>/dev/null) + SCORE_MAINTAINABILITY=$(echo "$AI_JSON" | jq -r '.maintainability // 0' 2>/dev/null) + AI_VERDICT=$(echo "$AI_JSON" | jq -r '.verdict // "PASS"' 2>/dev/null) + + # Extract structured issues array (objects with severity/category/description) + ISSUES_FOUND=() + ISSUE_COUNT=$(echo "$AI_JSON" | jq '.issues | length' 2>/dev/null || echo "0") + if [[ "$ISSUE_COUNT" -gt 0 ]] && [[ "$ISSUE_COUNT" != "null" ]]; then + for idx in $(seq 0 $((ISSUE_COUNT - 1))); do + # Support both structured objects and plain strings + issue=$(echo "$AI_JSON" | jq -r ' + if .issues['"$idx"'] | type == "object" then + "[" + (.issues['"$idx"'].severity // "?") + "] " + + (.issues['"$idx"'].category // "?") + ": " + + (.issues['"$idx"'].description // .issues['"$idx"'].rule // "unknown") + + (if .issues['"$idx"'].file then " (" + .issues['"$idx"'].file + ")" else "" end) + + (if .issues['"$idx"'].suggestion then " → " + .issues['"$idx"'].suggestion else "" end) + else + .issues['"$idx"'] // empty + end + ' 2>/dev/null) + [[ -n "$issue" ]] && ISSUES_FOUND+=("$issue") + done + fi + + # Extract positive notes and recommendations + AI_POSITIVE=() + POS_COUNT=$(echo "$AI_JSON" | jq '.positive_notes | length' 2>/dev/null || echo "0") + if [[ "$POS_COUNT" -gt 0 ]] && [[ "$POS_COUNT" != "null" ]]; then + for idx in $(seq 0 $((POS_COUNT - 1))); do + note=$(echo "$AI_JSON" | jq -r '.positive_notes['"$idx"'].description // empty' 2>/dev/null) + [[ -n "$note" ]] && AI_POSITIVE+=("$note") + done + fi + + AI_RECOMMENDATIONS=() + REC_COUNT=$(echo "$AI_JSON" | jq '.recommendations | length' 2>/dev/null || echo "0") + if [[ "$REC_COUNT" -gt 0 ]] && [[ "$REC_COUNT" != "null" ]]; then + for idx in $(seq 0 $((REC_COUNT - 1))); do + rec=$(echo "$AI_JSON" | jq -r '.recommendations['"$idx"'] // empty' 2>/dev/null) + [[ -n "$rec" ]] && AI_RECOMMENDATIONS+=("$rec") + done + fi + + AI_AVAILABLE=true + log_ok "AI review complete (verdict: $AI_VERDICT)" + else + log_warn "Could not parse AI response JSON, falling back to keyword-based" + fi + fi + fi + + # Fallback: keyword-based heuristics if AI is not available + if [[ "$AI_AVAILABLE" != "true" ]]; then + log_warn "AI not available, falling back to keyword-based analysis" + + SCORE_QUALITY=25 + SCORE_SECURITY=25 + SCORE_PERFORMANCE=25 + SCORE_MAINTAINABILITY=25 + ISSUES_FOUND=() + + if echo "$DIFF_CONTENT" | grep -qiE 'password|secret|token|api_key|apikey|private_key'; then + SCORE_SECURITY=$((SCORE_SECURITY - 15)) + ISSUES_FOUND+=("SECURITY: 检测到可能的硬编码凭证") + fi + if echo "$DIFF_CONTENT" | grep -qiE 'eval\(|exec\(|system\(|shell_exec|os\.system|subprocess\.call'; then + SCORE_SECURITY=$((SCORE_SECURITY - 10)) + ISSUES_FOUND+=("SECURITY: 检测到危险函数调用") + fi + if echo "$DIFF_CONTENT" | grep -qiE 'TODO|FIXME|HACK|XXX'; then + SCORE_QUALITY=$((SCORE_QUALITY - 5)) + ISSUES_FOUND+=("QUALITY: 存在 TODO/FIXME/HACK 注释") + fi + if echo "$DIFF_CONTENT" | grep -qiE 'SELECT \*|\.findAll\(\)|\.all\(\)'; then + SCORE_PERFORMANCE=$((SCORE_PERFORMANCE - 10)) + ISSUES_FOUND+=("PERFORMANCE: 可能的全表查询") + fi + if echo "$DIFF_CONTENT" | grep -qiE 'sleep\(|time\.sleep|Thread\.sleep'; then + SCORE_PERFORMANCE=$((SCORE_PERFORMANCE - 5)) + ISSUES_FOUND+=("PERFORMANCE: 检测到阻塞式 sleep") + fi + if [[ "$FILE_COUNT" -gt 20 ]]; then + SCORE_MAINTAINABILITY=$((SCORE_MAINTAINABILITY - 10)) + ISSUES_FOUND+=("MAINTAINABILITY: 变更文件数量过多 ($FILE_COUNT)") + fi + + TOTAL_SCORE=$((SCORE_QUALITY + SCORE_SECURITY + SCORE_PERFORMANCE + SCORE_MAINTAINABILITY)) + TOTAL_SCORE=$((TOTAL_SCORE < 0 ? 0 : TOTAL_SCORE)) + fi + + # Print review report + divider + if [[ "$AI_AVAILABLE" == "true" ]]; then + log_info "AI Review Report for PR #$pr_id" + else + log_info "Review Report for PR #$pr_id (keyword-based)" + fi + echo "" + echo " Overall Score: $TOTAL_SCORE / 100" + echo " Code Quality: $SCORE_QUALITY / 25" + echo " Security: $SCORE_SECURITY / 25" + echo " Performance: $SCORE_PERFORMANCE / 25" + echo " Maintainability: $SCORE_MAINTAINABILITY / 25" + echo "" + + if [[ ${#ISSUES_FOUND[@]} -gt 0 ]]; then + echo " Issues Found:" + for issue in "${ISSUES_FOUND[@]}"; do + echo " - $issue" + done + echo "" + fi + + if [[ "${#AI_POSITIVE[@]}" -gt 0 ]]; then + echo " Positive Notes:" + for note in "${AI_POSITIVE[@]}"; do + echo " + $note" + done + echo "" + fi + + if [[ "${#AI_RECOMMENDATIONS[@]}" -gt 0 ]]; then + echo " Recommendations:" + for rec in "${AI_RECOMMENDATIONS[@]}"; do + echo " > $rec" + done + echo "" + fi + + # Step 5: Post review comment + if [[ "$AI_AVAILABLE" == "true" ]]; then + REVIEW_HEADER="## AI Code Quality Review - PR #$pr_id" + else + REVIEW_HEADER="## Code Quality Review - PR #$pr_id (keyword-based)" + fi + + REVIEW_BODY="$REVIEW_HEADER + +### Scores +| Dimension | Score | Max | +|-----------|-------|-----| +| Code Quality | $SCORE_QUALITY | 25 | +| Security | $SCORE_SECURITY | 25 | +| Performance | $SCORE_PERFORMANCE | 25 | +| Maintainability | $SCORE_MAINTAINABILITY | 25 | +| **Total** | **$TOTAL_SCORE** | **100** | + +### Issues Found" + + if [[ ${#ISSUES_FOUND[@]} -gt 0 ]]; then + for issue in "${ISSUES_FOUND[@]}"; do + REVIEW_BODY+=$'\n'"- $issue" + done + else + REVIEW_BODY+=$'\n'"No issues found." + fi + + if [[ "${#AI_POSITIVE[@]}" -gt 0 ]]; then + REVIEW_BODY+=$'\n'$'\n'"### Positive Notes" + for note in "${AI_POSITIVE[@]}"; do + REVIEW_BODY+=$'\n'"- $note" + done + fi + + if [[ "${#AI_RECOMMENDATIONS[@]}" -gt 0 ]]; then + REVIEW_BODY+=$'\n'$'\n'"### Recommendations" + for rec in "${AI_RECOMMENDATIONS[@]}"; do + REVIEW_BODY+=$'\n'"- $rec" + done + fi + + REVIEW_BODY+=" + +### Verdict +$(if [[ $TOTAL_SCORE -ge $THRESHOLD ]]; then echo "**PASS** - Score $TOTAL_SCORE >= threshold $THRESHOLD. Ready to merge."; else echo "**FAIL** - Score $TOTAL_SCORE < threshold $THRESHOLD. Please address the issues above."; fi) + +--- +*Auto-reviewed by gitlink-cli code-quality-gatekeeper workflow (skill: gitlink-code-review)*" + + log_step "Posting review comment..." + REVIEW_RESULT=$(gl_run api POST "/$OWNER/$REPO/pulls/$pr_id/reviews" \ + --body "{\"body\": $(echo "$REVIEW_BODY" | jq -Rs .), \"event\": \"$(if [[ $TOTAL_SCORE -ge $THRESHOLD ]]; then echo "APPROVE"; else echo "COMMENT"; fi)\"}" 2>&1) || true + + if [[ "$(json_ok "$REVIEW_RESULT")" == "true" ]]; then + log_ok "Review posted" + else + log_warn "Review post may have failed (review API might not be available)" + fi + + # Step 6: Check CI status (API may not be available) + log_step "Checking CI build status..." + CI_JSON=$(gl_run ci +builds --owner "$OWNER" --repo "$REPO") + CI_COUNT=$(echo "$CI_JSON" | jq '.data.builds // .data | length' 2>/dev/null || echo "0") + CI_PASSED=true + + if [[ "$CI_COUNT" -gt 0 ]] && [[ "$CI_COUNT" != "null" ]]; then + for i in $(seq 0 $((CI_COUNT - 1))); do + CI_STATUS=$(echo "$CI_JSON" | jq -r ".data.builds[$i].status // .data.builds[$i].state // .data[$i].status // .data[$i].state // \"unknown\"") + CI_NAME=$(echo "$CI_JSON" | jq -r ".data.builds[$i].name // .data[$i].name // \"build\"") + if [[ "$CI_STATUS" != "success" && "$CI_STATUS" != "passed" && "$CI_STATUS" != "completed" ]]; then + CI_PASSED=false + log_warn "CI '$CI_NAME' status: $CI_STATUS" + else + log_ok "CI '$CI_NAME' status: $CI_STATUS" + fi + done + else + log_info "No CI builds found" + fi + + # Step 7: Auto-merge if quality passes + if [[ $TOTAL_SCORE -ge $THRESHOLD && "$CI_PASSED" == "true" ]]; then + log_step "Quality score $TOTAL_SCORE >= $THRESHOLD and CI passed" + if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would auto-merge PR #$pr_id" + else + log_step "Auto-merging PR #$pr_id..." + MERGE_RESULT=$(gl_run pr +merge --owner "$OWNER" --repo "$REPO" --id "$pr_id" --method merge 2>&1) || true + if [[ "$(json_ok "$MERGE_RESULT")" == "true" ]]; then + log_ok "PR #$pr_id merged successfully!" + else + log_err "Auto-merge failed: $(json_error "$MERGE_RESULT")" + fi + fi + else + log_warn "PR #$pr_id not auto-merged (score: $TOTAL_SCORE, threshold: $THRESHOLD, CI passed: $CI_PASSED)" + fi + + echo "" + return 0 +} + +# ── Main ───────────────────────────────────────────────────────────── +log_title "Code Quality Gatekeeper" + +if [[ -n "$PR_ID" ]]; then + # Review specific PR + review_pr "$PR_ID" +else + # Review all open PRs + log_step "Fetching open PRs..." + PRS_JSON=$(gl_check pr +list --owner "$OWNER" --repo "$REPO" --state open --limit 50) + PR_COUNT=$(echo "$PRS_JSON" | jq '.data.issues | length') + log_ok "Found $PR_COUNT open PRs" + + if [[ "$PR_COUNT" -eq 0 ]]; then + log_info "No open PRs to review" + exit 0 + fi + + REVIEWED=0 + PASSED=0 + FAILED=0 + + for i in $(seq 0 $((PR_COUNT - 1))); do + pid=$(echo "$PRS_JSON" | jq -r ".data.issues[$i].pull_request_number // .data.issues[$i].id // empty") + [[ -z "$pid" ]] && continue + review_pr "$pid" + ((REVIEWED++)) + done + + log_title "Gatekeeper Summary" + echo " PRs Reviewed: $REVIEWED" + echo " Threshold: $THRESHOLD" +fi diff --git a/workflows/03-project-init.sh b/workflows/03-project-init.sh new file mode 100644 index 00000000..a76e550b --- /dev/null +++ b/workflows/03-project-init.sh @@ -0,0 +1,347 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────── +# Scenario 3: One-Click Project Initialization +# Flow: Input description → Create repo → README/LICENSE/CI → Issues → Release +# +# Commands/Skills chained: +# 1. repo +create -- create repository +# 2. wiki +create -- create README wiki page +# 3. wiki +create -- create CONTRIBUTING guide +# 4. issue +create -- create initial issues +# 5. branch +protect -- protect default branch +# 6. release +create -- create initial release +# ───────────────────────────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +usage() { + echo "Usage: $0 --owner OWNER --name REPO_NAME --description DESC [--lang LANG] [--private] [--dry-run]" + echo "" + echo " --owner OWNER Repository owner (org or user)" + echo " --name REPO_NAME Repository name" + echo " --description DESC Repository description" + echo " --lang LANG Primary language: go|python|node|java (default: go)" + echo " --private Make repository private" + echo " --dry-run Preview actions without executing" + exit 1 +} + +LANG="go" +DRY_RUN=false +OWNER="" +REPO_NAME="" +DESCRIPTION="" +PRIVATE="false" + +while [[ $# -gt 0 ]]; do + case "$1" in + --owner) OWNER="$2"; shift 2 ;; + --name) REPO_NAME="$2"; shift 2 ;; + --description) DESCRIPTION="$2"; shift 2 ;; + --lang) LANG="$2"; shift 2 ;; + --private) PRIVATE="true"; shift ;; + --dry-run) DRY_RUN="true"; shift ;; + --help|-h) usage ;; + *) log_err "Unknown arg: $1"; usage ;; + esac +done + +if [[ -z "$OWNER" || -z "$REPO_NAME" || -z "$DESCRIPTION" ]]; then + log_err "Missing required parameters: --owner, --name, --description" + usage +fi + +check_auth + +# ───────────────────────────────────────────────────────────────────── +log_title "Project Initialization: $OWNER/$REPO_NAME" +# ───────────────────────────────────────────────────────────────────── +echo " Owner: $OWNER" +echo " Name: $REPO_NAME" +echo " Description: $DESCRIPTION" +echo " Language: $LANG" +echo " Private: $PRIVATE" +divider + +# ── Step 1: Create Repository ──────────────────────────────────────── +log_step "Creating repository..." +REPO_RESULT=$(gl_check repo +create --owner "$OWNER" --name "$REPO_NAME" --description "$DESCRIPTION" --private "$PRIVATE") +REPO_ID=$(echo "$REPO_RESULT" | jq -r '.data.id // .data.project_id // empty') +log_ok "Repository created: $OWNER/$REPO_NAME (id: $REPO_ID)" + +# ── Step 2: Create README ──────────────────────────────────────────── +log_step "Creating README wiki page..." + +# Wait for repo to be fully initialized +sleep 2 + +README_CONTENT="# $REPO_NAME + +$DESCRIPTION + +## Getting Started + +### Prerequisites" + +case "$LANG" in + go) + README_CONTENT+=" + +- Go 1.21+ +- Git + +### Installation + +\`\`\`bash +git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git +cd $REPO_NAME +go mod download +go build ./... +\`\`\` + +### Usage + +\`\`\`bash +go run main.go +\`\`\` + +### Testing + +\`\`\`bash +go test ./... +\`\`\`" + ;; + python) + README_CONTENT+=" + +- Python 3.9+ +- pip + +### Installation + +\`\`\`bash +git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git +cd $REPO_NAME +pip install -r requirements.txt +\`\`\` + +### Usage + +\`\`\`bash +python main.py +\`\`\` + +### Testing + +\`\`\`bash +pytest +\`\`\`" + ;; + node) + README_CONTENT+=" + +- Node.js 18+ +- npm or yarn + +### Installation + +\`\`\`bash +git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git +cd $REPO_NAME +npm install +\`\`\` + +### Usage + +\`\`\`bash +npm start +\`\`\` + +### Testing + +\`\`\`bash +npm test +\`\`\`" + ;; + java) + README_CONTENT+=" + +- JDK 17+ +- Maven 3.8+ + +### Installation + +\`\`\`bash +git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git +cd $REPO_NAME +mvn clean install +\`\`\` + +### Usage + +\`\`\`bash +mvn exec:java +\`\`\` + +### Testing + +\`\`\`bash +mvn test +\`\`\`" + ;; +esac + +README_CONTENT+=" + +## Contributing + +See [CONTRIBUTING](./CONTRIBUTING) for guidelines. + +## License + +This project is licensed under the MIT License." + +# Retry wiki creation up to 3 times +WIKI_OK=false +for attempt in 1 2 3; do + WIKI_RESULT=$(gl_run wiki +create --owner "$OWNER" --repo "$REPO_NAME" \ + --title "README" --content "$README_CONTENT" 2>&1) || true + if [[ "$(json_ok "$WIKI_RESULT")" == "true" ]]; then + log_ok "README created" + WIKI_OK=true + break + fi + [[ $attempt -lt 3 ]] && sleep 2 +done +[[ "$WIKI_OK" == "false" ]] && log_warn "README wiki creation may have failed" + +# ── Step 3: Create CONTRIBUTING Guide ──────────────────────────────── +log_step "Creating CONTRIBUTING guide..." + +CONTRIB_CONTENT="# Contributing to $REPO_NAME + +Thank you for your interest in contributing! + +## How to Contribute + +1. Fork the repository +2. Create a feature branch: \`git checkout -b feature/my-feature\` +3. Make your changes +4. Run tests to ensure everything passes +5. Commit your changes: \`git commit -m 'feat: add my feature'\` +6. Push to your fork: \`git push origin feature/my-feature\` +7. Create a Pull Request + +## Code Style + +- Follow the existing code style +- Write meaningful commit messages +- Add tests for new features +- Update documentation as needed + +## Reporting Issues + +- Use the issue tracker +- Include reproduction steps +- Include environment details + +## Code of Conduct + +Please be respectful and constructive in all interactions." + +# Retry wiki creation up to 3 times +WIKI_OK=false +for attempt in 1 2 3; do + WIKI_CONTRIB=$(gl_run wiki +create --owner "$OWNER" --repo "$REPO_NAME" \ + --title "CONTRIBUTING" --content "$CONTRIB_CONTENT" 2>&1) || true + if [[ "$(json_ok "$WIKI_CONTRIB")" == "true" ]]; then + log_ok "CONTRIBUTING guide created" + WIKI_OK=true + break + fi + [[ $attempt -lt 3 ]] && sleep 2 +done +[[ "$WIKI_OK" == "false" ]] && log_warn "CONTRIBUTING wiki creation may have failed" + +# ── Step 4: Create Initial Issues ──────────────────────────────────── +log_step "Creating initial issues..." + +ISSUES_TO_CREATE=( + "Setup CI/CD Pipeline|Configure continuous integration and deployment for the project.|feature" + "Write Project Documentation|Complete project documentation including API docs and architecture guide.|documentation" + "Setup Code Review Process|Establish code review guidelines and automation.|enhancement" + "Add Unit Tests|Add comprehensive unit test coverage for core modules.|enhancement" + "Setup Dependency Management|Configure dependency scanning and updates.|security" +) + +for entry in "${ISSUES_TO_CREATE[@]}"; do + IFS='|' read -r title body label <<< "$entry" + ISSUE_RESULT=$(gl_run issue +create --owner "$OWNER" --repo "$REPO_NAME" \ + --title "$title" --body "$body" 2>&1) || true + ISSUE_NUM=$(echo "$ISSUE_RESULT" | jq -r '.data.id // .data.number // empty') + if [[ -n "$ISSUE_NUM" ]]; then + # Add label + gl_run issue +label-add --owner "$OWNER" --repo "$REPO_NAME" --number "$ISSUE_NUM" --labels "$label" > /dev/null 2>&1 || true + log_ok "Issue created: #$ISSUE_NUM - $title" + else + log_warn "Issue creation may have failed: $title" + fi +done + +# ── Step 5: Protect Default Branch ─────────────────────────────────── +log_step "Protecting master branch..." +PROTECT_RESULT=$(gl_run branch +protect --owner "$OWNER" --repo "$REPO_NAME" --branch master 2>&1) || true + +if [[ "$(json_ok "$PROTECT_RESULT")" == "true" ]]; then + log_ok "Branch 'master' protected" +else + log_warn "Branch protection may have failed (may require admin permissions)" +fi + +# ── Step 6: Create Initial Release ─────────────────────────────────── +log_step "Creating initial release v0.1.0..." + +RELEASE_BODY="# v0.1.0 - Initial Release + +## What's New +- Project initialized with $LANG template +- README and CONTRIBUTING guides created +- CI/CD pipeline issues filed +- Branch protection enabled + +## Next Steps +- [ ] Setup CI/CD pipeline +- [ ] Write comprehensive tests +- [ ] Complete documentation +- [ ] First feature implementation + +--- +*Auto-initialized by gitlink-cli project-init workflow*" + +RELEASE_RESULT=$(gl_run release +create --owner "$OWNER" --repo "$REPO_NAME" \ + --tag "v0.1.0" --name "Initial Release" --body "$RELEASE_BODY" 2>&1) || true + +if [[ "$(json_ok "$RELEASE_RESULT")" == "true" ]]; then + log_ok "Release v0.1.0 created" +else + log_warn "Release creation may have failed" +fi + +# ───────────────────────────────────────────────────────────────────── +log_title "Project Initialization Complete" +# ───────────────────────────────────────────────────────────────────── + +echo -e "${GREEN}Created:${NC}" +echo " Repository: $OWNER/$REPO_NAME" +echo " README: Wiki page" +echo " CONTRIBUTING: Wiki page" +echo " Issues: ${#ISSUES_TO_CREATE[@]} initial issues" +echo " Branch: master (protected)" +echo " Release: v0.1.0" +echo "" +echo -e "${CYAN}Next steps:${NC}" +echo " 1. Clone: git clone https://gitlink.org.cn/$OWNER/$REPO_NAME.git" +echo " 2. Add your code and push" +echo " 3. Setup CI/CD by closing the first issue" +echo "" diff --git a/workflows/04-multi-repo-collab.sh b/workflows/04-multi-repo-collab.sh new file mode 100644 index 00000000..5da9add9 --- /dev/null +++ b/workflows/04-multi-repo-collab.sh @@ -0,0 +1,262 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────── +# Scenario 4: Multi-Repo Collaboration +# Flow: Cross-repo issue tracking → PR status dashboard → Coordinated release +# +# Commands/Skills chained: +# 1. repo +list -- list all repos in org +# 2. issue +list -- fetch issues from each repo +# 3. pr +list -- fetch PRs from each repo +# 4. pr +view -- get PR details for dashboard +# 5. release +list -- check release status across repos +# 6. release +create -- coordinated release +# 7. Generate HTML dashboard +# ───────────────────────────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +usage() { + echo "Usage: $0 --org ORG [--repos REPO1,REPO2,...] [--release TAG] [--dry-run]" + echo "" + echo " --org ORG Organization name" + echo " --repos REPO1,REPO2 Comma-separated repo list (default: all repos in org)" + echo " --release TAG Coordinated release tag to create" + echo " --output FILE Output HTML dashboard file (default: dashboard.html)" + echo " --dry-run Preview actions without executing" + exit 1 +} + +DRY_RUN=false +ORG="" +REPOS="" +RELEASE_TAG="" +OUTPUT_FILE="dashboard.html" + +while [[ $# -gt 0 ]]; do + case "$1" in + --org) ORG="$2"; shift 2 ;; + --repos) REPOS="$2"; shift 2 ;; + --release) RELEASE_TAG="$2"; shift 2 ;; + --output) OUTPUT_FILE="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + --help|-h) usage ;; + *) log_err "Unknown arg: $1"; usage ;; + esac +done + +if [[ -z "$ORG" ]]; then + log_err "Missing required parameter: --org" + usage +fi + +check_auth + +# ── Step 1: List Repositories ──────────────────────────────────────── +log_title "Multi-Repo Collaboration Dashboard" + +log_step "Fetching repositories for org: $ORG..." +REPOS_JSON=$(gl_check repo +list --owner "$ORG" --limit 100) +ALL_REPO_COUNT=$(echo "$REPOS_JSON" | jq '.data.projects | length') +log_ok "Found $ALL_REPO_COUNT repositories" + +# Filter repos if --repos specified +REPO_LIST=() +if [[ -n "$REPOS" ]]; then + IFS=',' read -ra REPO_LIST <<< "$REPOS" + log_info "Filtering to specified repos: ${REPO_LIST[*]}" +else + for i in $(seq 0 $((ALL_REPO_COUNT - 1))); do + RNAME=$(echo "$REPOS_JSON" | jq -r ".data.projects[$i].name // .data.projects[$i].identifier // empty") + [[ -n "$RNAME" ]] && REPO_LIST+=("$RNAME") + done +fi + +log_ok "Will process ${#REPO_LIST[@]} repositories" + +# ── Step 2-3: Collect Issues and PRs from each repo ────────────────── +log_title "Collecting Data Across Repos" + +# Data arrays for dashboard +DASHBOARD_ROWS="" +TOTAL_ISSUES=0 +TOTAL_PRS=0 +TOTAL_OPEN_ISSUES=0 +TOTAL_OPEN_PRS=0 + +for repo in "${REPO_LIST[@]}"; do + divider + log_step "Processing $ORG/$repo..." + + # Fetch open issues + ISSUES_JSON=$(gl_run issue +list --owner "$ORG" --repo "$repo" --state open --limit 50) + OPEN_ISSUES=$(echo "$ISSUES_JSON" | jq '.data.issues | length' 2>/dev/null || echo "0") + + # Fetch closed issues (recent) + CLOSED_JSON=$(gl_run issue +list --owner "$ORG" --repo "$repo" --state closed --limit 50) + CLOSED_ISSUES=$(echo "$CLOSED_JSON" | jq '.data.issues | length' 2>/dev/null || echo "0") + + # Fetch open PRs + PRS_JSON=$(gl_run pr +list --owner "$ORG" --repo "$repo" --state open --limit 50) + OPEN_PRS=$(echo "$PRS_JSON" | jq '.data.issues | length' 2>/dev/null || echo "0") + + # Fetch merged PRs (recent) + MERGED_JSON=$(gl_run pr +list --owner "$ORG" --repo "$repo" --state merged --limit 50) + MERGED_PRS=$(echo "$MERGED_JSON" | jq '.data.issues | length' 2>/dev/null || echo "0") + + # Fetch latest release + RELEASES_JSON=$(gl_run release +list --owner "$ORG" --repo "$repo" --limit 1) + LATEST_RELEASE=$(echo "$RELEASES_JSON" | jq -r '.data.releases[0].tag_name // .data.releases[0].name // "none"' 2>/dev/null) + + log_ok "$repo: Issues(open:$OPEN_ISSUES closed:$CLOSED_ISSUES) PRs(open:$OPEN_PRS merged:$MERGED_PRS) Release:$LATEST_RELEASE" + + # Get PR details for open PRs + PR_DETAILS="" + if [[ "$OPEN_PRS" -gt 0 ]] && [[ "$OPEN_PRS" != "null" ]]; then + for pi in $(seq 0 $((OPEN_PRS > 5 ? 4 : OPEN_PRS - 1))); do + PR_ID=$(echo "$PRS_JSON" | jq -r ".data.issues[$pi].pull_request_number // .data.issues[$pi].id // empty") + PR_TITLE=$(echo "$PRS_JSON" | jq -r ".data.issues[$pi].subject // .data.issues[$pi].name // \"\"") + PR_AUTHOR=$(echo "$PRS_JSON" | jq -r ".data.issues[$pi].author_login // .data.issues[$pi].author.login // \"unknown\"") + PR_DETAILS+="
| Repository | Open Issues | Closed Issues | Open PRs | Merged PRs | Latest Release | Health |
|---|
$OWNER/$REPO - Team Contribution Analysis
" >> "$REPORT_FILE" +echo "| Rank | Contributor | Issues | Merged PRs | Code Lines | Comments | Score | Badge |
|---|---|---|---|---|---|---|---|
| $rank | @$user | $iss | $mer | $lin | $com | $score | $badge |