forked from Gitlink/gitlink-cli
Merge pull request '1' (#24) from zk_branch into master
This commit is contained in:
commit
b2b0336236
|
|
@ -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 != "" {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
name: gitlink-onboard
|
||||
version: 2.0.0
|
||||
description: "新人引导:通过 AI 语义分析识别适合新手的 Good First Issue,调用 CLI 添加引导评论。当用户需要识别新手友好 issue 并欢迎新贡献者时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
|
|
|
|||
|
|
@ -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 <issue_id> --format json
|
||||
|
||||
# 3. 根据内容分析,通过 Raw API 添加标签
|
||||
gitlink-cli api POST /:owner/:repo/issues/:id --body '{"issue_tag_ids":[<tag_id>]}'
|
||||
```
|
||||
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 <pr_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 <pr_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 <pr_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` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -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 += "<tr>`n"
|
||||
$dashboardRows += " <td><a href=`"https://gitlink.org.cn/$Org/$repo`">$repo</a></td>`n"
|
||||
$dashboardRows += " <td>$openIssues</td><td>$closedIssues</td>`n"
|
||||
$dashboardRows += " <td>$openPRs</td><td>$mergedPRs</td><td>$latestRelease</td>`n"
|
||||
$dashboardRows += " <td style=`"color:$statusColor;font-weight:bold;`">$healthText</td>`n"
|
||||
$dashboardRows += "</tr>`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 = '<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Multi-Repo Collaboration Dashboard</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
h1 { color: #333; margin-bottom: 20px; }
|
||||
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 30px; }
|
||||
.card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.card h3 { color: #666; font-size: 14px; margin-bottom: 8px; }
|
||||
.card .value { font-size: 32px; font-weight: bold; color: #333; }
|
||||
.card.blue .value { color: #2196F3; }
|
||||
.card.green .value { color: #4CAF50; }
|
||||
.card.orange .value { color: #FF9800; }
|
||||
.card.purple .value { color: #9C27B0; }
|
||||
table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
th { background: #2196F3; color: white; padding: 12px 16px; text-align: left; }
|
||||
td { padding: 12px 16px; border-bottom: 1px solid #eee; }
|
||||
tr:hover { background: #f9f9f9; }
|
||||
a { color: #2196F3; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.timestamp { color: #999; font-size: 14px; margin-bottom: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Multi-Repo Collaboration Dashboard</h1>
|
||||
<p class="timestamp">Generated: ' + $timestamp + ' | Organization: ' + $Org + '</p>
|
||||
<div class="summary">
|
||||
<div class="card blue"><h3>Total Repos</h3><div class="value">' + $repoCount + '</div></div>
|
||||
<div class="card orange"><h3>Open Issues</h3><div class="value">' + $totalOpenIssues + '</div></div>
|
||||
<div class="card purple"><h3>Open PRs</h3><div class="value">' + $totalOpenPRs + '</div></div>
|
||||
<div class="card green"><h3>Total Activity</h3><div class="value">' + $totalIssues + '</div></div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Repository</th><th>Open Issues</th><th>Closed Issues</th><th>Open PRs</th><th>Merged PRs</th><th>Latest Release</th><th>Health</th></tr></thead>
|
||||
<tbody>
|
||||
' + $dashboardRows + '
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>'
|
||||
|
||||
$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"
|
||||
|
|
@ -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+="<tr><td>#$PR_ID</td><td>$PR_TITLE</td><td>@$PR_AUTHOR</td><td>open</td></tr>"
|
||||
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+="<tr>
|
||||
<td><a href=\"https://gitlink.org.cn/$ORG/$repo\">$repo</a></td>
|
||||
<td>$OPEN_ISSUES</td>
|
||||
<td>$CLOSED_ISSUES</td>
|
||||
<td>$OPEN_PRS</td>
|
||||
<td>$MERGED_PRS</td>
|
||||
<td>$LATEST_RELEASE</td>
|
||||
<td style=\"color:$STATUS_COLOR;font-weight:bold;\">$(
|
||||
[[ "$OPEN_ISSUES" -le 5 ]] && echo "Healthy" || \
|
||||
[[ "$OPEN_ISSUES" -le 15 ]] && echo "Moderate" || echo "Needs Attention"
|
||||
)</td>
|
||||
</tr>"
|
||||
done
|
||||
|
||||
# ── Step 4: Generate HTML Dashboard ──────────────────────────────────
|
||||
log_title "Generating Dashboard"
|
||||
|
||||
log_step "Creating HTML dashboard..."
|
||||
|
||||
cat > "$OUTPUT_FILE" << 'HTMLEOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Multi-Repo Collaboration Dashboard</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
h1 { color: #333; margin-bottom: 20px; }
|
||||
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 30px; }
|
||||
.card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.card h3 { color: #666; font-size: 14px; margin-bottom: 8px; }
|
||||
.card .value { font-size: 32px; font-weight: bold; color: #333; }
|
||||
.card.blue .value { color: #2196F3; }
|
||||
.card.green .value { color: #4CAF50; }
|
||||
.card.orange .value { color: #FF9800; }
|
||||
.card.purple .value { color: #9C27B0; }
|
||||
table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
th { background: #2196F3; color: white; padding: 12px 16px; text-align: left; }
|
||||
td { padding: 12px 16px; border-bottom: 1px solid #eee; }
|
||||
tr:hover { background: #f9f9f9; }
|
||||
a { color: #2196F3; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.timestamp { color: #999; font-size: 14px; margin-bottom: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Multi-Repo Collaboration Dashboard</h1>
|
||||
<p class="timestamp">Generated: TIMESTAMP_PLACEHOLDER | Organization: ORG_PLACEHOLDER</p>
|
||||
<div class="summary">
|
||||
<div class="card blue"><h3>Total Repos</h3><div class="value">REPOS_COUNT</div></div>
|
||||
<div class="card orange"><h3>Open Issues</h3><div class="value">OPEN_ISSUES_COUNT</div></div>
|
||||
<div class="card purple"><h3>Open PRs</h3><div class="value">OPEN_PRS_COUNT</div></div>
|
||||
<div class="card green"><h3>Total Activity</h3><div class="value">TOTAL_ACTIVITY</div></div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Repository</th><th>Open Issues</th><th>Closed Issues</th><th>Open PRs</th><th>Merged PRs</th><th>Latest Release</th><th>Health</th></tr></thead>
|
||||
<tbody>DASHBOARD_ROWS_PLACEHOLDER</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
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 ""
|
||||
|
|
@ -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 += ' <tr><td class="rank' + $rankCls + '">' + $r.Rank + '</td><td>@' + $r.User + '</td><td>' + $r.Issues + '</td><td>' + $r.Merged + '</td><td>' + $r.Lines + '</td><td>' + $r.Comments + '</td><td>' + $r.Score + '</td><td><span class="badge badge-' + $badgeCls + '">' + $r.Badge + '</span></td></tr>' + "`n"
|
||||
}
|
||||
|
||||
$totalIssuesCount = $openCount + $closedCount
|
||||
$contribCount = $contribData.Count
|
||||
|
||||
$html = '<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Contributor Report - ' + $Owner + '/' + $Repo + '</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 40px 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { text-align: center; color: white; margin-bottom: 40px; }
|
||||
.header h1 { font-size: 2.5rem; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
|
||||
.header p { font-size: 1.1rem; opacity: 0.9; }
|
||||
.card { background: white; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); padding: 30px; margin-bottom: 30px; }
|
||||
.card h2 { color: #333; margin-bottom: 20px; font-size: 1.5rem; border-bottom: 3px solid #667eea; padding-bottom: 10px; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
||||
.stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 20px; color: white; text-align: center; }
|
||||
.stat-value { font-size: 2rem; font-weight: bold; margin-bottom: 5px; }
|
||||
.stat-label { font-size: 0.9rem; opacity: 0.9; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 12px; text-align: left; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; }
|
||||
td { padding: 12px; border-bottom: 1px solid #eee; }
|
||||
tr:hover { background: #f8f9ff; }
|
||||
.rank { font-weight: bold; color: #667eea; font-size: 1.2rem; }
|
||||
.rank-1 { color: #FFD700; }
|
||||
.rank-2 { color: #C0C0C0; }
|
||||
.rank-3 { color: #CD7F32; }
|
||||
.badge { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; }
|
||||
.badge-champion { background: #FFD700; color: #333; }
|
||||
.badge-core { background: #C0C0C0; color: #333; }
|
||||
.badge-active { background: #CD7F32; color: white; }
|
||||
.badge-contributor { background: #4CAF50; color: white; }
|
||||
.badge-newcomer { background: #9E9E9E; color: white; }
|
||||
.chart-container { width: 100%; height: 400px; }
|
||||
.weight-info { background: #f8f9ff; border-radius: 12px; padding: 20px; margin-top: 20px; }
|
||||
.weight-info h3 { color: #667eea; margin-bottom: 15px; }
|
||||
.weight-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
|
||||
.weight-item { display: flex; justify-content: space-between; padding: 8px 12px; background: white; border-radius: 8px; border-left: 4px solid #667eea; }
|
||||
.weight-label { color: #666; }
|
||||
.weight-value { font-weight: 600; color: #667eea; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Contributor Report</h1>
|
||||
<p>' + $Owner + '/' + $Repo + ' - Team Contribution Analysis</p>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card"><div class="stat-value">' + $contribCount + '</div><div class="stat-label">Contributors</div></div>
|
||||
<div class="stat-card"><div class="stat-value">' + $totalIssuesCount + '</div><div class="stat-label">Total Issues</div></div>
|
||||
<div class="stat-card"><div class="stat-value">' + $prMergedCount + '</div><div class="stat-label">Merged PRs</div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Score Distribution</h2>
|
||||
<div id="pieChart" class="chart-container"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Detailed Rankings</h2>
|
||||
<table><thead><tr><th>Rank</th><th>Contributor</th><th>Issues</th><th>Merged PRs</th><th>Code Lines</th><th>Comments</th><th>Score</th><th>Badge</th></tr></thead><tbody>
|
||||
' + $tableRows + '
|
||||
</tbody></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Scoring System (AHP Weights)</h2>
|
||||
<div class="weight-info">
|
||||
<div class="weight-grid">
|
||||
<div class="weight-item"><span class="weight-label">Issues Created</span><span class="weight-value">15%</span></div>
|
||||
<div class="weight-item"><span class="weight-label">PRs Merged</span><span class="weight-value">25%</span></div>
|
||||
<div class="weight-item"><span class="weight-label">Code Changes</span><span class="weight-value">30%</span></div>
|
||||
<div class="weight-item"><span class="weight-label">Issue Comments</span><span class="weight-value">15%</span></div>
|
||||
<div class="weight-item"><span class="weight-label">Team Member</span><span class="weight-value">15%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var chart = echarts.init(document.getElementById("pieChart"));
|
||||
chart.setOption({
|
||||
tooltip: { trigger: "item", formatter: "{a} <br/>{b}: {c} ({d}%)" },
|
||||
legend: { orient: "vertical", left: "left", top: "middle" },
|
||||
series: [{
|
||||
name: "Score",
|
||||
type: "pie",
|
||||
radius: ["40%", "70%"],
|
||||
center: ["60%", "50%"],
|
||||
itemStyle: { borderRadius: 10, borderColor: "#fff", borderWidth: 2 },
|
||||
label: { show: true, formatter: "{b}\n{d}%" },
|
||||
data: [' + $pieData + ']
|
||||
}]
|
||||
});
|
||||
window.addEventListener("resize", function() { chart.resize(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>'
|
||||
|
||||
$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 }
|
||||
|
|
@ -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'
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Contributor Report</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 40px 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { text-align: center; color: white; margin-bottom: 40px; }
|
||||
.header h1 { font-size: 2.5rem; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
|
||||
.header p { font-size: 1.1rem; opacity: 0.9; }
|
||||
.card { background: white; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); padding: 30px; margin-bottom: 30px; }
|
||||
.card h2 { color: #333; margin-bottom: 20px; font-size: 1.5rem; border-bottom: 3px solid #667eea; padding-bottom: 10px; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
||||
.stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 20px; color: white; text-align: center; }
|
||||
.stat-value { font-size: 2rem; font-weight: bold; margin-bottom: 5px; }
|
||||
.stat-label { font-size: 0.9rem; opacity: 0.9; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 12px; text-align: left; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; }
|
||||
td { padding: 12px; border-bottom: 1px solid #eee; }
|
||||
tr:hover { background: #f8f9ff; }
|
||||
.rank { font-weight: bold; color: #667eea; font-size: 1.2rem; }
|
||||
.rank-1 { color: #FFD700; }
|
||||
.rank-2 { color: #C0C0C0; }
|
||||
.rank-3 { color: #CD7F32; }
|
||||
.badge { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; }
|
||||
.badge-champion { background: #FFD700; color: #333; }
|
||||
.badge-core { background: #C0C0C0; color: #333; }
|
||||
.badge-active { background: #CD7F32; color: white; }
|
||||
.badge-contributor { background: #4CAF50; color: white; }
|
||||
.badge-newcomer { background: #9E9E9E; color: white; }
|
||||
.chart-container { width: 100%; height: 400px; }
|
||||
.weight-info { background: #f8f9ff; border-radius: 12px; padding: 20px; margin-top: 20px; }
|
||||
.weight-info h3 { color: #667eea; margin-bottom: 15px; }
|
||||
.weight-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
|
||||
.weight-item { display: flex; justify-content: space-between; padding: 8px 12px; background: white; border-radius: 8px; border-left: 4px solid #667eea; }
|
||||
.weight-label { color: #666; }
|
||||
.weight-value { font-weight: 600; color: #667eea; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Contributor Report</h1>
|
||||
HTMLHEAD
|
||||
|
||||
echo " <p>$OWNER/$REPO - Team Contribution Analysis</p>" >> "$REPORT_FILE"
|
||||
echo " </div>" >> "$REPORT_FILE"
|
||||
|
||||
# Stats cards
|
||||
TOTAL_ISSUES=$((OPEN_COUNT + CLOSED_COUNT))
|
||||
echo " <div class=\"stats-grid\">" >> "$REPORT_FILE"
|
||||
echo " <div class=\"stat-card\"><div class=\"stat-value\">${#SCORES[@]}</div><div class=\"stat-label\">Contributors</div></div>" >> "$REPORT_FILE"
|
||||
echo " <div class=\"stat-card\"><div class=\"stat-value\">$TOTAL_ISSUES</div><div class=\"stat-label\">Total Issues</div></div>" >> "$REPORT_FILE"
|
||||
echo " <div class=\"stat-card\"><div class=\"stat-value\">$PR_MERGED_COUNT</div><div class=\"stat-label\">Merged PRs</div></div>" >> "$REPORT_FILE"
|
||||
echo " </div>" >> "$REPORT_FILE"
|
||||
|
||||
# Pie chart
|
||||
echo " <div class=\"card\">" >> "$REPORT_FILE"
|
||||
echo " <h2>Score Distribution</h2>" >> "$REPORT_FILE"
|
||||
echo " <div id=\"pieChart\" class=\"chart-container\"></div>" >> "$REPORT_FILE"
|
||||
echo " </div>" >> "$REPORT_FILE"
|
||||
|
||||
# Rankings table
|
||||
echo " <div class=\"card\">" >> "$REPORT_FILE"
|
||||
echo " <h2>Detailed Rankings</h2>" >> "$REPORT_FILE"
|
||||
echo " <table><thead><tr><th>Rank</th><th>Contributor</th><th>Issues</th><th>Merged PRs</th><th>Code Lines</th><th>Comments</th><th>Score</th><th>Badge</th></tr></thead><tbody>" >> "$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 " <tr><td class=\"rank$cls\">$rank</td><td>@$user</td><td>$iss</td><td>$mer</td><td>$lin</td><td>$com</td><td>$score</td><td><span class=\"badge badge-$badge_cls\">$badge</span></td></tr>" >> "$REPORT_FILE"
|
||||
PIE_JSON+="{value: $score, name: '$user'},"
|
||||
done < "$TEMP2.csv"
|
||||
|
||||
echo " </tbody></table>" >> "$REPORT_FILE"
|
||||
echo " </div>" >> "$REPORT_FILE"
|
||||
|
||||
# Weight info
|
||||
echo " <div class=\"card\">" >> "$REPORT_FILE"
|
||||
echo " <h2>Scoring System (AHP Weights)</h2>" >> "$REPORT_FILE"
|
||||
echo " <div class=\"weight-info\">" >> "$REPORT_FILE"
|
||||
echo " <div class=\"weight-grid\">" >> "$REPORT_FILE"
|
||||
echo " <div class=\"weight-item\"><span class=\"weight-label\">Issues Created</span><span class=\"weight-value\">15%</span></div>" >> "$REPORT_FILE"
|
||||
echo " <div class=\"weight-item\"><span class=\"weight-label\">PRs Merged</span><span class=\"weight-value\">25%</span></div>" >> "$REPORT_FILE"
|
||||
echo " <div class=\"weight-item\"><span class=\"weight-label\">Code Changes</span><span class=\"weight-value\">30%</span></div>" >> "$REPORT_FILE"
|
||||
echo " <div class=\"weight-item\"><span class=\"weight-label\">Issue Comments</span><span class=\"weight-value\">15%</span></div>" >> "$REPORT_FILE"
|
||||
echo " <div class=\"weight-item\"><span class=\"weight-label\">Team Member</span><span class=\"weight-value\">15%</span></div>" >> "$REPORT_FILE"
|
||||
echo " </div>" >> "$REPORT_FILE"
|
||||
echo " </div>" >> "$REPORT_FILE"
|
||||
echo " </div>" >> "$REPORT_FILE"
|
||||
|
||||
# JavaScript
|
||||
cat >> "$REPORT_FILE" << HTMLFOOT
|
||||
</div>
|
||||
<script>
|
||||
var chart = echarts.init(document.getElementById('pieChart'));
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
|
||||
legend: { orient: 'vertical', left: 'left', top: 'middle' },
|
||||
series: [{
|
||||
name: 'Score',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['60%', '50%'],
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: true, formatter: '{b}\\n{d}%' },
|
||||
data: [$PIE_JSON]
|
||||
}]
|
||||
});
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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 ""
|
||||
|
|
@ -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 工具函数
|
||||
- 所有脚本语法校验
|
||||
|
|
@ -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 *
|
||||
|
|
@ -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}"
|
||||
}
|
||||
Loading…
Reference in New Issue