diff --git a/shortcuts/milestone/milestone.go b/shortcuts/milestone/milestone.go index 23d55da..f0de1ad 100644 --- a/shortcuts/milestone/milestone.go +++ b/shortcuts/milestone/milestone.go @@ -36,14 +36,14 @@ func Shortcuts() []*common.Shortcut { Flags: []common.Flag{ {Name: "name", Short: "n", Usage: "Milestone name", Required: true}, {Name: "description", Short: "d", Usage: "Description"}, - {Name: "due", Usage: "Due date (YYYY-MM-DD)"}, + {Name: "due", Usage: "Due date (YYYY-MM-DD)"},0. }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } name, _ := ctx.RequireArg("name", `--name "My Name"`) - body := map[string]interface{}{ + body := 】[string]interface{}{ "title": name, } if d := ctx.Arg("description"); d != "" { diff --git a/skills/gitlink-onboard/SKILL.md b/skills/gitlink-onboard/SKILL.md index f7838c8..97be85f 100644 --- a/skills/gitlink-onboard/SKILL.md +++ b/skills/gitlink-onboard/SKILL.md @@ -1,6 +1,5 @@ --- name: gitlink-onboard -version: 2.0.0 description: "新人引导:通过 AI 语义分析识别适合新手的 Good First Issue,调用 CLI 添加引导评论。当用户需要识别新手友好 issue 并欢迎新贡献者时触发。" metadata: requires: diff --git a/skills/gitlink-workflow/SKILL.md b/skills/gitlink-workflow/SKILL.md index 8faeb7a..ab34f7d 100644 --- a/skills/gitlink-workflow/SKILL.md +++ b/skills/gitlink-workflow/SKILL.md @@ -1,100 +1,177 @@ --- name: gitlink-workflow -version: 1.0.0 -description: "AI 自动化工作流:Issue 分类、PR Review、Release Notes 生成、仓库初始化、Sprint 报告等。当用户需要 AI 自动化 GitLink 操作时触发。" +version: 2.0.0 +description: "AI 自动化工作流:代码质量审查(PR Review)。当用户需要 AI 审查 PR 代码质量时触发。" metadata: requires: bins: ["gitlink-cli"] - cliHelp: "gitlink-cli workflow --help" --- -# gitlink-workflow(AI 自动化工作流) +# gitlink-workflow(AI 代码质量看门人) > **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) -**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。`gh` 仅适用于 GitHub 平台。** +**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。** -本技能提供 Claude Code 可直接执行的高级工作流模板。 +## 工作流:代码质量看门人(Code Quality Gatekeeper) -## 工作流 1:Issue Triage(Issue 自动分类) +**触发词**:代码审查、PR review、质量检查、gatekeeper、审查 PR -**场景**:自动为新 Issue 添加标签分类。 +**参数**: +- `--owner`:仓库所属组织或用户 +- `--repo`:仓库名称 +- `--pr-id`:指定 PR ID(可选,默认审查所有 open PR) +- `--threshold`:质量阈值(默认 80) -```bash -# 1. 获取未标记的 Issue 列表 -gitlink-cli issue +list --state open --format json +### 流程总览 -# 2. 逐个查看 Issue 详情 -gitlink-cli issue +view --id --format json - -# 3. 根据内容分析,通过 Raw API 添加标签 -gitlink-cli api POST /:owner/:repo/issues/:id --body '{"issue_tag_ids":[]}' +``` +PR 提交 → 获取详情 → 获取 Diff → AI 四维度评分 → 发布审查评论 → 检查 CI → 自动合并 ``` -**分类规则建议**: -- 标题/描述包含 "bug"、"错误"、"失败" → bug 标签 -- 标题/描述包含 "feature"、"新增"、"建议" → enhancement 标签 -- 标题/描述包含 "question"、"如何"、"怎么" → question 标签 - -## 工作流 2:PR Review(代码审查辅助) - -**场景**:获取 PR 变更,分析代码质量,添加 Review 评论。 +### 审查单个 PR 的步骤 +**步骤 1** — 获取 PR 详情: ```bash -# 1. 获取 PR 详情 -gitlink-cli pr +view --id --format json +gitlink-cli pr +view --owner {OWNER} --repo {REPO} --id {PR_ID} --format json +``` +提取字段: +- title: `.data.title // .data.subject // .data.issue.subject` +- author: `.data.author.login // .data.author.username` +- state: `.data.state // .data.status` -# 2. 获取变更文件列表 -gitlink-cli pr +files --id --format json +**步骤 2** — 获取变更文件列表: +```bash +gitlink-cli pr +files --owner {OWNER} --repo {REPO} --id {PR_ID} --format json +``` +文件列表路径:`.data.files[]`,字段:`.name`(或 `.filename`)、`.additions`、`.deletions` -# 3. 获取 PR 提交列表 -gitlink-cli pr +diff --id --format json +**步骤 3** — 获取代码差异: +```bash +gitlink-cli pr +diff --owner {OWNER} --repo {REPO} --id {PR_ID} --format json +``` +提取 diff:`.data.files[].sections[].lines[].content`,截取前 5000 字符 -# 4. 添加 Review 评论 -gitlink-cli api POST /:owner/:repo/pulls/:id/reviews --body '{"body":"代码审查意见...","event":"COMMENT"}' +**步骤 4** — AI 四维度代码审查(总分 100): + +| 维度 | 满分 | 检查项 | +|------|------|--------| +| 代码质量 | 25 | 复杂度、命名规范、注释、格式一致性 | +| 安全性 | 25 | SQL注入、XSS、硬编码凭证、认证绕过、输入验证 | +| 性能 | 25 | 循环效率、资源泄漏、N+1查询、内存占用、阻塞调用 | +| 可维护性 | 25 | 代码重复、职责单一、依赖耦合、测试覆盖 | + +评分标准: +- 90-100:优秀,可直接合并 +- 75-89:良好,建议合并 +- 60-74:一般,需要改进 +- <60:较差,不建议合并 + +问题严重级别: +- CRITICAL:阻止合并(安全漏洞、数据丢失风险) +- HIGH:强烈建议修复(性能问题、逻辑错误) +- MEDIUM:建议修复(代码质量、可维护性) +- LOW:可选修复(风格、命名) + +**输出要求**:AI 必须输出以下 JSON 结构: +```json +{ + "total": 85, + "quality": 22, + "security": 25, + "performance": 20, + "maintainability": 18, + "issues": [ + { + "severity": "MEDIUM", + "category": "quality", + "file": "src/main.go", + "rule": "naming", + "description": "变量名过于简短", + "suggestion": "使用更具描述性的变量名" + } + ], + "positive_notes": [ + {"description": "错误处理完善"} + ], + "recommendations": [ + "建议添加单元测试" + ], + "verdict": "PASS" +} ``` -## 工作流 3:Release Notes 生成 +**AI 不可用时的降级方案**(关键词检测): +- 检测到 password/secret/token/api_key/private_key → security -15 +- 检测到 eval()/exec()/system()/os.system → security -10 +- 检测到 TODO/FIXME/HACK/XXX → quality -5 +- 检测到 SELECT */findAll()/.all() → performance -10 +- 检测到 sleep()/Thread.sleep → performance -5 +- 变更文件 > 20 个 → maintainability -10 -**场景**:从提交历史自动生成版本发布说明。 +**步骤 5** — 组装审查评论 Markdown: -> 此工作流已独立为 [`gitlink-changelog`](../gitlink-changelog/SKILL.md) skill,包含完整的数据收集、分类规则、模板和发布流程。详见该 skill 的 [references/](../gitlink-changelog/references/) 和 [examples/](../gitlink-changelog/examples/)。 +```markdown +## AI Code Quality Review - PR #{PR_ID} -## 工作流 4:Repo Setup(仓库初始化) +### Scores +| Dimension | Score | Max | +|-----------|-------|-----| +| Code Quality | {QUALITY} | 25 | +| Security | {SECURITY} | 25 | +| Performance | {PERFORMANCE} | 25 | +| Maintainability | {MAINTAINABILITY} | 25 | +| **Total** | **{TOTAL}** | **100** | -**场景**:创建仓库并完成基础配置。 +### Issues Found +- [{SEVERITY}] {CATEGORY}: {DESCRIPTION} ({FILE}) → {SUGGESTION} -```bash -# 1. 创建仓库 -gitlink-cli repo +create --name my-project --description "项目描述" +### Positive Notes +- {NOTE} -# 2. 设置分支保护 -gitlink-cli branch +protect --name main --owner myuser --repo my-project +### Recommendations +- {REC} -# 3. 创建初始 Issue -gitlink-cli issue +create --title "项目初始化" --body "- [ ] 完善 README\n- [ ] 配置 CI\n- [ ] 添加 License" --owner myuser --repo my-project +### Verdict +{PASS/FAIL} - Score {TOTAL} {>=/<} threshold {THRESHOLD} + +--- +*Auto-reviewed by gitlink-cli code-quality-gatekeeper workflow* ``` -## 工作流 5:Sprint Report(Sprint 报告) - -**场景**:汇总 Issue/PR 统计,生成周报。 - +**步骤 6** — 发布审查评论: ```bash -# 1. 获取 Issue 统计 -gitlink-cli issue +list --state open --format json -gitlink-cli issue +list --state closed --format json +gitlink-cli api POST /{OWNER}/{REPO}/pulls/{PR_ID}/reviews --body '{"body":"{REVIEW_MD}","event":"{EVENT}"}' +``` +- 分数 >= 阈值 → event = "APPROVE" +- 分数 < 阈值 → event = "COMMENT" -# 2. 获取 PR 统计 -gitlink-cli pr +list --state open --format json -gitlink-cli pr +list --state merged --format json +**步骤 7** — 检查 CI 构建状态: +```bash +gitlink-cli ci +builds --owner {OWNER} --repo {REPO} --format json +``` +遍历 `.data.builds[]` 或 `.data[]`,检查 status 是否为 success/passed/completed -# 3. 获取项目动态 -gitlink-cli api GET /:owner/:repo/activity --format json +**步骤 8** — 自动合并(条件:分数 >= 阈值 且 CI 全部通过): +```bash +gitlink-cli pr +merge --owner {OWNER} --repo {REPO} --id {PR_ID} --method merge ``` -## 最佳实践 +### 审查所有 open PR -- 所有工作流命令使用 `--format json` 以便解析输出 -- 写入操作前确认用户意图 -- 批量操作建议先用小范围测试 -- 保存工作流执行结果以便回溯 +如果用户没指定 `--pr-id`,先获取列表再逐个审查: +```bash +gitlink-cli pr +list --owner {OWNER} --repo {REPO} --state open --limit 50 --format json +``` +PR ID 提取:`.data.issues[]` 或 `.data.pulls[]` 或 `.data[]` 中的 `.pull_request_number // .number // .id` + +## 其他自动化工作流 + +以下工作流不需要 AI,已实现为 PowerShell 脚本(`workflows/` 目录): + +| 工作流 | 脚本 | 用法 | +|--------|------|------| +| 社区运营自动化 | `01-community-ops.ps1` | `pwsh workflows/01-community-ops.ps1 -Owner zzx-coder -Repo gitlink-cli` | +| 项目一键初始化 | `03-project-init.ps1` | `pwsh workflows/03-project-init.ps1 -Owner org -Name my-app -Desc "描述" -Lang go` | +| 多仓库协同 | `04-multi-repo-collab.ps1` | `pwsh workflows/04-multi-repo-collab.ps1 -Org myorg` | +| 贡献者成长体系 | `05-contributor-growth.ps1` | `pwsh workflows/05-contributor-growth.ps1 -Owner zzx-coder -Repo gitlink-cli` | diff --git a/workflows/01-community-ops.ps1 b/workflows/01-community-ops.ps1 new file mode 100644 index 0000000..a7ec5f2 --- /dev/null +++ b/workflows/01-community-ops.ps1 @@ -0,0 +1,260 @@ +# ---------------------------------------------------------------- +# Scenario 1: Community Operations Automation +# Flow: Issue auto-classify -> Assign responsible -> Weekly report -> Release notes +# +# Commands chained: +# 1. issue +list -- fetch open issues +# 2. issue +label-add -- add classification labels +# 3. repo +members -- get repo members +# 4. api PATCH -- 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 +# ---------------------------------------------------------------- +#Requires -Version 5.1 + +param( + [string]$Owner = "", + [string]$Repo = "", + [int]$WeeksAgo = 0, + [switch]$DryRun, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +Import-Module "$PSScriptRoot/lib/common.psm1" -Force + +if ($Help) { + Write-Host "Usage: powershell 01-community-ops.ps1 -Owner OWNER -Repo REPO [-WeeksAgo N] [-DryRun]" + Write-Host "" + Write-Host " -Owner OWNER Repository owner (org or user)" + Write-Host " -Repo REPO Repository name" + Write-Host " -WeeksAgo N Generate report for N weeks ago (default: 0 = this week)" + Write-Host " -DryRun Preview actions without executing" + exit 0 +} + +Check-Auth +$r = Resolve-OwnerRepo $Owner $Repo +$Owner = $r.Owner; $Repo = $r.Repo + +# Classification keywords +$BugKw = @('bug','error','crash','fault','fix') +$FeatureKw = @('feature','enhancement','add','support','request') +$QuestionKw = @('how','question','help') +$DocsKw = @('doc','readme','guide','tutorial','example') + +# ---------------------------------------------------------------- +Log-Title "Phase 1: Issue Auto-Classification" +# ---------------------------------------------------------------- + +Log-Step "Fetching open issues..." +$issuesJson = Invoke-GLCheck issue,+list,--owner,$Owner,--repo,$Repo,--state,open,--limit,100 +if (-not $issuesJson) { Log-Err "Failed to fetch issues"; exit 1 } + +$issues = @($issuesJson.data.issues) +$issueCount = $issues.Count +Log-Ok "Found $issueCount open issues" + +$BugIds = @(); $FeatureIds = @(); $QuestionIds = @(); $DocsIds = @() + +if ($issueCount -gt 0) { + Log-Step "Classifying issues by content..." + + foreach ($issue in $issues) { + $id = $issue.id + $title = if ($issue.subject) { $issue.subject } elseif ($issue.title) { $issue.title } else { "" } + $desc = if ($issue.description) { $issue.description } else { "" } + $combined = "$title $desc".ToLower() + + $classified = $false + foreach ($kw in $BugKw) { + if ($combined -match [regex]::Escape($kw)) { $BugIds += $id; Log-Info " #$id -> BUG: $title"; $classified = $true; break } + } + if (-not $classified) { + foreach ($kw in $FeatureKw) { + if ($combined -match [regex]::Escape($kw)) { $FeatureIds += $id; Log-Info " #$id -> FEATURE: $title"; $classified = $true; break } + } + } + if (-not $classified) { + foreach ($kw in $QuestionKw) { + if ($combined -match [regex]::Escape($kw)) { $QuestionIds += $id; Log-Info " #$id -> QUESTION: $title"; $classified = $true; break } + } + } + if (-not $classified) { + foreach ($kw in $DocsKw) { + if ($combined -match [regex]::Escape($kw)) { $DocsIds += $id; Log-Info " #$id -> DOCS: $title"; $classified = $true; break } + } + } + if (-not $classified) { Log-Info " #$id -> UNCATEGORIZED: $title" } + } + + Divider + Log-Info "Classification summary:" + Log-Info " Bugs: $($BugIds.Count)" + Log-Info " Features: $($FeatureIds.Count)" + Log-Info " Questions: $($QuestionIds.Count)" + Log-Info " Docs: $($DocsIds.Count)" + + # Apply labels + $labelGroups = @( + @{ Ids = $BugIds; Label = "bug" }, + @{ Ids = $FeatureIds; Label = "feature" }, + @{ Ids = $QuestionIds; Label = "question" }, + @{ Ids = $DocsIds; Label = "documentation" } + ) + foreach ($g in $labelGroups) { + if ($g.Ids.Count -gt 0) { + Log-Step "Labeling $($g.Label) issues..." + foreach ($id in $g.Ids) { + Invoke-GL issue,+label-add,--owner,$Owner,--repo,$Repo,--number,$id,--labels,$g.Label | Out-Null + } + Log-Ok "Labeled $($g.Ids.Count) $($g.Label) issues" + } + } +} + +# ---------------------------------------------------------------- +Log-Title "Phase 2: Assign Responsible Persons" +# ---------------------------------------------------------------- + +Log-Step "Fetching repo members..." +$membersJson = Invoke-GL repo,+members,--owner,$Owner,--repo,$Repo,--limit,50 +$members = @() +if ($membersJson) { + $md = $membersJson.data + if ($md.members) { $members = @($md.members) } + elseif ($md -is [array]) { $members = $md } +} + +$memberLogins = @() +foreach ($m in $members) { + $login = if ($m.login) { $m.login } elseif ($m.username) { $m.username } else { $null } + if ($login) { $memberLogins += $login } +} + +if ($memberLogins.Count -gt 0) { + $assignIds = @($BugIds + $FeatureIds) + if ($assignIds.Count -gt 0) { + Log-Step "Assigning issues to members (round-robin)..." + $idx = 0 + foreach ($id in $assignIds) { + $assignee = $memberLogins[$idx % $memberLogins.Count] + $bodyJson = "{`"assigned_to_id`": `"$assignee`"}" + Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$id",--body,$bodyJson | Out-Null + Log-Info " Assigned #$id -> @$assignee" + $idx++ + } + Log-Ok "Assignment complete" + } +} else { + Log-Warn "No repo members found, skipping assignment" +} + +# ---------------------------------------------------------------- +Log-Title "Phase 3: Generate Community Weekly Report" +# ---------------------------------------------------------------- + +$weekStart = (Get-Date).AddDays(-$WeeksAgo * 7).ToString("yyyy-MM-dd") +$weekEnd = Get-DateToday + +Log-Step "Collecting weekly data (week of $weekStart)..." + +$closedJson = Invoke-GL issue,+list,--owner,$Owner,--repo,$Repo,--state,closed,--limit,100 +$closedCount = if ($closedJson) { @($closedJson.data.issues).Count } else { 0 } + +$mergedJson = Invoke-GL pr,+list,--owner,$Owner,--repo,$Repo,--state,merged,--limit,100 +$mergedData = @() +if ($mergedJson) { + $d = $mergedJson.data + if ($d.issues) { $mergedData = @($d.issues) } + elseif ($d.pulls) { $mergedData = @($d.pulls) } + elseif ($d -is [array]) { $mergedData = $d } +} +$mergedCount = $mergedData.Count + +$newIssuesCount = $issueCount +$totalClassified = $BugIds.Count + $FeatureIds.Count + $QuestionIds.Count + $DocsIds.Count + +$reportTitle = "Community Weekly Report: $weekStart ~ $weekEnd" +$reportBody = "# $reportTitle" + "`n`n" +$reportBody += "## Summary" + "`n" +$reportBody += "- New Issues: **$newIssuesCount**" + "`n" +$reportBody += "- Closed Issues: **$closedCount**" + "`n" +$reportBody += "- Merged PRs: **$mergedCount**" + "`n`n" +$reportBody += "## Issue Classification" + "`n" +$reportBody += "| Type | Count |" + "`n" +$reportBody += "|------|-------|" + "`n" +$reportBody += "| Bug | $($BugIds.Count) |" + "`n" +$reportBody += "| Feature | $($FeatureIds.Count) |" + "`n" +$reportBody += "| Question | $($QuestionIds.Count) |" + "`n" +$reportBody += "| Docs | $($DocsIds.Count) |" + "`n`n" +$reportBody += "## Highlights" + "`n" +$reportBody += "- Auto-classified and labeled $totalClassified issues" + "`n" +$reportBody += "- Assigned responsible persons for bug and feature issues" + "`n`n" +$reportBody += "---" + "`n" +$reportBody += "*Auto-generated by gitlink-cli community-ops workflow*" + +Log-Ok "Weekly report generated" +Write-Host "" +Write-Host $reportBody + +Log-Step "Publishing weekly report to Wiki..." +$wikiResult = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Repo,--title,$reportTitle,--body,$reportBody +if ($wikiResult -and (Get-JsonOk ($wikiResult | ConvertFrom-Json))) { + Log-Ok "Weekly report published to Wiki" +} else { + Log-Warn "Wiki publish may have failed (wiki module might not be enabled)" +} + +# ---------------------------------------------------------------- +Log-Title "Phase 4: Auto-Publish Release Notes" +# ---------------------------------------------------------------- + +Log-Step "Collecting recent changes for release notes..." + +$tagName = "weekly-$(Get-Date -Format 'yyyyMMdd')" +$releaseName = "Weekly Release $(Get-Date -Format 'yyyy-MM-dd')" + +$releaseBody = "# Release Notes - $(Get-Date -Format 'yyyy-MM-dd')" + "`n`n" +$releaseBody += "## Merged PRs ($mergedCount)" + +if ($mergedCount -gt 0) { + $limit = [Math]::Min($mergedCount, 10) + for ($i = 0; $i -lt $limit; $i++) { + $prTitle = if ($mergedData[$i].subject) { $mergedData[$i].subject } elseif ($mergedData[$i].title) { $mergedData[$i].title } else { "" } + $prNum = if ($mergedData[$i].id) { $mergedData[$i].id } elseif ($mergedData[$i].number) { $mergedData[$i].number } else { "" } + $releaseBody += "`n- #$prNum $prTitle" + } +} + +$releaseBody += "`n`n## Closed Issues ($closedCount)" +if ($closedCount -gt 0) { + $closedIssues = @($closedJson.data.issues) + $limit = [Math]::Min($closedCount, 10) + for ($i = 0; $i -lt $limit; $i++) { + $issueTitle = if ($closedIssues[$i].subject) { $closedIssues[$i].subject } elseif ($closedIssues[$i].title) { $closedIssues[$i].title } else { "" } + $issueNum = if ($closedIssues[$i].number) { $closedIssues[$i].number } elseif ($closedIssues[$i].id) { $closedIssues[$i].id } else { "" } + if ($issueTitle) { $releaseBody += "`n- #$issueNum $issueTitle" } + } +} + +$releaseBody += "`n`n---`n*Auto-generated by gitlink-cli community-ops workflow*" + +Log-Step "Creating release: $tagName..." +$releaseResult = Invoke-GL release,+create,--owner,$Owner,--repo,$Repo,--tag,$tagName,--name,$releaseName,--body,$releaseBody +if ($releaseResult -and (Get-JsonOk ($releaseResult | ConvertFrom-Json))) { + Log-Ok "Release $tagName created successfully" +} else { + Log-Warn "Release creation may have failed (tag might already exist)" +} + +# ---------------------------------------------------------------- +Log-Title "Community Operations Complete" +# ---------------------------------------------------------------- + +Write-Host " Issues classified: $totalClassified" -ForegroundColor Green +Write-Host " Closed this week: $closedCount" -ForegroundColor Green +Write-Host " Merged PRs: $mergedCount" -ForegroundColor Green +Write-Host " Weekly report: Published to Wiki" -ForegroundColor Green +Write-Host " Release notes: $tagName" -ForegroundColor Green diff --git a/workflows/01-community-ops.sh b/workflows/01-community-ops.sh new file mode 100644 index 0000000..b3b53bb --- /dev/null +++ b/workflows/01-community-ops.sh @@ -0,0 +1,302 @@ +#!/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 // .data | if type == "array" then . else [] end) | 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=() + MEMBERS_DATA_PATH='(.data.members // .data | if type == "array" then . else [] end)' + for i in $(seq 0 $((MEMBER_COUNT - 1))); do + LOGIN=$(echo "$MEMBERS_JSON" | jq -r "$MEMBERS_DATA_PATH[$i].login // $MEMBERS_DATA_PATH[$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[@]}))]}" + # issue +update doesn't support --assignee, use raw API + gl_run api PATCH "/v1/$OWNER/$REPO/issues/$id" \ + --body "{\"assigned_to_id\": \"$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) +# PR list may return .data[], .data.pulls[], or .data.issues[] +MERGED_COUNT=$(echo "$MERGED_PRS" | jq ' + (.data.issues // .data.pulls // .data | if type == "array" then . else [] end) | 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 + PR_DATA_PATH='(.data.issues // .data.pulls // .data | if type == "array" then . else [] end)' + for i in $(seq 0 $((MERGED_COUNT > 10 ? 9 : MERGED_COUNT - 1))); do + PR_TITLE=$(echo "$MERGED_PRS" | jq -r "$PR_DATA_PATH[$i].subject // $PR_DATA_PATH[$i].title // \"\"") + PR_NUM=$(echo "$MERGED_PRS" | jq -r "$PR_DATA_PATH[$i].id // $PR_DATA_PATH[$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 // empty") + ISSUE_NUM=$(echo "$CLOSED_ISSUES" | jq -r ".data.issues[$i].number // .data.issues[$i].id // empty") + [[ -n "$ISSUE_TITLE" ]] && 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 0000000..3555289 --- /dev/null +++ b/workflows/02-code-quality-gatekeeper.sh @@ -0,0 +1,494 @@ +#!/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 // .data.issue.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.issue.author.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" + + # Ensure CLAUDE_CODE_GIT_BASH_PATH is set for Windows + if [[ -z "${CLAUDE_CODE_GIT_BASH_PATH:-}" ]] && command -v cygpath &>/dev/null; then + export CLAUDE_CODE_GIT_BASH_PATH="$(cygpath -w "$(which bash)")" + fi + + # Run claude in a subshell to isolate from set -euo pipefail + # NOTE: must use pipe (not file redirect) for claude -p on Windows + AI_EXIT=0 + ( + cat "$PROMPT_FILE" | timeout 300 claude -p --output-format json > "$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_EVENT=$(if [[ $TOTAL_SCORE -ge $THRESHOLD ]]; then echo "APPROVE"; else echo "COMMENT"; fi) + REVIEW_JSON=$(jq -n --arg body "$REVIEW_BODY" --arg event "$REVIEW_EVENT" \ + '{body: $body, event: $event}') + REVIEW_RESULT=$(gl_run api POST "/$OWNER/$REPO/pulls/$pr_id/reviews" \ + --body "$REVIEW_JSON" 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 // .data.pulls // .data | if type == "array" then . else [] end) | 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 + + PR_DATA_PATH='(.data.issues // .data.pulls // .data | if type == "array" then . else [] end)' + for i in $(seq 0 $((PR_COUNT - 1))); do + pid=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$i].pull_request_number // $PR_DATA_PATH[$i].number // $PR_DATA_PATH[$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.ps1 b/workflows/03-project-init.ps1 new file mode 100644 index 0000000..327f480 --- /dev/null +++ b/workflows/03-project-init.ps1 @@ -0,0 +1,231 @@ +# ---------------------------------------------------------------- +# Scenario 3: One-Click Project Initialization +# Flow: Input description -> Create repo -> README/CONTRIBUTING/CI config -> +# Initial Issues -> Branch protection -> Initial Release +# +# Commands chained: +# 1. repo +create -- create repository +# 2. wiki +create -- create README wiki page +# 3. wiki +create -- create CONTRIBUTING guide +# 4. wiki +create -- create CI/CD config guide +# 5. issue +create -- create initial issues +# 6. branch +protect -- protect master branch +# 7. release +create -- create initial release +# ---------------------------------------------------------------- +#Requires -Version 5.1 + +param( + [string]$Owner = "", + [Parameter(Mandatory=$true)] + [string]$Name, + [Parameter(Mandatory=$true)] + [string]$Description, + [string]$Lang = "go", + [switch]$Private, + [switch]$DryRun, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +Import-Module "$PSScriptRoot/lib/common.psm1" -Force + +if ($Help) { + Write-Host "Usage: powershell 03-project-init.ps1 -Owner OWNER -Name REPO_NAME -Description DESC [-Lang go|python|node|java] [-Private] [-DryRun]" + exit 0 +} + +Check-Auth +if (-not $Owner) { + $detected = Detect-OwnerRepo + $Owner = $detected.Owner +} + +# ---------------------------------------------------------------- +Log-Title "Project Initialization: $Owner/$Name" +# ---------------------------------------------------------------- +Write-Host " Owner: $Owner" +Write-Host " Name: $Name" +Write-Host " Description: $Description" +Write-Host " Language: $Lang" +Write-Host " Private: $($Private.IsPresent)" +Divider + +# -- Step 1: Create Repository -- +Log-Step "Creating repository..." +$privateStr = if ($Private) { "true" } else { "false" } +$repoResult = Invoke-GLCheck repo,+create,--owner,$Owner,--name,$Name,--description,$Description,--private,$privateStr +if ($repoResult) { + Log-Ok "Repository created: $Owner/$Name" +} else { + Log-Err "Repository creation failed" + exit 1 +} + +# -- Step 2: Create README -- +Log-Step "Creating README wiki page..." +Start-Sleep -Seconds 2 + +$langSection = switch ($Lang) { + "go" { + "### Prerequisites`n- Go 1.21+`n- Git`n`n### Installation`n``````bash`ngit clone https://gitlink.org.cn/$Owner/$Name.git`ncd $Name`ngo mod download`ngo build ./...`n```````n`n### Usage`n``````bash`ngo run main.go`n```````n`n### Testing`n``````bash`ngo test ./...`n``````" + } + "python" { + "### Prerequisites`n- Python 3.9+`n- pip`n`n### Installation`n``````bash`ngit clone https://gitlink.org.cn/$Owner/$Name.git`ncd $Name`npip install -r requirements.txt`n```````n`n### Usage`n``````bash`npython main.py`n```````n`n### Testing`n``````bash`npytest`n``````" + } + "node" { + "### Prerequisites`n- Node.js 18+`n- npm or yarn`n`n### Installation`n``````bash`ngit clone https://gitlink.org.cn/$Owner/$Name.git`ncd $Name`nnpm install`n```````n`n### Usage`n``````bash`nnpm start`n```````n`n### Testing`n``````bash`nnpm test`n``````" + } + "java" { + "### Prerequisites`n- JDK 17+`n- Maven 3.8+`n`n### Installation`n``````bash`ngit clone https://gitlink.org.cn/$Owner/$Name.git`ncd $Name`nmvn clean install`n```````n`n### Usage`n``````bash`nmvn exec:java`n```````n`n### Testing`n``````bash`nmvn test`n``````" + } + default { "" } +} + +$readmeContent = "# $Name" + "`n`n" +$readmeContent += "$Description" + "`n`n" +$readmeContent += "## Getting Started" + "`n`n" +$readmeContent += $langSection + "`n`n" +$readmeContent += "## Contributing" + "`n`n" +$readmeContent += "See [CONTRIBUTING](./CONTRIBUTING) for guidelines." + "`n`n" +$readmeContent += "## License" + "`n`n" +$readmeContent += "This project is licensed under the MIT License." + +$wikiOk = $false +for ($attempt = 1; $attempt -le 3; $attempt++) { + $wikiResult = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"README",--body,$readmeContent + if ($wikiResult -and (Get-JsonOk ($wikiResult | ConvertFrom-Json))) { + Log-Ok "README created" + $wikiOk = $true + break + } + if ($attempt -lt 3) { Start-Sleep -Seconds 2 } +} +if (-not $wikiOk) { Log-Warn "README wiki creation may have failed" } + +# -- Step 3: Create CONTRIBUTING Guide -- +Log-Step "Creating CONTRIBUTING guide..." + +$contribContent = "# Contributing to $Name" + "`n`n" +$contribContent += "Thank you for your interest in contributing!" + "`n`n" +$contribContent += "## How to Contribute" + "`n`n" +$contribContent += "1. Fork the repository" + "`n" +$contribContent += "2. Create a feature branch: ``git checkout -b feature/my-feature```n" +$contribContent += "3. Make your changes" + "`n" +$contribContent += "4. Run tests to ensure everything passes" + "`n" +$contribContent += "5. Commit your changes: ``git commit -m 'feat: add my feature'```n" +$contribContent += "6. Push to your fork: ``git push origin feature/my-feature```n" +$contribContent += "7. Create a Pull Request" + "`n`n" +$contribContent += "## Code Style" + "`n`n" +$contribContent += "- Follow the existing code style" + "`n" +$contribContent += "- Write meaningful commit messages" + "`n" +$contribContent += "- Add tests for new features" + "`n" +$contribContent += "- Update documentation as needed" + "`n`n" +$contribContent += "## Reporting Issues" + "`n`n" +$contribContent += "- Use the issue tracker" + "`n" +$contribContent += "- Include reproduction steps" + "`n" +$contribContent += "- Include environment details" + +$wikiOk = $false +for ($attempt = 1; $attempt -le 3; $attempt++) { + $wikiContrib = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"CONTRIBUTING",--body,$contribContent + if ($wikiContrib -and (Get-JsonOk ($wikiContrib | ConvertFrom-Json))) { + Log-Ok "CONTRIBUTING guide created" + $wikiOk = $true + break + } + if ($attempt -lt 3) { Start-Sleep -Seconds 2 } +} +if (-not $wikiOk) { Log-Warn "CONTRIBUTING wiki creation may have failed" } + +# -- Step 4: Create CI Config Guide -- +Log-Step "Creating CI/CD configuration guide..." + +$ciContent = "# CI/CD Configuration" + "`n`n" +$ciContent += "## GitLink CI Setup" + "`n`n" +$ciContent += "This project uses GitLink CI for continuous integration." + "`n`n" +$ciContent += "### Pipeline Stages" + "`n`n" +$ciContent += "1. **Test**: Run unit tests" + "`n" +$ciContent += "2. **Build**: Build the project" + "`n" +$ciContent += "3. **Deploy**: Deploy to staging (master branch only)" + "`n`n" +$ciContent += "### Configuration" + "`n`n" +$ciContent += "Create a ``.gitlink-ci.yml`` file in the repository root." + +Invoke-GL wiki,+create,--owner,$Owner,--repo,$Name,--title,"CI/CD Configuration",--body,$ciContent | Out-Null +Log-Ok "CI/CD configuration guide created" + +# -- Step 5: Create Initial Issues -- +Log-Step "Creating initial issues..." + +$issuesToCreate = @( + @{ Title = "Setup CI/CD Pipeline"; Body = "Configure continuous integration and deployment for the project.`n`n## Tasks`n- [ ] Create .gitlink-ci.yml configuration`n- [ ] Setup test stage`n- [ ] Setup build stage`n- [ ] Setup deploy stage`n- [ ] Add status badge to README"; Label = "feature" }, + @{ Title = "Write Project Documentation"; Body = "Complete project documentation including API docs and architecture guide.`n`n## Tasks`n- [ ] Write API documentation`n- [ ] Create architecture diagram`n- [ ] Add usage examples`n- [ ] Document configuration options"; Label = "documentation" }, + @{ Title = "Setup Code Review Process"; Body = "Establish code review guidelines and automation.`n`n## Tasks`n- [ ] Define review checklist`n- [ ] Setup branch protection rules`n- [ ] Configure required reviewers`n- [ ] Document review process"; Label = "enhancement" }, + @{ Title = "Add Unit Tests"; Body = "Add comprehensive unit test coverage for core modules.`n`n## Tasks`n- [ ] Setup test framework`n- [ ] Write tests for core modules`n- [ ] Achieve 80 percent code coverage`n- [ ] Add CI test integration"; Label = "enhancement" }, + @{ Title = "Setup Dependency Management"; Body = "Configure dependency scanning and updates.`n`n## Tasks`n- [ ] Setup dependency scanner`n- [ ] Configure automatic updates`n- [ ] Add license compliance check`n- [ ] Document dependency policy"; Label = "security" } +) + +foreach ($entry in $issuesToCreate) { + $issueResult = Invoke-GL issue,+create,--owner,$Owner,--repo,$Name,--title,$entry.Title,--body,$entry.Body + if ($issueResult) { + try { + $issueJson = $issueResult | ConvertFrom-Json + $issueNum = if ($issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.data.number) { $issueJson.data.number } else { $null } + if ($issueNum) { + Invoke-GL issue,+label-add,--owner,$Owner,--repo,$Name,--number,$issueNum,--labels,$entry.Label | Out-Null + Log-Ok "Issue created: #$issueNum - $($entry.Title)" + } + } catch { + Log-Warn "Issue creation may have failed: $($entry.Title)" + } + } +} + +# -- Step 6: Protect Default Branch -- +Log-Step "Protecting master branch..." +$protectResult = Invoke-GL branch,+protect,--owner,$Owner,--repo,$Name,--name,master +if ($protectResult -and (Get-JsonOk ($protectResult | ConvertFrom-Json))) { + Log-Ok "Branch 'master' protected" +} else { + Log-Warn "Branch protection may have failed (may require admin permissions)" +} + +# -- Step 7: Create Initial Release -- +Log-Step "Creating initial release v0.1.0..." + +$releaseBody = "# v0.1.0 - Initial Release" + "`n`n" +$releaseBody += "## What's New" + "`n" +$releaseBody += "- Project initialized with $Lang template" + "`n" +$releaseBody += "- README and CONTRIBUTING guides created" + "`n" +$releaseBody += "- CI/CD configuration guide created" + "`n" +$releaseBody += "- 5 initial issues filed" + "`n" +$releaseBody += "- Branch protection enabled" + "`n`n" +$releaseBody += "## Next Steps" + "`n" +$releaseBody += "- [ ] Setup CI/CD pipeline" + "`n" +$releaseBody += "- [ ] Write comprehensive tests" + "`n" +$releaseBody += "- [ ] Complete documentation" + "`n" +$releaseBody += "- [ ] First feature implementation" + "`n`n" +$releaseBody += "---`n*Auto-initialized by gitlink-cli project-init workflow*" + +$releaseResult = Invoke-GL release,+create,--owner,$Owner,--repo,$Name,--tag,"v0.1.0",--name,"Initial Release",--body,$releaseBody +if ($releaseResult -and (Get-JsonOk ($releaseResult | ConvertFrom-Json))) { + Log-Ok "Release v0.1.0 created" +} else { + Log-Warn "Release creation may have failed" +} + +# ---------------------------------------------------------------- +Log-Title "Project Initialization Complete" +# ---------------------------------------------------------------- + +Write-Host " Repository: $Owner/$Name" -ForegroundColor Green +Write-Host " README: Wiki page" -ForegroundColor Green +Write-Host " CONTRIBUTING: Wiki page" -ForegroundColor Green +Write-Host " CI/CD Guide: Wiki page" -ForegroundColor Green +Write-Host " Issues: 5 initial issues" -ForegroundColor Green +Write-Host " Branch: master (protected)" -ForegroundColor Green +Write-Host " Release: v0.1.0" -ForegroundColor Green +Write-Host "" +Write-Host "Next steps:" -ForegroundColor Cyan +Write-Host " 1. Clone: git clone https://gitlink.org.cn/$Owner/$Name.git" +Write-Host " 2. Add your code and push" +Write-Host " 3. Setup CI/CD by closing the first issue" diff --git a/workflows/03-project-init.sh b/workflows/03-project-init.sh new file mode 100644 index 0000000..22a3b85 --- /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 +} + +PROJ_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) PROJ_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: $PROJ_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 "$PROJ_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" --name 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 $PROJ_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.ps1 b/workflows/04-multi-repo-collab.ps1 new file mode 100644 index 0000000..9e01e75 --- /dev/null +++ b/workflows/04-multi-repo-collab.ps1 @@ -0,0 +1,209 @@ +# ---------------------------------------------------------------- +# Scenario 4: Multi-Repo Collaboration +# Flow: Cross-repo issue tracking -> PR status dashboard -> Coordinated release +# +# Commands 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. release +list -- check release status across repos +# 5. release +create -- coordinated release (optional) +# 6. Generate HTML dashboard +# ---------------------------------------------------------------- +#Requires -Version 5.1 + +param( + [Parameter(Mandatory=$true)] + [string]$Org, + [string]$Repos = "", + [string]$Release = "", + [string]$Output = "dashboard.html", + [switch]$DryRun, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +Import-Module "$PSScriptRoot/lib/common.psm1" -Force + +if ($Help) { + Write-Host "Usage: powershell 04-multi-repo-collab.ps1 -Org ORG [-Repos 'repo1,repo2'] [-Release TAG] [-Output FILE] [-DryRun]" + exit 0 +} + +Check-Auth + +# -- Step 1: List Repositories -- +Log-Title "Multi-Repo Collaboration Dashboard" + +Log-Step "Fetching repositories for org: $Org..." +$reposJson = Invoke-GLCheck repo,+list,--user,$Org,--limit,100 +if (-not $reposJson) { Log-Err "Failed to fetch repos"; exit 1 } + +$allRepos = @() +$rd = $reposJson.data +if ($rd.projects) { $allRepos = @($rd.projects) } +elseif ($rd -is [array]) { $allRepos = $rd } + +Log-Ok "Found $($allRepos.Count) repositories" + +$repoList = @() +if ($Repos) { + $repoList = $Repos -split ',' + Log-Info "Filtering to specified repos: $($repoList -join ', ')" +} else { + foreach ($r in $allRepos) { + $rname = if ($r.name) { $r.name } elseif ($r.identifier) { $r.identifier } else { $null } + if ($rname) { $repoList += $rname } + } +} + +Log-Ok "Will process $($repoList.Count) repositories" + +# -- Step 2-3: Collect Issues and PRs from each repo -- +Log-Title "Collecting Data Across Repos" + +$totalIssues = 0; $totalOpenIssues = 0; $totalPRs = 0; $totalOpenPRs = 0 +$dashboardRows = "" + +foreach ($repo in $repoList) { + Divider + Log-Step "Processing $Org/$repo..." + + $issuesJson = Invoke-GL issue,+list,--owner,$Org,--repo,$repo,--state,open,--limit,50 + $openIssues = if ($issuesJson) { @($issuesJson.data.issues).Count } else { 0 } + + $closedJson = Invoke-GL issue,+list,--owner,$Org,--repo,$repo,--state,closed,--limit,50 + $closedIssues = if ($closedJson) { @($closedJson.data.issues).Count } else { 0 } + + $prsJson = Invoke-GL pr,+list,--owner,$Org,--repo,$repo,--state,open,--limit,50 + $openPRs = 0 + if ($prsJson) { + $pd = $prsJson.data + if ($pd.issues) { $openPRs = @($pd.issues).Count } + elseif ($pd.pulls) { $openPRs = @($pd.pulls).Count } + elseif ($pd -is [array]) { $openPRs = $pd.Count } + } + + $mergedJson = Invoke-GL pr,+list,--owner,$Org,--repo,$repo,--state,merged,--limit,50 + $mergedPRs = 0 + if ($mergedJson) { + $md = $mergedJson.data + if ($md.issues) { $mergedPRs = @($md.issues).Count } + elseif ($md.pulls) { $mergedPRs = @($md.pulls).Count } + elseif ($md -is [array]) { $mergedPRs = $md.Count } + } + + $releaseJson = Invoke-GL release,+list,--owner,$Org,--repo,$repo,--limit,1 + $latestRelease = "none" + if ($releaseJson -and $releaseJson.data.releases) { + $releases = @($releaseJson.data.releases) + if ($releases.Count -gt 0) { + $latestRelease = if ($releases[0].tag_name) { $releases[0].tag_name } elseif ($releases[0].name) { $releases[0].name } else { "none" } + } + } + + Log-Ok "$repo : Issues(open:$openIssues closed:$closedIssues) PRs(open:$openPRs merged:$mergedPRs) Release:$latestRelease" + + $statusColor = "green" + $healthText = "Healthy" + if ($openIssues -gt 10) { $statusColor = "orange"; $healthText = "Moderate" } + if ($openIssues -gt 20) { $statusColor = "red"; $healthText = "Needs Attention" } + + $dashboardRows += "`n" + $dashboardRows += " $repo`n" + $dashboardRows += " $openIssues$closedIssues`n" + $dashboardRows += " $openPRs$mergedPRs$latestRelease`n" + $dashboardRows += " $healthText`n" + $dashboardRows += "`n" + + $totalIssues += $openIssues + $closedIssues + $totalOpenIssues += $openIssues + $totalPRs += $openPRs + $mergedPRs + $totalOpenPRs += $openPRs +} + +# -- Step 4: Generate HTML Dashboard -- +Log-Title "Generating Dashboard" + +$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" +$repoCount = $repoList.Count + +$html = ' + + + + +Multi-Repo Collaboration Dashboard + + + +
+

Multi-Repo Collaboration Dashboard

+

Generated: ' + $timestamp + ' | Organization: ' + $Org + '

+
+

Total Repos

' + $repoCount + '
+

Open Issues

' + $totalOpenIssues + '
+

Open PRs

' + $totalOpenPRs + '
+

Total Activity

' + $totalIssues + '
+
+ + + +' + $dashboardRows + ' + +
RepositoryOpen IssuesClosed IssuesOpen PRsMerged PRsLatest ReleaseHealth
+
+ +' + +$html | Out-File -FilePath $Output -Encoding UTF8 +Log-Ok "Dashboard saved to: $Output" + +# -- Step 5: Coordinated Release -- +if ($Release) { + Log-Title "Coordinated Release: $Release" + + foreach ($repo in $repoList) { + Log-Step "Creating release for $Org/$repo..." + $relBody = "Coordinated release $Release for $Org/$repo" + $relResult = Invoke-GL release,+create,--owner,$Org,--repo,$repo,--tag,$Release,--name,"Release $Release",--body,$relBody + if ($relResult -and (Get-JsonOk ($relResult | ConvertFrom-Json))) { + Log-Ok "Release $Release created for $repo" + } else { + Log-Warn "Release creation failed for $repo (tag may already exist)" + } + } +} + +# ---------------------------------------------------------------- +Log-Title "Multi-Repo Dashboard Complete" +# ---------------------------------------------------------------- + +Write-Host " Repos processed: $repoCount" -ForegroundColor Green +Write-Host " Total issues: $totalIssues (open: $totalOpenIssues)" -ForegroundColor Green +Write-Host " Total PRs: $totalPRs (open: $totalOpenPRs)" -ForegroundColor Green +Write-Host " Dashboard: $Output" -ForegroundColor Green +if ($Release) { Write-Host " Coordinated release: $Release" -ForegroundColor Green } +Write-Host "" +Write-Host "Open dashboard:" -ForegroundColor Cyan +Write-Host " Start-Process $Output" diff --git a/workflows/04-multi-repo-collab.sh b/workflows/04-multi-repo-collab.sh new file mode 100644 index 0000000..2f4715a --- /dev/null +++ b/workflows/04-multi-repo-collab.sh @@ -0,0 +1,265 @@ +#!/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 --user "$ORG" --limit 100) +# Response may have .data.projects[] or .data[] +ALL_REPO_COUNT=$(echo "$REPOS_JSON" | jq '(.data.projects // .data | if type == "array" then . else [] end) | 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 + REPOS_DATA_PATH='(.data.projects // .data | if type == "array" then . else [] end)' + for i in $(seq 0 $((ALL_REPO_COUNT - 1))); do + RNAME=$(echo "$REPOS_JSON" | jq -r "$REPOS_DATA_PATH[$i].name // $REPOS_DATA_PATH[$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 // .data.pulls // .data | if type == "array" then . else [] end) | 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 // .data.pulls // .data | if type == "array" then . else [] end) | 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="" + PR_DATA_PATH='(.data.issues // .data.pulls // .data | if type == "array" then . else [] end)' + 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 "$PR_DATA_PATH[$pi].pull_request_number // $PR_DATA_PATH[$pi].number // $PR_DATA_PATH[$pi].id // empty") + PR_TITLE=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].subject // $PR_DATA_PATH[$pi].title // empty") + PR_AUTHOR=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].author_login // $PR_DATA_PATH[$pi].author.login // \"unknown\"") + PR_DETAILS+="#$PR_ID$PR_TITLE@$PR_AUTHORopen" + done + fi + + # Accumulate totals + TOTAL_ISSUES=$((TOTAL_ISSUES + OPEN_ISSUES + CLOSED_ISSUES)) + TOTAL_OPEN_ISSUES=$((TOTAL_OPEN_ISSUES + OPEN_ISSUES)) + TOTAL_PRS=$((TOTAL_PRS + OPEN_PRS + MERGED_PRS)) + TOTAL_OPEN_PRS=$((TOTAL_OPEN_PRS + OPEN_PRS)) + + # Add to dashboard rows + STATUS_COLOR="green" + [[ "$OPEN_ISSUES" -gt 10 ]] && STATUS_COLOR="orange" + [[ "$OPEN_ISSUES" -gt 20 ]] && STATUS_COLOR="red" + + DASHBOARD_ROWS+=" + $repo + $OPEN_ISSUES + $CLOSED_ISSUES + $OPEN_PRS + $MERGED_PRS + $LATEST_RELEASE + $( + [[ "$OPEN_ISSUES" -le 5 ]] && echo "Healthy" || \ + [[ "$OPEN_ISSUES" -le 15 ]] && echo "Moderate" || echo "Needs Attention" + ) + " +done + +# ── Step 4: Generate HTML Dashboard ────────────────────────────────── +log_title "Generating Dashboard" + +log_step "Creating HTML dashboard..." + +cat > "$OUTPUT_FILE" << 'HTMLEOF' + + + + + +Multi-Repo Collaboration Dashboard + + + +
+

Multi-Repo Collaboration Dashboard

+

Generated: TIMESTAMP_PLACEHOLDER | Organization: ORG_PLACEHOLDER

+
+

Total Repos

REPOS_COUNT
+

Open Issues

OPEN_ISSUES_COUNT
+

Open PRs

OPEN_PRS_COUNT
+

Total Activity

TOTAL_ACTIVITY
+
+ + +DASHBOARD_ROWS_PLACEHOLDER +
RepositoryOpen IssuesClosed IssuesOpen PRsMerged PRsLatest ReleaseHealth
+
+ + +HTMLEOF + +# Replace placeholders using temp file approach for complex content +TEMP_HTML=$(mktemp) +TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') + +while IFS= read -r line; do + line="${line//TIMESTAMP_PLACEHOLDER/$TIMESTAMP}" + line="${line//ORG_PLACEHOLDER/$ORG}" + line="${line//REPOS_COUNT/${#REPO_LIST[@]}}" + line="${line//OPEN_ISSUES_COUNT/$TOTAL_OPEN_ISSUES}" + line="${line//OPEN_PRS_COUNT/$TOTAL_OPEN_PRS}" + line="${line//TOTAL_ACTIVITY/$TOTAL_ISSUES}" + line="${line//DASHBOARD_ROWS_PLACEHOLDER/$DASHBOARD_ROWS}" + echo "$line" +done < "$OUTPUT_FILE" > "$TEMP_HTML" + +mv "$TEMP_HTML" "$OUTPUT_FILE" + +log_ok "Dashboard saved to: $OUTPUT_FILE" + +# ── Step 5: Coordinated Release ────────────────────────────────────── +if [[ -n "$RELEASE_TAG" ]]; then + log_title "Coordinated Release: $RELEASE_TAG" + + RELEASE_BODY="# Coordinated Release: $RELEASE_TAG + +## Repos Included +" + + for repo in "${REPO_LIST[@]}"; do + log_step "Creating release for $ORG/$repo..." + RELEASE_BODY+="- $ORG/$repo"$'\n' + + RELEASE_RESULT=$(gl_run release +create --owner "$ORG" --repo "$repo" \ + --tag "$RELEASE_TAG" --name "Release $RELEASE_TAG" \ + --body "Coordinated release $RELEASE_TAG for $ORG/$repo" 2>&1) || true + + if [[ "$(json_ok "$RELEASE_RESULT")" == "true" ]]; then + log_ok "Release $RELEASE_TAG created for $repo" + else + log_warn "Release creation failed for $repo (tag may already exist)" + fi + done + + RELEASE_BODY+=$'\n'"---"$'\n'"*Coordinated release by gitlink-cli multi-repo-collab workflow*" +fi + +# ───────────────────────────────────────────────────────────────────── +log_title "Multi-Repo Dashboard Complete" +# ───────────────────────────────────────────────────────────────────── + +echo -e "${GREEN}Summary:${NC}" +echo " Repos processed: ${#REPO_LIST[@]}" +echo " Total issues: $TOTAL_ISSUES (open: $TOTAL_OPEN_ISSUES)" +echo " Total PRs: $TOTAL_PRS (open: $TOTAL_OPEN_PRS)" +echo " Dashboard: $OUTPUT_FILE" +[[ -n "$RELEASE_TAG" ]] && echo " Coordinated release: $RELEASE_TAG" +echo "" +echo -e "${CYAN}Open dashboard:${NC}" +echo " xdg-open $OUTPUT_FILE # Linux" +echo " open $OUTPUT_FILE # macOS" +echo "" diff --git a/workflows/05-contributor-growth.ps1 b/workflows/05-contributor-growth.ps1 new file mode 100644 index 0000000..bba7ef6 --- /dev/null +++ b/workflows/05-contributor-growth.ps1 @@ -0,0 +1,432 @@ +# ---------------------------------------------------------------- +# Scenario 5: Contributor Growth System +# Flow: Collect data -> Calculate scores -> Generate HTML -> Publish Wiki -> Award badges +# +# Scoring (AHP weight model): +# - Issues Created: 15% weight (issue +list) +# - PRs Merged: 25% weight (pr +list state=merged) +# - Code Changes: 30% weight (pr +files) +# - Issue Comments: 15% weight (issue +view) +# - Team Member: 15% weight (repo +members) +# +# Badges: +# - Champion >= 80 +# - Core Contributor >= 60 +# - Active Contributor >= 40 +# - Contributor >= 20 +# - Newcomer < 20 +# ---------------------------------------------------------------- +#Requires -Version 5.1 + +param( + [string]$Owner = "", + [string]$Repo = "", + [int]$Sample = 10, + [switch]$Award, + [switch]$DryRun, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +Import-Module "$PSScriptRoot/lib/common.psm1" -Force + +if ($Help) { + Write-Host "Usage: powershell 05-contributor-growth.ps1 -Owner OWNER -Repo REPO [-Sample N] [-Award] [-DryRun]" + Write-Host "" + Write-Host " -Owner OWNER Repository owner" + Write-Host " -Repo REPO Repository name" + Write-Host " -Sample N Sample N PRs for code stats (default: 10)" + Write-Host " -Award Auto-create badge award issues" + Write-Host " -DryRun Preview actions without executing" + exit 0 +} + +Check-Auth +$r = Resolve-OwnerRepo $Owner $Repo +$Owner = $r.Owner; $Repo = $r.Repo + +$reportFile = "contrib-report-$Owner-$Repo.html" + +# ---------------------------------------------------------------- +Log-Title "Contributor Growth System: $Owner/$Repo" +# ---------------------------------------------------------------- + +# -- Step 1: Collect Data -- +Log-Step "Collecting data..." + +$issuesOpen = Invoke-GLCheck issue,+list,--owner,$Owner,--repo,$Repo,--state,open,--limit,100 +$issuesClosed = Invoke-GL issue,+list,--owner,$Owner,--repo,$Repo,--state,closed,--limit,100 +$openCount = if ($issuesOpen) { @($issuesOpen.data.issues).Count } else { 0 } +$closedCount = if ($issuesClosed) { @($issuesClosed.data.issues).Count } else { 0 } + +$prsMerged = Invoke-GL pr,+list,--owner,$Owner,--repo,$Repo,--state,merged,--limit,100 +$prMergedData = @() +if ($prsMerged) { + $pd = $prsMerged.data + if ($pd.issues) { $prMergedData = @($pd.issues) } + elseif ($pd.pulls) { $prMergedData = @($pd.pulls) } + elseif ($pd -is [array]) { $prMergedData = $pd } +} +$prMergedCount = $prMergedData.Count + +$membersJson = Invoke-GL repo,+members,--owner,$Owner,--repo,$Repo,--limit,100 +$memberData = @() +if ($membersJson) { + $md = $membersJson.data + if ($md.members) { $memberData = @($md.members) } + elseif ($md -is [array]) { $memberData = $md } +} +$memberCount = $memberData.Count + +Log-Ok "Issues(open:$openCount closed:$closedCount) PRs(merged:$prMergedCount) Members:$memberCount" + +# -- Step 2: Build Contributor Data -- +Log-Step "Building contributor profiles..." + +$contribData = @{} + +function Ensure-Contrib { + param([string]$User) + if (-not $User) { return } + if (-not $contribData.ContainsKey($User)) { + $contribData[$User] = @{ + Issues = 0; Merged = 0; Additions = 0; Deletions = 0; Comments = 0; IsMember = $false + } + } +} + +# Issues +$allIssues = @() +if ($issuesOpen) { $allIssues += @($issuesOpen.data.issues) } +if ($issuesClosed) { $allIssues += @($issuesClosed.data.issues) } +foreach ($issue in $allIssues) { + $author = if ($issue.author.login) { $issue.author.login } elseif ($issue.author.username) { $issue.author.username } else { $null } + if ($author) { + Ensure-Contrib $author + $contribData[$author].Issues++ + } +} + +# Merged PRs + code stats +Log-Step "Analyzing PR code changes (sampling $Sample)..." +$prSample = [Math]::Min($prMergedCount, $Sample) +for ($i = 0; $i -lt $prMergedCount; $i++) { + $pr = $prMergedData[$i] + $author = if ($pr.author_login) { $pr.author_login } elseif ($pr.author.login) { $pr.author.login } else { $null } + $prId = if ($pr.pull_request_number) { $pr.pull_request_number } elseif ($pr.number) { $pr.number } elseif ($pr.id) { $pr.id } else { $null } + if ($author) { + Ensure-Contrib $author + $contribData[$author].Merged++ + } + if ($i -lt $prSample -and $prId -and $author) { + $filesJson = Invoke-GL pr,+files,--owner,$Owner,--repo,$Repo,--id,$prId + if ($filesJson -and $filesJson.data.files) { + foreach ($f in $filesJson.data.files) { + $add = if ($f.additions) { $f.additions } elseif ($f.addition) { $f.addition } else { 0 } + $del = if ($f.deletions) { $f.deletions } elseif ($f.deletion) { $f.deletion } else { 0 } + $contribData[$author].Additions += $add + $contribData[$author].Deletions += $del + } + } + } +} + +# Members +foreach ($m in $memberData) { + $login = if ($m.login) { $m.login } elseif ($m.username) { $m.username } else { $null } + if ($login) { + Ensure-Contrib $login + $contribData[$login].IsMember = $true + } +} + +# Comments (sample open issues) +Log-Step "Sampling issue comments..." +if ($issuesOpen) { + $openIssuesArr = @($issuesOpen.data.issues) + $commentSample = [Math]::Min($openIssuesArr.Count, 10) + for ($i = 0; $i -lt $commentSample; $i++) { + $id = $openIssuesArr[$i].id + if (-not $id) { continue } + $detail = Invoke-GL issue,+view,--owner,$Owner,--repo,$Repo,--number,$id + if ($detail) { + $commentCount = if ($detail.data.comment_journals_count) { $detail.data.comment_journals_count } else { 0 } + if ($commentCount -gt 0) { + $author = $openIssuesArr[$i].author.login + if ($author) { + Ensure-Contrib $author + $contribData[$author].Comments += $commentCount + } + } + } + } +} + +# -- Step 3: Calculate Scores -- +Log-Step "Calculating scores..." + +$maxIssues = 0; $maxMerged = 0; $maxLines = 0; $maxComments = 0 +foreach ($user in $contribData.Keys) { + $c = $contribData[$user] + if ($c.Issues -gt $maxIssues) { $maxIssues = $c.Issues } + if ($c.Merged -gt $maxMerged) { $maxMerged = $c.Merged } + $lines = $c.Additions + $c.Deletions + if ($lines -gt $maxLines) { $maxLines = $lines } + if ($c.Comments -gt $maxComments) { $maxComments = $c.Comments } +} + +$scores = @{} +foreach ($user in $contribData.Keys) { + $c = $contribData[$user] + $ni = if ($maxIssues -gt 0) { $c.Issues / $maxIssues } else { 0 } + $nm = if ($maxMerged -gt 0) { $c.Merged / $maxMerged } else { 0 } + $lines = $c.Additions + $c.Deletions + $nl = if ($maxLines -gt 0) { $lines / $maxLines } else { 0 } + $nc = if ($maxComments -gt 0) { $c.Comments / $maxComments } else { 0 } + $ms = if ($c.IsMember) { 1 } else { 0 } + $score = [Math]::Round($ni * 15 + $nm * 25 + $nl * 30 + $nc * 15 + $ms * 15, 1) + $scores[$user] = $score +} + +# -- Step 4: Display Rankings -- +Log-Title "Contributor Rankings" +Write-Host "" +Write-Host ("{0,-4} {1,-18} {2,-8} {3,-8} {4,-12} {5,-10} {6,-8} {7}" -f "Rank","Contributor","Issues","Merged","+/- Lines","Comments","Score","Badge") -ForegroundColor White +Write-Host " ---- ------------------ -------- -------- ------------ ---------- -------- -------------" + +$ranked = $scores.GetEnumerator() | Sort-Object -Property Value -Descending +$rank = 1 +$rankedList = @() +foreach ($entry in $ranked) { + $user = $entry.Key + $score = $entry.Value + $c = $contribData[$user] + $si = [int]$score + $badge = if ($si -ge 80) { "Champion" } elseif ($si -ge 60) { "Core Contributor" } elseif ($si -ge 40) { "Active Contributor" } elseif ($si -ge 20) { "Contributor" } else { "Newcomer" } + $lines = $c.Additions + $c.Deletions + Write-Host ("{0,-4} {1,-18} {2,-8} {3,-8} +{4,-6}/-{5,-4} {6,-10} {7,-8} {8}" -f $rank,$user,$c.Issues,$c.Merged,$c.Additions,$c.Deletions,$c.Comments,$score,$badge) + $rankedList += @{ Rank=$rank; User=$user; Issues=$c.Issues; Merged=$c.Merged; Lines=$lines; Additions=$c.Additions; Deletions=$c.Deletions; Comments=$c.Comments; Score=$score; Badge=$badge } + $rank++ +} + +# -- Step 5: Generate HTML Report -- +Log-Title "Generating HTML Report" + +$pieData = "" +foreach ($entry in $ranked) { + $pieData += "{value: $($entry.Value), name: '$($entry.Key)'}," +} + +$tableRows = "" +foreach ($r in $rankedList) { + $rankCls = "" + if ($r.Rank -eq 1) { $rankCls = " rank-1" } + elseif ($r.Rank -eq 2) { $rankCls = " rank-2" } + elseif ($r.Rank -eq 3) { $rankCls = " rank-3" } + + $badgeCls = switch ($r.Badge) { + "Champion" { "champion" } + "Core Contributor" { "core" } + "Active Contributor" { "active" } + "Contributor" { "contributor" } + default { "newcomer" } + } + + $tableRows += ' ' + $r.Rank + '@' + $r.User + '' + $r.Issues + '' + $r.Merged + '' + $r.Lines + '' + $r.Comments + '' + $r.Score + '' + $r.Badge + '' + "`n" +} + +$totalIssuesCount = $openCount + $closedCount +$contribCount = $contribData.Count + +$html = ' + + + + + Contributor Report - ' + $Owner + '/' + $Repo + ' + + + + +
+
+

Contributor Report

+

' + $Owner + '/' + $Repo + ' - Team Contribution Analysis

+
+
+
' + $contribCount + '
Contributors
+
' + $totalIssuesCount + '
Total Issues
+
' + $prMergedCount + '
Merged PRs
+
+
+

Score Distribution

+
+
+
+

Detailed Rankings

+ +' + $tableRows + ' +
RankContributorIssuesMerged PRsCode LinesCommentsScoreBadge
+
+
+

Scoring System (AHP Weights)

+
+
+
Issues Created15%
+
PRs Merged25%
+
Code Changes30%
+
Issue Comments15%
+
Team Member15%
+
+
+
+
+ + +' + +$html | Out-File -FilePath $reportFile -Encoding UTF8 +Log-Ok "HTML report: $reportFile" + +# -- Step 6: Publish to Wiki -- +Log-Step "Publishing to Wiki..." + +$wikiRankRows = "" +foreach ($r in $rankedList) { + $shortBadge = switch ($r.Badge) { + "Champion" { "Champion" } + "Core Contributor" { "Core" } + "Active Contributor" { "Active" } + "Contributor" { "Contributor" } + default { "Newcomer" } + } + $wikiRankRows += "| $($r.Rank) | @$($r.User) | $($r.Issues) | $($r.Merged) | $($r.Lines) | $($r.Comments) | $($r.Score) | $shortBadge |" + "`n" +} + +$wikiTitle = "Contributor Leaderboard $(Get-Date -Format 'yyyy-MM-dd')" +$wikiContent = "# Contributor Leaderboard - $Owner/$Repo" + "`n`n" +$wikiContent += "*Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm')*" + "`n`n" +$wikiContent += "## Scoring System" + "`n`n" +$wikiContent += "| Dimension | Weight | Source |" + "`n" +$wikiContent += "|-----------|--------|--------|" + "`n" +$wikiContent += "| Issues Created | 15% | issue +list |" + "`n" +$wikiContent += "| PRs Merged | 25% | pr +list state=merged |" + "`n" +$wikiContent += "| Code Changes | 30% | pr +files |" + "`n" +$wikiContent += "| Issue Comments | 15% | issue +view |" + "`n" +$wikiContent += "| Team Member | 15% | repo +members |" + "`n`n" +$wikiContent += "## Rankings" + "`n`n" +$wikiContent += "| Rank | Contributor | Issues | Merged | Lines | Comments | Score | Badge |" + "`n" +$wikiContent += "|------|-------------|--------|--------|-------|----------|-------|-------|" + "`n" +$wikiContent += $wikiRankRows + "`n" +$wikiContent += "---" + "`n" +$wikiContent += "*Auto-generated by gitlink-cli*" + +$wikiResult = Invoke-GL wiki,+create,--owner,$Owner,--repo,$Repo,--title,$wikiTitle,--body,$wikiContent +if ($wikiResult -and (Get-JsonOk ($wikiResult | ConvertFrom-Json))) { + Log-Ok "Published to Wiki: $wikiTitle" +} else { + Log-Warn "Wiki publish failed" +} + +# -- Step 7: Award Badges (optional) -- +if ($Award) { + Log-Title "Awarding Badges" + + $badgeGroups = @{} + foreach ($r in $rankedList) { + if (-not $badgeGroups.ContainsKey($r.Badge)) { $badgeGroups[$r.Badge] = @() } + $badgeGroups[$r.Badge] += $r.User + } + + foreach ($badge in $badgeGroups.Keys) { + $users = $badgeGroups[$badge] + if ($badge -eq "Newcomer") { continue } + + $userList = ($users | ForEach-Object { "@$_" }) -join ", " + $issueTitle = "Badge Award: $badge" + $issueBody = "## Congratulations!" + "`n`n" + $issueBody += "The following contributors have earned the **$badge** badge:" + "`n`n" + $issueBody += $userList + "`n`n" + $issueBody += "### Badge Criteria" + "`n" + $issueBody += switch ($badge) { + "Champion" { "- Score >= 80: Exceptional contribution to the project" } + "Core Contributor" { "- Score >= 60: Significant and consistent contributions" } + "Active Contributor" { "- Score >= 40: Regular contributions to the project" } + "Contributor" { "- Score >= 20: Made meaningful contributions" } + } + $issueBody += "`n`n---`n*Auto-awarded by gitlink-cli contributor-growth workflow*" + + $issueResult = Invoke-GL issue,+create,--owner,$Owner,--repo,$Repo,--title,$issueTitle,--body,$issueBody + if ($issueResult) { + try { + $issueJson = $issueResult | ConvertFrom-Json + $issueNum = if ($issueJson.data.id) { $issueJson.data.id } elseif ($issueJson.data.number) { $issueJson.data.number } else { $null } + if ($issueNum) { + Invoke-GL issue,+label-add,--owner,$Owner,--repo,$Repo,--number,$issueNum,--labels,"badge" | Out-Null + Log-Ok "Badge issue created: #$issueNum - $issueTitle ($($users.Count) recipients)" + } + } catch { + Log-Warn "Badge issue creation may have failed: $issueTitle" + } + } + } +} + +# ---------------------------------------------------------------- +Log-Title "Complete" +# ---------------------------------------------------------------- + +Write-Host " Contributors: $contribCount" -ForegroundColor Green +Write-Host " HTML Report: $reportFile" -ForegroundColor Green +Write-Host " Wiki: $wikiTitle" -ForegroundColor Green +if ($Award) { Write-Host " Badges: Awarded" -ForegroundColor Green } diff --git a/workflows/05-contributor-growth.sh b/workflows/05-contributor-growth.sh new file mode 100644 index 0000000..bd0011e --- /dev/null +++ b/workflows/05-contributor-growth.sh @@ -0,0 +1,401 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────── +# Scenario 5: Contributor Growth System +# Flow: Collect data → Calculate scores → Generate HTML → Publish Wiki +# +# Scoring (based on available shortcuts): +# - Issue created: 15% weight (issue +list) +# - PR merged: 25% weight (pr +list state=merged) +# - Code changes: 30% weight (pr +files) +# - Issue comments: 15% weight (issue +view) +# - Team member: 15% weight (repo +members) +# ───────────────────────────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +usage() { + echo "Usage: $0 --owner OWNER --repo REPO [--sample N] [--dry-run]" + echo "" + echo " --owner OWNER Repository owner" + echo " --repo REPO Repository name" + echo " --sample N Sample N PRs for code stats (default: 10)" + echo " --dry-run Preview actions without executing" + exit 1 +} + +DRY_RUN=false +OWNER="" +REPO="" +SAMPLE_SIZE=10 + +while [[ $# -gt 0 ]]; do + case "$1" in + --owner) OWNER="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --sample) SAMPLE_SIZE="$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 + +REPORT_FILE="contrib-report-$OWNER-$REPO.html" + +# ───────────────────────────────────────────────────────────────────── +log_title "Contributor Growth System: $OWNER/$REPO" +# ───────────────────────────────────────────────────────────────────── + +# ── Step 1: Collect Data ───────────────────────────────────────────── +log_step "Collecting data..." + +ISSUES_OPEN=$(gl_check issue +list --owner "$OWNER" --repo "$REPO" --state open --limit 100) +ISSUES_CLOSED=$(gl_run issue +list --owner "$OWNER" --repo "$REPO" --state closed --limit 100) +OPEN_COUNT=$(echo "$ISSUES_OPEN" | jq '.data.issues | length' 2>/dev/null || echo "0") +CLOSED_COUNT=$(echo "$ISSUES_CLOSED" | jq '.data.issues | length' 2>/dev/null || echo "0") + +PRS_MERGED=$(gl_run pr +list --owner "$OWNER" --repo "$REPO" --state merged --limit 100) +PR_MERGED_COUNT=$(echo "$PRS_MERGED" | jq '(.data.issues // .data.pulls // .data | if type == "array" then . else [] end) | length' 2>/dev/null || echo "0") + +MEMBERS=$(gl_run repo +members --owner "$OWNER" --repo "$REPO" --limit 100) +# Members may be in .data.members[] or .data[] +MEMBER_COUNT=$(echo "$MEMBERS" | jq '(.data.members // .data | if type == "array" then . else [] end) | length' 2>/dev/null || echo "0") + +log_ok "Issues(open:$OPEN_COUNT closed:$CLOSED_COUNT) PRs(merged:$PR_MERGED_COUNT) Members:$MEMBER_COUNT" + +# ── Step 2: Build Contributor Data ─────────────────────────────────── +log_step "Building contributor profiles..." + +declare -A C_ISSUES C_MERGED C_ADDITIONS C_DELETIONS C_COMMENTS C_IS_MEMBER + +# Issues +for i in $(seq 0 $((OPEN_COUNT - 1))); do + A=$(echo "$ISSUES_OPEN" | jq -r ".data.issues[$i].author.login // empty") + [[ -n "$A" ]] && C_ISSUES["$A"]=$(( ${C_ISSUES["$A"]:-0} + 1 )) +done +for i in $(seq 0 $((CLOSED_COUNT - 1))); do + A=$(echo "$ISSUES_CLOSED" | jq -r ".data.issues[$i].author.login // empty") + [[ -n "$A" ]] && C_ISSUES["$A"]=$(( ${C_ISSUES["$A"]:-0} + 1 )) +done + +# Merged PRs + code stats +log_step "Analyzing PR code changes (sampling $SAMPLE_SIZE)..." +PR_SAMPLE=$((PR_MERGED_COUNT > SAMPLE_SIZE ? SAMPLE_SIZE : PR_MERGED_COUNT)) +PR_DATA_PATH='(.data.issues // .data.pulls // .data | if type == "array" then . else [] end)' +for i in $(seq 0 $((PR_MERGED_COUNT - 1))); do + A=$(echo "$PRS_MERGED" | jq -r "$PR_DATA_PATH[$i].author_login // $PR_DATA_PATH[$i].author.login // empty") + ID=$(echo "$PRS_MERGED" | jq -r "$PR_DATA_PATH[$i].pull_request_number // $PR_DATA_PATH[$i].number // $PR_DATA_PATH[$i].id // empty") + [[ -n "$A" ]] && C_MERGED["$A"]=$(( ${C_MERGED["$A"]:-0} + 1 )) + if [[ $i -lt $PR_SAMPLE ]] && [[ -n "$ID" ]]; then + FILES=$(gl_run pr +files --owner "$OWNER" --repo "$REPO" --id "$ID" 2>&1) + ADD=$(echo "$FILES" | jq -r '[.data.files[]? | (.additions // .addition // 0)] | add // 0' 2>/dev/null || echo "0") + DEL=$(echo "$FILES" | jq -r '[.data.files[]? | (.deletions // .deletion // 0)] | add // 0' 2>/dev/null || echo "0") + [[ -n "$A" ]] && C_ADDITIONS["$A"]=$(( ${C_ADDITIONS["$A"]:-0} + ADD )) + [[ -n "$A" ]] && C_DELETIONS["$A"]=$(( ${C_DELETIONS["$A"]:-0} + DEL )) + fi +done + +# Members +MEMBERS_DATA_PATH='(.data.members // .data | if type == "array" then . else [] end)' +for i in $(seq 0 $((MEMBER_COUNT - 1))); do + L=$(echo "$MEMBERS" | jq -r "$MEMBERS_DATA_PATH[$i].login // $MEMBERS_DATA_PATH[$i].username // empty") + [[ -n "$L" ]] && C_IS_MEMBER["$L"]="yes" +done + +# Comments (sample) +log_step "Sampling issue comments..." +for i in $(seq 0 $((OPEN_COUNT > 10 ? 9 : OPEN_COUNT - 1))); do + ID=$(echo "$ISSUES_OPEN" | jq -r ".data.issues[$i].id // empty") + [[ -z "$ID" ]] && continue + DETAIL=$(gl_run issue +view --owner "$OWNER" --repo "$REPO" --number "$ID" 2>&1) + C=$(echo "$DETAIL" | jq -r '.data.comment_journals_count // 0' 2>/dev/null || echo "0") + if [[ "$C" -gt 0 ]]; then + A=$(echo "$ISSUES_OPEN" | jq -r ".data.issues[$i].author.login // empty") + [[ -n "$A" ]] && C_COMMENTS["$A"]=$(( ${C_COMMENTS["$A"]:-0} + C )) + fi +done + +# ── Step 3: Calculate Scores ───────────────────────────────────────── +log_step "Calculating scores..." + +declare -A SCORES +ALL_USERS=() +for user in "${!C_ISSUES[@]}" "${!C_MERGED[@]}" "${!C_COMMENTS[@]}"; do + [[ -n "$user" ]] && ALL_USERS+=("$user") +done +ALL_USERS=($(printf '%s\n' "${ALL_USERS[@]}" | sort -u)) + +MAX_ISSUES=0; MAX_MERGED=0; MAX_LINES=0; MAX_COMMENTS=0 +for user in "${ALL_USERS[@]}"; do + [[ ${C_ISSUES[$user]:-0} -gt $MAX_ISSUES ]] && MAX_ISSUES=${C_ISSUES[$user]} + [[ ${C_MERGED[$user]:-0} -gt $MAX_MERGED ]] && MAX_MERGED=${C_MERGED[$user]} + LINES=$(( ${C_ADDITIONS[$user]:-0} + ${C_DELETIONS[$user]:-0} )) + [[ $LINES -gt $MAX_LINES ]] && MAX_LINES=$LINES + [[ ${C_COMMENTS[$user]:-0} -gt $MAX_COMMENTS ]] && MAX_COMMENTS=${C_COMMENTS[$user]} +done + +for user in "${ALL_USERS[@]}"; do + SCORE=$(awk -v iss="${C_ISSUES[$user]:-0}" -v mi="$MAX_ISSUES" \ + -v mer="${C_MERGED[$user]:-0}" -v mm="$MAX_MERGED" \ + -v lin="$(( ${C_ADDITIONS[$user]:-0} + ${C_DELETIONS[$user]:-0} ))" -v ml="$MAX_LINES" \ + -v com="${C_COMMENTS[$user]:-0}" -v mc="$MAX_COMMENTS" \ + -v mem="${C_IS_MEMBER[$user]:-no}" \ + 'BEGIN { + ni=(mi>0)?iss/mi:0; nm=(mm>0)?mer/mm:0; nl=(ml>0)?lin/ml:0; nc=(mc>0)?com/mc:0; ms=(mem=="yes")?1:0; + printf "%.1f", (ni*15+nm*25+nl*30+nc*15+ms*15) + }') + SCORES["$user"]="$SCORE" +done + +# ── Step 4: Display Rankings ───────────────────────────────────────── +log_title "Contributor Rankings" +echo "" +printf " ${BOLD}%-4s %-18s %-8s %-8s %-12s %-10s %-8s %s${NC}\n" "Rank" "Contributor" "Issues" "Merged" "+/- Lines" "Comments" "Score" "Badge" +echo " ──── ─────────────────── ──────── ──────── ──────────── ────────── ──────── ─────────────" + +TEMP=$(mktemp) +for u in "${!SCORES[@]}"; do echo "${SCORES[$u]} $u" >> "$TEMP"; done + +RANK=1 +sort -rn "$TEMP" | while read -r score user; do + iss=${C_ISSUES[$user]:-0}; mer=${C_MERGED[$user]:-0} + add=${C_ADDITIONS[$user]:-0}; del=${C_DELETIONS[$user]:-0} + com=${C_COMMENTS[$user]:-0}; si=${score%.*} + if [[ $si -ge 80 ]]; then B="Champion" + elif [[ $si -ge 60 ]]; then B="Core Contributor" + elif [[ $si -ge 40 ]]; then B="Active Contributor" + elif [[ $si -ge 20 ]]; then B="Contributor" + else B="Newcomer" + fi + printf " %-4d %-18s %-8d %-8d +%-6d/-%-4d %-10d %-8s %s\n" "$RANK" "$user" "$iss" "$mer" "$add" "$del" "$com" "$score" "$B" + RANK=$((RANK + 1)) +done +rm -f "$TEMP" + +# ── Step 5: Generate HTML Report ───────────────────────────────────── +log_title "Generating HTML Report" + +# Build JSON data for charts +PIE_DATA="" +TABLE_ROWS="" +RANK=1 + +TEMP2=$(mktemp) +for u in "${!SCORES[@]}"; do echo "${SCORES[$u]} $u" >> "$TEMP2"; done + +sort -rn "$TEMP2" | while read -r score user; do + iss=${C_ISSUES[$user]:-0}; mer=${C_MERGED[$user]:-0} + add=${C_ADDITIONS[$user]:-0}; del=${C_DELETIONS[$user]:-0} + lin=$((add + del)); com=${C_COMMENTS[$user]:-0} + si=${score%.*} + if [[ $si -ge 80 ]]; then B="Champion" + elif [[ $si -ge 60 ]]; then B="Core Contributor" + elif [[ $si -ge 40 ]]; then B="Active Contributor" + elif [[ $si -ge 20 ]]; then B="Contributor" + else B="Newcomer" + fi + # Output as CSV for processing + echo "$RANK|$user|$iss|$mer|$lin|$com|$score|$B|$add|$del" + RANK=$((RANK + 1)) +done > "$TEMP2.csv" +rm -f "$TEMP2" + +# Generate HTML +cat > "$REPORT_FILE" << 'HTMLHEAD' + + + + + + Contributor Report + + + + +
+
+

Contributor Report

+HTMLHEAD + +echo "

$OWNER/$REPO - Team Contribution Analysis

" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" + +# Stats cards +TOTAL_ISSUES=$((OPEN_COUNT + CLOSED_COUNT)) +echo "
" >> "$REPORT_FILE" +echo "
${#SCORES[@]}
Contributors
" >> "$REPORT_FILE" +echo "
$TOTAL_ISSUES
Total Issues
" >> "$REPORT_FILE" +echo "
$PR_MERGED_COUNT
Merged PRs
" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" + +# Pie chart +echo "
" >> "$REPORT_FILE" +echo "

Score Distribution

" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" + +# Rankings table +echo "
" >> "$REPORT_FILE" +echo "

Detailed Rankings

" >> "$REPORT_FILE" +echo " " >> "$REPORT_FILE" + +PIE_JSON="" +while IFS='|' read -r rank user iss mer lin com score badge add del; do + cls=""; [[ $rank -eq 1 ]] && cls=" rank-1" + [[ $rank -eq 2 ]] && cls=" rank-2" + [[ $rank -eq 3 ]] && cls=" rank-3" + + badge_cls="newcomer" + [[ "$badge" == "Champion" ]] && badge_cls="champion" + [[ "$badge" == "Core Contributor" ]] && badge_cls="core" + [[ "$badge" == "Active Contributor" ]] && badge_cls="active" + [[ "$badge" == "Contributor" ]] && badge_cls="contributor" + + echo " " >> "$REPORT_FILE" + PIE_JSON+="{value: $score, name: '$user'}," +done < "$TEMP2.csv" + +echo "
RankContributorIssuesMerged PRsCode LinesCommentsScoreBadge
$rank@$user$iss$mer$lin$com$score$badge
" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" + +# Weight info +echo "
" >> "$REPORT_FILE" +echo "

Scoring System (AHP Weights)

" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" +echo "
Issues Created15%
" >> "$REPORT_FILE" +echo "
PRs Merged25%
" >> "$REPORT_FILE" +echo "
Code Changes30%
" >> "$REPORT_FILE" +echo "
Issue Comments15%
" >> "$REPORT_FILE" +echo "
Team Member15%
" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" +echo "
" >> "$REPORT_FILE" + +# JavaScript +cat >> "$REPORT_FILE" << HTMLFOOT +
+ + + +HTMLFOOT + +rm -f "$TEMP2.csv" +log_ok "HTML report: $REPORT_FILE" + +# ── Step 6: Publish to Wiki ────────────────────────────────────────── +log_step "Publishing to Wiki..." + +WIKI_CONTENT="# Contributor Leaderboard - $OWNER/$REPO + +*Generated: $(date '+%Y-%m-%d %H:%M')* + +## Scoring System + +| Dimension | Weight | Source | +|-----------|--------|--------| +| Issues Created | 15% | \`issue +list\` | +| PRs Merged | 25% | \`pr +list state=merged\` | +| Code Changes | 30% | \`pr +files\` | +| Issue Comments | 15% | \`issue +view\` | +| Team Member | 15% | \`repo +members\` | + +## Rankings + +| Rank | Contributor | Issues | Merged | Lines | Comments | Score | Badge | +|------|-------------|--------|--------|-------|----------|-------|-------| +" + +TEMP3=$(mktemp) +for u in "${!SCORES[@]}"; do echo "${SCORES[$u]} $u" >> "$TEMP3"; done +RANK=1 +sort -rn "$TEMP3" | while read -r score user; do + iss=${C_ISSUES[$user]:-0}; mer=${C_MERGED[$user]:-0} + lin=$(( ${C_ADDITIONS[$user]:-0} + ${C_DELETIONS[$user]:-0} )) + com=${C_COMMENTS[$user]:-0}; si=${score%.*} + if [[ $si -ge 80 ]]; then B="Champion" + elif [[ $si -ge 60 ]]; then B="Core" + elif [[ $si -ge 40 ]]; then B="Active" + elif [[ $si -ge 20 ]]; then B="Contributor" + else B="Newcomer" + fi + echo "| $RANK | @$user | $iss | $mer | $lin | $com | $score | $B |" + RANK=$((RANK + 1)) +done > "$TEMP3.rows" +WIKI_CONTENT+=$(cat "$TEMP3.rows") +rm -f "$TEMP3" "$TEMP3.rows" + +WIKI_CONTENT+=" + +--- +*Auto-generated by gitlink-cli*" + +# Use timestamp to avoid title conflicts with cached deletions +WIKI_TITLE="Contributor Leaderboard $(date '+%Y-%m-%d')" +WIKI_RESULT=$(gl_run wiki +create --owner "$OWNER" --repo "$REPO" \ + --title "$WIKI_TITLE" --content "$WIKI_CONTENT" 2>&1) || true + +if [[ "$(json_ok "$WIKI_RESULT")" == "true" ]]; then + log_ok "Published to Wiki: $WIKI_TITLE" +else + log_warn "Wiki publish failed" +fi + +# ───────────────────────────────────────────────────────────────────── +log_title "Complete" +echo " Contributors: ${#SCORES[@]}" +echo " HTML Report: $REPORT_FILE" +echo "" diff --git a/workflows/README.md b/workflows/README.md new file mode 100644 index 0000000..1596eae --- /dev/null +++ b/workflows/README.md @@ -0,0 +1,515 @@ +# GitLink CLI 工作流自动化 + +5 个端到端自动化场景,将 gitlink-cli 的 shortcut 命令串联成完整工作流,解决实际项目管理痛点。 + +--- + +## 环境准备 + +### 1. 安装 gitlink-cli + +```bash +# 确认已安装 +gitlink-cli version + +# 未安装则从项目根目录构建 +cd /home/kevin/gitlink-cli +make build +``` + +### 2. 安装 jq + +脚本用 `jq` 解析 CLI 返回的 JSON。 + +```bash +# Ubuntu/Debian +sudo apt-get install -y jq + +# macOS +brew install jq +``` + +### 3. 登录认证 + +```bash +# 方式一:交互式登录(推荐) +gitlink-cli auth login + +# 方式二:环境变量 +export GITLINK_TOKEN="你的私人令牌" +# 令牌获取:https://gitlink.org.cn → 个人设置 → 私人令牌 + +# 验证 +gitlink-cli auth status +# 应显示:✓ Logged in as 用户名 +``` + +### 4. 验证环境 + +```bash +# 测试 JSON 输出是否正常 +gitlink-cli issue +list --owner zzx-coder --repo gitlink-cli --state open --limit 3 --format json | jq '.ok' +# 应输出:true +``` + +--- + +## 五个场景 + +| # | 场景 | 脚本 | 串联命令 | 解决什么问题 | +|---|------|------|---------|-------------| +| 1 | 社区运营自动化 | `01-community-ops.sh` | 7 个 | Issue 积压无人处理、周报手写、Release Notes 手动整理 | +| 2 | 代码质量看门人 | `02-code-quality-gatekeeper.sh` | 7 个 | PR 审查效率低、质量标准不统一、AI 代码审查(基于 gitlink-code-review skill) | +| 3 | 项目一键初始化 | `03-project-init.sh` | 6 个 | 新建项目重复劳动多、Issue/文档/分支保护手动配 | +| 4 | 多仓库协同 | `04-multi-repo-collab.sh` | 7 个 | 跨仓库状态分散、缺乏统一视图 | +| 5 | 贡献者成长体系 | `05-contributor-growth.sh` | 6 个 | 贡献者活跃度难追踪、缺乏激励机制 | + +--- + +## 场景一:社区运营自动化 + +**脚本**: `01-community-ops.sh` + +### 解决什么问题 + +新 Issue 没人分类、不知道谁该负责、社区周报手写、发版时才手忙脚乱写 Release Notes。 + +### 工作流程 + +``` +issue +list → 读取所有 open Issue + ↓ +按关键词分类: Bug / Feature / Question / Docs + ↓ +issue +label-add → 自动打标签 + ↓ +repo +members → 获取仓库成员列表 +issue +update → 轮询分配负责人 + ↓ +pr +list → 统计本周合并的 PR +issue +list → 统计本周关闭的 Issue + ↓ +wiki +create → 发布社区周报到 Wiki + ↓ +release +create → 自动生成 Release Notes +``` + +### 串联的命令 + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `issue +list` | 获取所有 open Issue | +| 2 | `issue +label-add` | 按分类打标签 (bug/feature/question/documentation) | +| 3 | `repo +members` | 获取仓库成员列表 | +| 4 | `issue +update` | 给 Bug/Feature Issue 分配负责人 | +| 5 | `pr +list` | 统计本周合并的 PR | +| 6 | `wiki +create` | 发布社区周报 | +| 7 | `release +create` | 自动生成 Release Notes | + +### 输出有什么用 + +- **标签分类**: 仓库 Issue 页面可按标签筛选,一目了然 +- **负责人分配**: 每个 Issue 有明确负责人,避免互相推诿 +- **Wiki 周报**: 团队和社区用户可在 Wiki 查看每周进展 +- **Release Notes**: 发版时无需手动整理变更 + +### 运行 + +```bash +bash workflows/01-community-ops.sh --owner 你的组织 --repo 你的仓库 + +# 示例 +bash workflows/01-community-ops.sh --owner zzx-coder --repo gitlink-cli +``` + +--- + +## 场景二:代码质量看门人 + +**脚本**: `02-code-quality-gatekeeper.sh` + +### 解决什么问题 + +PR 审查是代码质量的核心环节,但人工审查耗时且标准不统一。这个工作流加载 **gitlink-code-review skill** 的审查方法论,用 AI (Claude) 对 PR 进行四维度代码审查,自动打分评级,达标后自动合并。 + +### 工作流程 + +``` +pr +list → 获取所有 open PR + ↓ +pr +view → 读取 PR 详情 +pr +files → 获取变更文件列表 +pr +diff → 获取代码差异 + ↓ +加载 gitlink-code-review skill: + - 审查维度与检查项 + - 评分标准 (90-100 优秀, 75-89 良好, ...) + - 问题严重级别 (CRITICAL/HIGH/MEDIUM/LOW) + ↓ +┌─────────────────────────────────────┐ +│ AI 代码审查 (Claude + Skill) │ +│ 四维度评分 (各 0-25,总分 100): │ +│ - 代码质量: 复杂度、命名、注释 │ +│ - 安全性: SQL注入、XSS、敏感信息 │ +│ - 性能: 循环效率、资源泄漏、N+1 │ +│ - 可维护性: 重复、职责单一、耦合 │ +│ │ +│ 输出: │ +│ - 结构化问题清单 (severity+file+ │ +│ rule+description+suggestion) │ +│ - 优秀实践 (positive_notes) │ +│ - 改进建议 (recommendations) │ +│ - 总分 + PASS/FAIL │ +└─────────────────────────────────────┘ + ↓ +api POST /reviews → 发布审查评论到 PR + ↓ +ci +builds → 检查 CI 构建状态 + ↓ +pr +merge → 分数 >= 阈值 且 CI 通过 → 自动合并 +``` + +### AI 审查示例输出(基于 gitlink-code-review skill) + +``` +Overall Score: 88 / 100 +Code Quality: 23 / 25 +Security: 25 / 25 +Performance: 20 / 25 +Maintainability: 20 / 25 + +Issues Found: + - [LOW] quality: 条目格式说明中 PR 条目用 (@作者) 带括号,commit 条目用 (作者名) 不带 @ 前缀 + → 统一格式规范,建议 commit 条目也使用 (@作者) 格式 + - [LOW] maintainability: 示例中贡献者列表变更但完整变更日志链接仍指向旧仓库 + → 将变更日志链接中的 OWNER 也更新为与示例贡献者一致 + +Positive Notes: + + 变更目的清晰,所有文件的修改一致地贯彻了需求,无遗漏 + + 变更范围合理,仅修改文档和示例,不涉及代码逻辑变更,风险极低 + +Recommendations: + > 统一 PR 条目和 commit 条目的作者标注格式 + > 在 collect-data.md 中补充 author 字段为空时的降级处理说明 +``` + +### 串联的命令 + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `pr +list` | 获取 open PR 列表 | +| 2 | `pr +view` | 读取 PR 详情(标题、作者、状态) | +| 3 | `pr +files` | 获取变更文件列表 | +| 4 | `pr +diff` | 获取代码差异内容 | +| 5 | `gitlink-code-review` | 加载 skill 的审查维度、检查项、评分标准 | +| 6 | `claude -p` | AI 按 skill 方法论进行四维度代码审查 | +| 7 | `api POST .../reviews` | 将审查评论发布到 PR | +| 8 | `pr +merge` | 质量分 >= 阈值且 CI 通过时自动合并 | + +### 输出有什么用 + +- **结构化评分**: 每个 PR 有 0-100 的质量评分,团队可设定统一合并门槛 +- **AI 问题清单**: 自动列出安全隐患、性能问题、代码质量问题,人工审查时重点关注 +- **PR 评论**: 审查结果直接评论在 PR 上,作者和审查者都能看到 +- **自动合并**: 高质量 PR 无需人工点击 + +### 运行 + +```bash +# 审查所有 open PR +bash workflows/02-code-quality-gatekeeper.sh --owner 你的组织 --repo 你的仓库 + +# 审查指定 PR +bash workflows/02-code-quality-gatekeeper.sh --owner 你的组织 --repo 你的仓库 --pr-id 42 + +# 自定义质量阈值(默认 80) +bash workflows/02-code-quality-gatekeeper.sh --owner 你的组织 --repo 你的仓库 --threshold 70 + +# 预览模式(不实际合并) +bash workflows/02-code-quality-gatekeeper.sh --owner 你的组织 --repo 你的仓库 --dry-run + +# 示例 +bash workflows/02-code-quality-gatekeeper.sh --owner zzx-coder --repo gitlink-cli --pr-id 20 +``` + +--- + +## 场景三:项目一键初始化 + +**脚本**: `03-project-init.sh` + +### 解决什么问题 + +新建项目仓库后,还要手动创建 README、写 CONTRIBUTING 指南、创建初始 Issue、设置分支保护、打初始 Release。一条命令搞定全部。 + +### 工作流程 + +``` +repo +create → 创建仓库 + ↓ +wiki +create → 生成 README(根据语言模板) +wiki +create → 生成 CONTRIBUTING 贡献指南 + ↓ +issue +create × 5 → 创建初始待办 Issue: + - 搭建 CI/CD 流水线 + - 编写项目文档 + - 建立代码审查流程 + - 添加单元测试 + - 配置依赖管理 + ↓ +branch +protect → 保护 master 分支 + ↓ +release +create → 创建 v0.1.0 初始版本 +``` + +### 串联的命令 + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `repo +create` | 创建新仓库 | +| 2 | `wiki +create` | 生成 README(支持 Go/Python/Node/Java) | +| 3 | `wiki +create` | 生成 CONTRIBUTING 贡献指南 | +| 4 | `issue +create` | 创建 5 个初始 Issue 并打标签 | +| 5 | `branch +protect` | 设置 master 分支保护规则 | +| 6 | `release +create` | 创建 v0.1.0 初始版本 | + +### 输出有什么用 + +- **开箱即用**: 新成员克隆后就知道怎么构建、测试、贡献 +- **标准化 Issue**: 关键待办已创建好,团队可直接认领 +- **分支保护**: 防止直接 push 到 master,强制走 PR 流程 +- **首个 Release**: 项目从创建之初就有版本管理 + +### 运行 + +```bash +# Go 项目 +bash workflows/03-project-init.sh --owner 你的组织 --name my-go-app --description "我的Go应用" --lang go + +# Python 项目(私有) +bash workflows/03-project-init.sh --owner 你的组织 --name my-api --description "REST API服务" --lang python --private + +# Node.js 项目 +bash workflows/03-project-init.sh --owner 你的组织 --name my-web --description "Web前端" --lang node + +# Java 项目 +bash workflows/03-project-init.sh --owner 你的组织 --name my-service --description "微服务" --lang java +``` + +--- + +## 场景四:多仓库协同 + +**脚本**: `04-multi-repo-collab.sh` + +### 解决什么问题 + +当一个组织有多个仓库时,管理者需要逐个查看每个仓库的 Issue、PR、Release 状态。这个工作流汇总所有仓库数据,生成一个 HTML 仪表盘,并支持一键协调发版。 + +### 工作流程 + +``` +repo +list → 列出组织下所有仓库 + ↓ +对每个仓库: + issue +list → 获取 open/closed Issue + pr +list → 获取 open/merged PR + release +list → 获取最新 Release + ↓ +生成 HTML 仪表盘: + - 总览卡片: 仓库数、Open Issue、Open PR、总活动量 + - 详情表格: 每个仓库的 Issue/PR/Release 状态 + - 健康度: Healthy / Moderate / Needs Attention + ↓ +(可选)release +create → 一键为所有仓库创建同一版本号的 Release +``` + +### 串联的命令 + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `repo +list` | 列出组织下所有仓库 | +| 2 | `issue +list` | 获取每个仓库的 Issue 数据 | +| 3 | `pr +list` | 获取每个仓库的 PR 数据 | +| 4 | `release +list` | 获取每个仓库的最新 Release | +| 5 | 生成 HTML | 输出可视化仪表盘 | +| 6 | `release +create` | (可选)协调发版 | + +### 输出有什么用 + +- **统一视图**: 一个 HTML 页面看到组织所有仓库的健康状态 +- **健康度预警**: Open Issue 超 10 个标橙色,超 20 个标红色 +- **协调发版**: 多个关联仓库需要同步发版时,一条命令搞定 +- **可分享**: HTML 文件可直接发给团队或部署到内部网站 + +### 运行 + +```bash +# 扫描组织下所有仓库 +bash workflows/04-multi-repo-collab.sh --org 你的组织 + +# 只看指定仓库 +bash workflows/04-multi-repo-collab.sh --org 你的组织 --repos "repo-a,repo-b,repo-c" + +# 生成仪表盘 + 协调发版 +bash workflows/04-multi-repo-collab.sh --org 你的组织 --release v2.0.0 + +# 自定义输出文件 +bash workflows/04-multi-repo-collab.sh --org 你的组织 --output my-dashboard.html + +# 示例 +bash workflows/04-multi-repo-collab.sh --org zzx-coder +``` + +运行后在当前目录生成 `dashboard.html`,浏览器打开即可查看。 + +--- + +## 场景五:贡献者成长体系 + +**脚本**: `05-contributor-growth.sh` + +### 解决什么问题 + +开源项目需要激励贡献者持续参与,但很难量化每个人的贡献。这个工作流自动追踪贡献者活动,计算贡献分数,生成排行榜,并可选自动颁发成就徽章。 + +### 工作流程 + +``` +contrib +report → 生成带 ECharts 饼图的 HTML 贡献报告 + ↓ +issue +list → 统计 Issue 活动 +pr +list → 统计 PR 活动 +api GET /contributors → 获取提交数等 API 统计 + ↓ +计算贡献分数 (AHP 权重模型): + - PR 被合并: 10 分 + - 提交 PR: 5 分 + - 创建/解决 Issue: 3 分 + - 代码提交: 2 分 + ↓ +评定等级: + Champion (冠军) >= 50 分 + Core Contributor >= 30 分 + Active Contributor >= 15 分 + Contributor >= 5 分 + Newcomer (新人) < 5 分 + ↓ +(可选)issue +create → 自动创建徽章颁发 Issue + ↓ +wiki +create → 发布排行榜到 Wiki +``` + +### 串联的命令 + +| 步骤 | 命令 | 作用 | +|------|------|------| +| 1 | `contrib +report` | 生成 HTML 贡献报告(带 ECharts 图表) | +| 2 | `issue +list` | 统计 open/closed Issue 活动 | +| 3 | `pr +list` | 统计 open/merged PR 活动 | +| 4 | `api GET /contributors` | 获取 API 级别的贡献者统计 | +| 5 | `issue +create` | (可选)自动颁发成就徽章 | +| 6 | `wiki +create` | 发布排行榜到 Wiki | + +### 输出有什么用 + +- **HTML 贡献报告**: 可视化展示贡献分布,适合团队会议演示 +- **贡献排行榜**: 量化每个人的贡献,公开透明 +- **Wiki 排行榜**: 永久保存,贡献者可随时查看排名 +- **徽章激励**: 通过 Issue 颁发徽章,增强成就感和归属感 + +### 运行 + +```bash +# 基本运行 +bash workflows/05-contributor-growth.sh --owner 你的组织 --repo 你的仓库 + +# 自定义统计周期(默认 30 天) +bash workflows/05-contributor-growth.sh --owner 你的组织 --repo 你的仓库 --period 90 + +# 启用自动颁发徽章 +bash workflows/05-contributor-growth.sh --owner 你的组织 --repo 你的仓库 --award + +# 示例 +bash workflows/05-contributor-growth.sh --owner zzx-coder --repo gitlink-cli --award +``` + +--- + +## 通用参数 + +| 参数 | 说明 | +|------|------| +| `--owner OWNER` | 仓库所属组织或用户(在 git 仓库内可自动检测) | +| `--repo REPO` | 仓库名称(在 git 仓库内可自动检测) | +| `--dry-run` | 预览模式,不实际执行写操作 | +| `--help` | 显示帮助信息 | + +--- + +## 项目结构 + +``` +workflows/ +├── lib/ +│ └── common.sh # 共享工具库(认证、JSON解析、CLI封装、日志) +├── 01-community-ops.sh # 场景一:社区运营自动化 +├── 02-code-quality-gatekeeper.sh # 场景二:代码质量看门人(AI审查) +├── 03-project-init.sh # 场景三:项目一键初始化 +├── 04-multi-repo-collab.sh # 场景四:多仓库协同 +├── 05-contributor-growth.sh # 场景五:贡献者成长体系 +├── test.sh # 测试套件 +└── README.md # 本文档 +``` + +### 共享库 `lib/common.sh` + +所有脚本共享的基础设施: + +| 函数 | 作用 | +|------|------| +| `check_auth` | 检查认证状态(环境变量 或 CLI 登录) | +| `gl_run` | CLI 封装,自动追加 `--format json` | +| `gl_check` | CLI 封装 + JSON 格式校验 + ok 字段检查 | +| `json_ok` / `json_get` / `json_error` | JSON 解析工具 | +| `detect_owner_repo` | 从 git remote 自动检测 owner/repo | +| `log_step` / `log_ok` / `log_warn` / `log_err` | 彩色日志输出 | + +--- + +## 涉及的 Skill + +工作流通过加载 Skill 的审查方法论、分类规则和模板来指导 AI 分析: + +| Skill | 被哪个场景使用 | 作用 | +|-------|-------------|------| +| `gitlink-code-review` | 场景 2 | **已集成** — 加载审查维度、检查项、评分标准,指导 AI 代码审查 | +| `gitlink-issue-triage` | 场景 1 | Issue 分类规则(关键词匹配、优先级判定) | +| `gitlink-changelog` | 场景 1 | Release Notes 生成模板(按类型分组、贡献者列表) | +| `gitlink-health` | 场景 4 | 项目健康度评分体系(100 分制) | +| `gitlink-onboard` | 场景 5 | 新人引导和 Issue 推荐规则 | +| `gitlink-workflow` | 全部 | 基础工作流编排(Issue 分类、PR 审查、发版、Sprint 报告) | + +> 场景 2 的 `gitlink-code-review` skill 已完整集成:脚本运行时自动从 `skills/gitlink-code-review/SKILL.md` 加载审查维度和检查项,传给 AI 作为审查方法论。其他场景使用关键词匹配等规则引擎。 + +--- + +## 测试 + +```bash +# 运行测试套件(使用真实 GitLink 仓库验证) +bash workflows/test.sh + +# 指定仓库 +bash workflows/test.sh zzx-coder gitlink-cli +``` + +测试覆盖: +- 认证状态检查 +- CLI JSON 输出格式验证 +- 数据字段提取(issue/PR/repo/release/member/contributor) +- PR 文件和 Diff 内容解析 +- Issue/PR View 接口 +- Wiki / Label 列表接口 +- common.sh 工具函数 +- 所有脚本语法校验 diff --git a/workflows/lib/common.psm1 b/workflows/lib/common.psm1 new file mode 100644 index 0000000..612a8a0 --- /dev/null +++ b/workflows/lib/common.psm1 @@ -0,0 +1,92 @@ +# Common utilities for gitlink-cli workflow scripts (PowerShell 5.1+) + +$Script:GL = "gitlink-cli" + +# -- Logging -- +function Log-Step { param([string]$Msg) Write-Host "[STEP] $Msg" -ForegroundColor Blue } +function Log-Ok { param([string]$Msg) Write-Host "[ OK] $Msg" -ForegroundColor Green } +function Log-Warn { param([string]$Msg) Write-Host "[WARN] $Msg" -ForegroundColor Yellow } +function Log-Err { param([string]$Msg) Write-Host "[ ERR] $Msg" -ForegroundColor Red } +function Log-Info { param([string]$Msg) Write-Host "[INFO] $Msg" -ForegroundColor Cyan } +function Log-Title { param([string]$Msg) Write-Host ""; Write-Host "====== $Msg ======" -ForegroundColor White; Write-Host "" } +function Divider { Write-Host "------------------------------------------------" -ForegroundColor Cyan } + +# -- Auth Check -- +function Check-Auth { + if ($env:GITLINK_TOKEN) { + Log-Ok "GITLINK_TOKEN is set" + return + } + $status = & $Script:GL auth status 2>&1 + $statusStr = $status -join " " + if ($statusStr -match "logged in") { + Log-Ok "Authenticated" + return + } + Log-Err "Not authenticated. Please login first:" + Log-Info " gitlink-cli auth login" + Log-Info ' $env:GITLINK_TOKEN = "your-private-token"' + exit 1 +} + +# -- CLI Wrapper -- +function Invoke-GL { + param([string[]]$Args) + $output = & $Script:GL @Args --format json 2>&1 + return ($output -join "`n") +} + +function Invoke-GLCheck { + param([string[]]$Args) + $output = Invoke-GL $Args + try { + $json = $output | ConvertFrom-Json + if (-not $json.ok) { + $errMsg = if ($json.error.message) { $json.error.message } else { "unknown error" } + Log-Err "Command failed: $Script:GL $($Args -join ' ')" + Log-Err $errMsg + return $null + } + return $json + } catch { + Log-Err "Command failed (non-JSON): $Script:GL $($Args -join ' ')" + Log-Err $output + return $null + } +} + +# -- JSON Helpers -- +function Get-JsonOk { + param($Json) + return ($Json.ok -eq $true) +} + +# -- Owner/Repo Detection -- +function Detect-OwnerRepo { + $remote = git remote get-url origin 2>$null + if (-not $remote) { + Log-Err "No git remote 'origin' found. Use -Owner and -Repo flags." + exit 1 + } + if ($remote -match "gitlink\.org\.cn[:/]([^/]+)/([^/.]+)") { + return @{ Owner = $Matches[1]; Repo = $Matches[2] } + } + Log-Err "Cannot parse owner/repo from remote: $remote" + exit 1 +} + +function Resolve-OwnerRepo { + param([string]$Owner, [string]$Repo) + if (-not $Owner -or -not $Repo) { + $detected = Detect-OwnerRepo + if (-not $Owner) { $Owner = $detected.Owner } + if (-not $Repo) { $Repo = $detected.Repo } + } + Log-Info "Using: $Owner/$Repo" + return @{ Owner = $Owner; Repo = $Repo } +} + +# -- Date Helpers -- +function Get-DateToday { return (Get-Date -Format "yyyy-MM-dd") } + +Export-ModuleMember -Function * diff --git a/workflows/lib/common.sh b/workflows/lib/common.sh new file mode 100644 index 0000000..4a16f20 --- /dev/null +++ b/workflows/lib/common.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# Common utilities for gitlink-cli workflow scripts + +set -euo pipefail +# Trap SIGPIPE to prevent premature exit when piping through head/truncate +trap '' PIPE + +# Ensure jq is available (WinGet installs to non-default PATH on Windows) +if ! command -v jq &>/dev/null; then + for d in "$LOCALAPPDATA/Microsoft/WinGet/Links" "$HOME/AppData/Local/Microsoft/WinGet/Links"; do + [[ -d "$d" ]] && export PATH="$d:$PATH" + done +fi + +# Ensure CLAUDE_CODE_GIT_BASH_PATH is set for Windows (needed by claude CLI) +if [[ -z "${CLAUDE_CODE_GIT_BASH_PATH:-}" ]] && command -v cygpath &>/dev/null; then + export CLAUDE_CODE_GIT_BASH_PATH="$(cygpath -w "$(which bash)")" +fi + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# ── Logging ────────────────────────────────────────────────────────── +log_step() { echo -e "${BLUE}[STEP]${NC} $*"; } +log_ok() { echo -e "${GREEN}[ OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_err() { echo -e "${RED}[ ERR]${NC} $*" >&2; } +log_info() { echo -e "${CYAN}[INFO]${NC} $*"; } +log_title(){ echo -e "\n${BOLD}══════ $* ══════${NC}\n"; } + +# ── Auth Check ─────────────────────────────────────────────────────── +check_auth() { + # Check env var first, then try CLI auth status + if [[ -n "${GITLINK_TOKEN:-}" ]]; then + log_ok "GITLINK_TOKEN is set" + return 0 + fi + local status + status=$(gitlink-cli auth status 2>&1) + if echo "$status" | grep -qi "logged in\|✓"; then + log_ok "Authenticated: $(echo "$status" | sed -n 's/.*as //p' | tr -d '[:space:]')" + return 0 + fi + log_err "Not authenticated. Please login first:" + log_info " gitlink-cli auth login" + log_info " export GITLINK_TOKEN=\"your-private-token\"" + exit 1 +} + +# ── JSON Helpers ───────────────────────────────────────────────────── +# Extract a field from CLI JSON output (Envelope: {ok, data, ...}) +json_ok() { + echo "$1" | jq -r '.ok // false' 2>/dev/null +} + +json_data() { + echo "$1" | jq -r '.data' 2>/dev/null +} + +json_get() { + echo "$1" | jq -r "$2" 2>/dev/null +} + +json_error() { + echo "$1" | jq -r '.error.message // "unknown error"' 2>/dev/null +} + +# ── CLI Wrapper ────────────────────────────────────────────────────── +GL="gitlink-cli" + +gl_run() { + local output + # Always use JSON format for scripting + output=$("$GL" "$@" --format json 2>&1) || true + echo "$output" +} + +gl_check() { + local output + output=$(gl_run "$@") + # Check if output is valid JSON (use here-string to avoid SIGPIPE) + if ! jq empty <<< "$output" 2>/dev/null; then + log_err "Command failed (non-JSON response): $GL $*" + log_err "$output" + return 1 + fi + if [[ "$(json_ok "$output")" != "true" ]]; then + log_err "Command failed: $GL $*" + log_err "$(json_error "$output")" + return 1 + fi + echo "$output" +} + +# ── Owner/Repo Detection ──────────────────────────────────────────── +detect_owner_repo() { + local remote_url + remote_url=$(git remote get-url origin 2>/dev/null || echo "") + if [[ -z "$remote_url" ]]; then + log_err "No git remote 'origin' found. Use --owner and --repo flags." + exit 1 + fi + # Parse gitlink URL patterns + # https://gitlink.org.cn/owner/repo.git or git@gitlink.org.cn:owner/repo.git + if [[ "$remote_url" =~ gitlink\.org\.cn[:/]([^/]+)/([^/.]+) ]]; then + DETECTED_OWNER="${BASH_REMATCH[1]}" + DETECTED_REPO="${BASH_REMATCH[2]}" + else + log_err "Cannot parse owner/repo from remote: $remote_url" + exit 1 + fi +} + +require_owner_repo() { + if [[ -z "${OWNER:-}" || -z "${REPO:-}" ]]; then + detect_owner_repo + OWNER="${OWNER:-$DETECTED_OWNER}" + REPO="${REPO:-$DETECTED_REPO}" + fi + log_info "Using: ${OWNER}/${REPO}" +} + +# ── Confirmation ───────────────────────────────────────────────────── +confirm() { + local msg="${1:-Proceed?}" + if [[ "${DRY_RUN:-false}" == "true" ]]; then + log_warn "[DRY RUN] Would execute: $msg" + return 1 + fi + read -rp "$(echo -e "${YELLOW}$msg [y/N]${NC} ")" answer + [[ "$answer" =~ ^[Yy] ]] +} + +# ── Date Helpers ───────────────────────────────────────────────────── +date_today() { + date +%Y-%m-%d +} + +date_week_ago() { + date -d "7 days ago" +%Y-%m-%d 2>/dev/null || date -v-7d +%Y-%m-%d 2>/dev/null +} + +date_month_ago() { + date -d "30 days ago" +%Y-%m-%d 2>/dev/null || date -v-30d +%Y-%m-%d 2>/dev/null +} + +# ── Parameter Parsing ──────────────────────────────────────────────── +parse_common_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --owner) OWNER="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + --help|-h) usage; exit 0 ;; + *) break ;; + esac + done +} + +# ── Section Divider ────────────────────────────────────────────────── +divider() { + echo -e "${CYAN}────────────────────────────────────────────────${NC}" +}