diff --git a/workflows/04-multi-repo-collab.ps1 b/workflows/04-multi-repo-collab.ps1
index 24753c09..6e41b9b6 100644
--- a/workflows/04-multi-repo-collab.ps1
+++ b/workflows/04-multi-repo-collab.ps1
@@ -1,14 +1,14 @@
-# ----------------------------------------------------------------
+# ----------------------------------------------------------------
# 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
+# 1. repo +list -- list all repos in org
+# 2. issue +list -- fetch open/closed issues from each repo
+# 3. pr +list -- fetch open/merged PRs from each repo
+# 4. release +list -- check latest release across repos
+# 5. Generate HTML dashboard (summary cards + table + per-repo details)
+# 6. release +create -- (optional) coordinated release across all repos
# ----------------------------------------------------------------
#Requires -Version 5.1
@@ -17,6 +17,7 @@ param(
[string]$Repos = "",
[string]$Release = "",
[string]$Output = "dashboard.html",
+ [int]$DetailLimit = 10,
[switch]$DryRun,
[switch]$Help
)
@@ -25,7 +26,7 @@ $ErrorActionPreference = "Stop"
Import-Module "$PSScriptRoot/lib/common.psm1" -Force -WarningAction SilentlyContinue
if ($Help) {
- Write-Host "Usage: powershell 04-multi-repo-collab.ps1 -Org ORG [-Repos 'repo1,repo2'] [-Release TAG] [-Output FILE] [-DryRun]"
+ Write-Host "Usage: powershell 04-multi-repo-collab.ps1 -Org ORG [-Repos 'repo1,repo2'] [-Release TAG] [-Output FILE] [-DetailLimit N] [-DryRun]"
exit 0
}
@@ -33,24 +34,66 @@ if (-not $Org) { Log-Err "Org is required (use -Help for usage)"; exit 1 }
Check-Auth
-# -- Step 1: List Repositories --
-Log-Title "Multi-Repo Collaboration Dashboard"
+# -- Helpers --
+function Html-Escape([string]$s) {
+ if ($null -eq $s) { return "" }
+ $s = $s -replace '&', '&'
+ $s = $s -replace '<', '<'
+ $s = $s -replace '>', '>'
+ $s = $s -replace '"', '"'
+ return $s
+}
-Log-Step "Fetching repositories for org: $Org..."
+# Health score (0-100) aligned with gitlink-health skill.
+# Core 75 (issue efficiency / PR efficiency / activity, -25 each),
+# auxiliary 25 (issue backlog -10, PR backlog -10, no recent release -5).
+# releaseAge: days since latest release, -1 = none.
+function Compute-Health([int]$oi, [int]$ci, [int]$op, [int]$mp, [int]$age) {
+ $health = 100
+ # 核心指标 (75)
+ $ti = $oi + $ci
+ $res = if ($ti -gt 0) { $ci / $ti } else { 1 }
+ if ($res -lt 0.5) { $health -= 25 } # Issue 处理效率 (响应慢代理)
+ $tp = $op + $mp
+ $mr = if ($tp -gt 0) { $mp / $tp } else { 1 }
+ if ($mr -lt 0.5) { $health -= 25 } # PR 合并效率 (合并慢代理)
+ if (($mp + $ci) -eq 0) { $health -= 25 } # 活跃度低: 无已完成工作
+ # 辅助指标 (25)
+ if ($oi -gt 20) { $health -= 10 } # Issue 积压
+ if ($op -gt 10) { $health -= 10 } # PR 积压
+ if ($age -lt 0 -or $age -gt 30) { $health -= 5 } # 无近期发布
+ if ($health -lt 0) { $health = 0 }
+ if ($health -gt 100) { $health = 100 }
+ return [int]$health
+}
+
+# Map score -> @{ Label; Class } (5 bands per gitlink-health skill)
+function Get-HealthGrade([int]$score) {
+ if ($score -ge 90) { return @{ Label = "优秀"; Class = "excellent" } }
+ elseif ($score -ge 70) { return @{ Label = "良好"; Class = "good" } }
+ elseif ($score -ge 50) { return @{ Label = "一般"; Class = "moderate" } }
+ elseif ($score -ge 30) { return @{ Label = "需关注"; Class = "attention" } }
+ else { return @{ Label = "严重"; Class = "critical" } }
+}
+
+# -- Step 1: List Repositories --
+Log-Title "多仓库协同看板 (Multi-Repo Collaboration Dashboard)"
+
+Log-Step "获取组织 $Org 下的仓库列表..."
$reposJson = Invoke-GLCheck "repo", "+list", "--user", $Org, "--limit", "100"
-if (-not $reposJson) { Log-Err "Failed to fetch repos"; exit 1 }
+if (-not $reposJson) { Log-Err "获取仓库列表失败"; exit 1 }
$allRepos = @()
$rd = $reposJson.data
if ($rd.projects) { $allRepos = @($rd.projects) }
elseif ($rd -is [array]) { $allRepos = $rd }
-Log-Ok "Found $($allRepos.Count) repositories"
+Log-Ok "组织下共有 $($allRepos.Count) 个仓库"
$repoList = @()
if ($Repos) {
$repoList = $Repos -split ','
- Log-Info "Filtering to specified repos: $($repoList -join ', ')"
+ Log-Info "仅处理指定仓库: $($repoList -join ', ')"
} else {
foreach ($r in $allRepos) {
$rname = if ($r.name) { $r.name } elseif ($r.identifier) { $r.identifier } else { $null }
@@ -58,182 +101,259 @@ if ($Repos) {
}
}
-Log-Ok "Will process $($repoList.Count) repositories"
+if ($repoList.Count -eq 0) { Log-Err "没有可处理的仓库。请检查 -Org 或用 -Repos 指定。"; exit 1 }
+Log-Ok "将处理 $($repoList.Count) 个仓库"
-# -- Step 2-3: Collect Issues and PRs from each repo --
-Log-Title "Collecting Data Across Repos"
+# -- Step 2-4: Collect data across repos --
+Log-Title "跨仓库数据采集"
-$totalIssues = 0; $totalOpenIssues = 0; $totalPRs = 0; $totalOpenPRs = 0
+$totalActivity = 0; $totalOpenIssues = 0; $totalOpenPRs = 0; $totalMergedPRs = 0; $healthSum = 0
$dashboardRows = ""
+$repoDetails = ""
foreach ($repo in $repoList) {
Divider
- Log-Step "Processing $Org/$repo..."
+ Log-Step "处理 $Org/$repo..."
+ # Open issues
+ $issuesObj = $null; $openIssues = 0; $openIssueItems = @()
$issuesRaw = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50"
- $openIssues = 0
- if ($issuesRaw) {
- try {
- $issuesObj = $issuesRaw | ConvertFrom-Json
- if ($issuesObj.ok -and $issuesObj.data.issues) {
- $openIssues = @($issuesObj.data.issues).Count
- }
- } catch {}
- }
+ if ($issuesRaw) { try {
+ $issuesObj = $issuesRaw | ConvertFrom-Json
+ if ($issuesObj.ok -and $issuesObj.data.issues) { $openIssueItems = @($issuesObj.data.issues); $openIssues = $openIssueItems.Count }
+ } catch {} }
- $closedRaw = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "closed", "--limit", "50"
+ # Closed issues
$closedIssues = 0
- if ($closedRaw) {
- try {
- $closedObj = $closedRaw | ConvertFrom-Json
- if ($closedObj.ok -and $closedObj.data.issues) {
- $closedIssues = @($closedObj.data.issues).Count
- }
- } catch {}
- }
+ $closedRaw = Invoke-GL "issue", "+list", "--owner", $Org, "--repo", $repo, "--state", "closed", "--limit", "50"
+ if ($closedRaw) { try {
+ $closedObj = $closedRaw | ConvertFrom-Json
+ if ($closedObj.ok -and $closedObj.data.issues) { $closedIssues = @($closedObj.data.issues).Count }
+ } catch {} }
+ # Open PRs
+ $openPRs = 0; $openPRItems = @()
$prsRaw = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "open", "--limit", "50"
- $openPRs = 0
- if ($prsRaw) {
- try {
- $prsObj = $prsRaw | ConvertFrom-Json
- if ($prsObj.ok) {
- if ($prsObj.data.issues) { $openPRs = @($prsObj.data.issues).Count }
- elseif ($prsObj.data.pulls) { $openPRs = @($prsObj.data.pulls).Count }
- elseif ($prsObj.data -is [array]) { $openPRs = $prsObj.data.Count }
- }
- } catch {}
- }
+ if ($prsRaw) { try {
+ $prsObj = $prsRaw | ConvertFrom-Json
+ if ($prsObj.ok) {
+ if ($prsObj.data.issues) { $openPRItems = @($prsObj.data.issues) }
+ elseif ($prsObj.data.pulls) { $openPRItems = @($prsObj.data.pulls) }
+ elseif ($prsObj.data -is [array]) { $openPRItems = @($prsObj.data) }
+ $openPRs = $openPRItems.Count
+ }
+ } catch {} }
- $mergedRaw = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "merged", "--limit", "50"
+ # Merged PRs
$mergedPRs = 0
- if ($mergedRaw) {
- try {
- $mergedObj = $mergedRaw | ConvertFrom-Json
- if ($mergedObj.ok) {
- if ($mergedObj.data.issues) { $mergedPRs = @($mergedObj.data.issues).Count }
- elseif ($mergedObj.data.pulls) { $mergedPRs = @($mergedObj.data.pulls).Count }
- elseif ($mergedObj.data -is [array]) { $mergedPRs = $mergedObj.data.Count }
- }
- } catch {}
- }
+ $mergedRaw = Invoke-GL "pr", "+list", "--owner", $Org, "--repo", $repo, "--state", "merged", "--limit", "50"
+ if ($mergedRaw) { try {
+ $mergedObj = $mergedRaw | ConvertFrom-Json
+ if ($mergedObj.ok) {
+ if ($mergedObj.data.issues) { $mergedPRs = @($mergedObj.data.issues).Count }
+ elseif ($mergedObj.data.pulls) { $mergedPRs = @($mergedObj.data.pulls).Count }
+ elseif ($mergedObj.data -is [array]) { $mergedPRs = @($mergedObj.data).Count }
+ }
+ } catch {} }
+ # Latest release + age
+ $latestRelease = "none"; $releaseAge = -1
$releaseRaw = Invoke-GL "release", "+list", "--owner", $Org, "--repo", $repo, "--limit", "1"
- $latestRelease = "none"
- if ($releaseRaw) {
- try {
- $releaseObj = $releaseRaw | ConvertFrom-Json
- if ($releaseObj.ok -and $releaseObj.data.releases) {
- $releases = @($releaseObj.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" }
- }
- }
- } catch {}
+ if ($releaseRaw) { try {
+ $releaseObj = $releaseRaw | ConvertFrom-Json
+ $rels = $null
+ if ($releaseObj.ok -and $releaseObj.data.releases) { $rels = @($releaseObj.data.releases) }
+ elseif ($releaseObj.ok -and $releaseObj.data -is [array]) { $rels = @($releaseObj.data) }
+ if ($rels -and $rels.Count -gt 0) {
+ $latestRelease = if ($rels[0].tag_name) { $rels[0].tag_name } elseif ($rels[0].name) { $rels[0].name } else { "none" }
+ $relDate = if ($rels[0].created_at) { $rels[0].created_at } elseif ($rels[0].created_on) { $rels[0].created_on } elseif ($rels[0].published_at) { $rels[0].published_at } else { $null }
+ if ($relDate) {
+ try { $releaseAge = [int]((Get-Date) - [datetime]$relDate).TotalDays } catch { $releaseAge = 0 }
+ } else { $releaseAge = 0 }
+ }
+ } catch {} }
+
+ $health = Compute-Health $openIssues $closedIssues $openPRs $mergedPRs $releaseAge
+ $grade = Get-HealthGrade $health
+
+ Log-Ok "$repo : Issue(开:$openIssues 闭:$closedIssues) PR(开:$openPRs 合:$mergedPRs) Release:$latestRelease 健康度:$health($($grade.Label))"
+
+ # Open issue detail rows
+ $issueRows = ""
+ $limit = [Math]::Min($openIssues, $DetailLimit)
+ for ($i = 0; $i -lt $limit; $i++) {
+ $it = $openIssueItems[$i]
+ $iid = if ($it.id) { $it.id } elseif ($it.number) { $it.number } else { "" }
+ $ititle = if ($it.name) { $it.name } elseif ($it.subject) { $it.subject } elseif ($it.title) { $it.title } else { "" }
+ $iauthor = if ($it.author_name) { $it.author_name } elseif ($it.author_login) { $it.author_login } elseif ($it.author.login) { $it.author.login } else { "unknown" }
+ $issueRows += "
| #$(Html-Escape ([string]$iid)) | $(Html-Escape ([string]$ititle)) | @$(Html-Escape ([string]$iauthor)) |
"
}
+ if (-not $issueRows) { $issueRows = '| 无未关闭 Issue |
' }
- Log-Ok "$repo : Issues(open:$openIssues closed:$closedIssues) PRs(open:$openPRs merged:$mergedPRs) Release:$latestRelease"
+ # Open PR detail rows
+ $prRows = ""
+ $limit = [Math]::Min($openPRs, $DetailLimit)
+ for ($i = 0; $i -lt $limit; $i++) {
+ $pt = $openPRItems[$i]
+ $pid = if ($pt.pull_request_number) { $pt.pull_request_number } elseif ($pt.number) { $pt.number } elseif ($pt.id) { $pt.id } else { "" }
+ $ptitle = if ($pt.subject) { $pt.subject } elseif ($pt.title) { $pt.title } elseif ($pt.name) { $pt.name } else { "" }
+ $pauthor = if ($pt.author_name) { $pt.author_name } elseif ($pt.author_login) { $pt.author_login } elseif ($pt.author.login) { $pt.author.login } else { "unknown" }
+ $prRows += "| #$(Html-Escape ([string]$pid)) | $(Html-Escape ([string]$ptitle)) | @$(Html-Escape ([string]$pauthor)) |
"
+ }
+ if (-not $prRows) { $prRows = '| 无未合并 PR |
' }
- $statusColor = "green"
- $healthText = "Healthy"
- if ($openIssues -gt 10) { $statusColor = "orange"; $healthText = "Moderate" }
- if ($openIssues -gt 20) { $statusColor = "red"; $healthText = "Needs Attention" }
+ $repoDetails += @"
+$health $(Html-Escape $repo) — Issue 开 $openIssues / PR 开 $openPRs
+
+
未关闭 Issue(最多 $DetailLimit 条)
+
+
未合并 PR(最多 $DetailLimit 条)
+
+
+
+"@
- $dashboardRows += "`n"
- $dashboardRows += " | $repo | `n"
- $dashboardRows += " $openIssues | $closedIssues | `n"
- $dashboardRows += " $openPRs | $mergedPRs | $latestRelease | `n"
- $dashboardRows += " $healthText | `n"
- $dashboardRows += "
`n"
-
- $totalIssues += $openIssues + $closedIssues
+ $totalActivity += $openIssues + $closedIssues + $openPRs + $mergedPRs
$totalOpenIssues += $openIssues
- $totalPRs += $openPRs + $mergedPRs
$totalOpenPRs += $openPRs
+ $totalMergedPRs += $mergedPRs
+ $healthSum += $health
+
+ $relDisplay = if ($latestRelease -eq "none") { "—" } else { Html-Escape $latestRelease }
+
+ $dashboardRows += @"
+
+ | $(Html-Escape $repo) |
+ $openIssues | $closedIssues |
+ $openPRs | $mergedPRs | $relDisplay |
+ $health $($grade.Label) |
+
+"@
}
-# -- Step 4: Generate HTML Dashboard --
-Log-Title "Generating Dashboard"
+$avgHealth = if ($repoList.Count -gt 0) { [int]($healthSum / $repoList.Count) } else { 0 }
+
+# -- Step 5: Generate HTML Dashboard --
+Log-Title "生成 HTML 看板"
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$repoCount = $repoList.Count
-$html = '
-
+$html = @"
+
+
-Multi-Repo Collaboration Dashboard
+多仓库协同看板 - $Org
-
Multi-Repo Collaboration Dashboard
-
Generated: ' + $timestamp + ' | Organization: ' + $Org + '
+
多仓库协同看板
+
生成时间:$timestamp | 组织:$Org
-
Total Repos
' + $repoCount + '
-
Open Issues
' + $totalOpenIssues + '
-
Open PRs
' + $totalOpenPRs + '
-
Total Activity
' + $totalIssues + '
+
+
未关闭 Issue
$totalOpenIssues
+
+
+
+
+
仓库概览
-| Repository | Open Issues | Closed Issues | Open PRs | Merged PRs | Latest Release | Health |
+| 仓库 | 未关闭 Issue | 已关闭 Issue | 未合并 PR | 已合并 PR | 最新 Release | 健康度 |
-' + $dashboardRows + '
+$dashboardRows
+
+
仓库详情
+$repoDetails
-'
+
+"@
-$html | Out-File -FilePath $Output -Encoding UTF8
-Log-Ok "Dashboard saved to: $Output"
+$utf8 = [System.Text.UTF8Encoding]::new($false)
+[System.IO.File]::WriteAllText((Join-Path (Get-Location) $Output), $html, $utf8)
+Log-Ok "看板已保存: $Output"
-# -- Step 5: Coordinated Release --
+# -- Step 6: Coordinated Release --
if ($Release) {
- Log-Title "Coordinated Release: $Release"
+ Log-Title "协调发版: $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)"
+ if ($DryRun) {
+ Log-Warn "[DRY RUN] 将为以下 $($repoList.Count) 个仓库创建 Release ${Release}:"
+ foreach ($repo in $repoList) { Log-Warn " [DRY RUN] $Org/$repo -> $Release" }
+ } else {
+ $relOk = 0; $relFail = 0
+ foreach ($repo in $repoList) {
+ Log-Step "为 $Org/$repo 创建 Release..."
+ $relBody = "Coordinated release $Release for $Org/$repo"
+ $relResult = Invoke-GL "release", "+create", "--owner", $Org, "--repo", $repo, "--tag", $Release, "--name", "Release $Release", "--body", $relBody
+ $ok = $false
+ if ($relResult) { try { $ok = ($relResult | ConvertFrom-Json).ok } catch { $ok = $false } }
+ if ($ok) { Log-Ok "Release $Release 已在 $repo 创建"; $relOk++ }
+ else { Log-Warn "Release 创建失败: $repo (tag 可能已存在)"; $relFail++ }
}
+ Log-Info "协调发版结果: 成功 $relOk / 失败 $relFail"
}
}
# ----------------------------------------------------------------
-Log-Title "Multi-Repo Dashboard Complete"
+Log-Title "多仓库看板完成"
# ----------------------------------------------------------------
-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 " 处理仓库: $repoCount" -ForegroundColor Green
+Write-Host " 未关闭 Issue: $totalOpenIssues" -ForegroundColor Green
+Write-Host " 未合并 PR: $totalOpenPRs" -ForegroundColor Green
+Write-Host " 已合并 PR: $totalMergedPRs" -ForegroundColor Green
+Write-Host " 平均健康度: $avgHealth / 100" -ForegroundColor Green
+Write-Host " 看板文件: $Output" -ForegroundColor Green
+if ($Release) {
+ $suffix = if ($DryRun) { " (dry-run)" } else { "" }
+ Write-Host " 协调发版: $Release$suffix" -ForegroundColor Green
+}
Write-Host ""
-Write-Host "Open dashboard:" -ForegroundColor Cyan
+Write-Host "打开看板:" -ForegroundColor Cyan
Write-Host " Start-Process $Output"
diff --git a/workflows/04-multi-repo-collab.sh b/workflows/04-multi-repo-collab.sh
index 2f4715a8..0585d8f4 100644
--- a/workflows/04-multi-repo-collab.sh
+++ b/workflows/04-multi-repo-collab.sh
@@ -3,27 +3,27 @@
# 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
+# Commands chained:
+# 1. repo +list -- list all repos in org
+# 2. issue +list -- fetch open/closed issues from each repo
+# 3. pr +list -- fetch open/merged PRs from each repo
+# 4. release +list -- check latest release across repos
+# 5. Generate HTML dashboard (summary cards + table + per-repo details)
+# 6. release +create -- (optional) coordinated release across all repos
# ─────────────────────────────────────────────────────────────────────
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 "Usage: $0 --org ORG [--repos REPO1,REPO2,...] [--release TAG] [--output FILE] [--dry-run]"
echo ""
- echo " --org ORG Organization name"
+ echo " --org ORG Organization / user name (required)"
echo " --repos REPO1,REPO2 Comma-separated repo list (default: all repos in org)"
- echo " --release TAG Coordinated release tag to create"
+ echo " --release TAG Coordinated release tag to create across all repos"
echo " --output FILE Output HTML dashboard file (default: dashboard.html)"
- echo " --dry-run Preview actions without executing"
+ echo " --detail-limit N Max open issues/PRs listed per repo in details (default: 10)"
+ echo " --dry-run Preview actions without creating releases"
exit 1
}
@@ -32,16 +32,18 @@ ORG=""
REPOS=""
RELEASE_TAG=""
OUTPUT_FILE="dashboard.html"
+DETAIL_LIMIT=10
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 ;;
+ --org) ORG="$2"; shift 2 ;;
+ --repos) REPOS="$2"; shift 2 ;;
+ --release) RELEASE_TAG="$2"; shift 2 ;;
+ --output) OUTPUT_FILE="$2"; shift 2 ;;
+ --detail-limit) DETAIL_LIMIT="$2"; shift 2 ;;
+ --dry-run) DRY_RUN="true"; shift ;;
+ --help|-h) usage ;;
+ *) log_err "Unknown arg: $1"; usage ;;
esac
done
@@ -52,214 +54,330 @@ fi
check_auth
-# ── Step 1: List Repositories ────────────────────────────────────────
-log_title "Multi-Repo Collaboration Dashboard"
+# ── HTML escape helper ───────────────────────────────────────────────
+html_escape() {
+ local s="$1"
+ s="${s//&/&}"
+ s="${s//<}"
+ s="${s//>/>}"
+ s="${s//\"/"}"
+ echo "$s"
+}
-log_step "Fetching repositories for org: $ORG..."
+# ── Health score (0-100) — aligned with gitlink-health skill ─────────
+# Model: start 100, deduct. Core 75 (issue efficiency / PR efficiency /
+# activity, -25 each), auxiliary 25 (issue backlog -10, PR backlog -10,
+# no recent release -5). Time-based core metrics are approximated by
+# list-level ratios for a fast multi-repo scan.
+# release_age: days since latest release, -1 = no release.
+compute_health() {
+ local open_issues="$1" closed_issues="$2" open_prs="$3" merged_prs="$4" release_age="$5"
+ awk -v oi="$open_issues" -v ci="$closed_issues" -v op="$open_prs" \
+ -v mp="$merged_prs" -v age="$release_age" 'BEGIN {
+ health = 100
+ # ── 核心指标 (75) ──
+ # Issue 处理效率 (proxy for 响应慢): 解决率 < 50% 扣 25
+ ti = oi + ci
+ res = (ti > 0) ? ci / ti : 1
+ if (res < 0.5) health -= 25
+ # PR 合并效率 (proxy for 合并慢): 合并率 < 50% 扣 25
+ tp = op + mp
+ mr = (tp > 0) ? mp / tp : 1
+ if (mr < 0.5) health -= 25
+ # 活跃度低: 无任何已完成工作 (合并 PR + 关闭 Issue == 0) 扣 25
+ if ((mp + ci) == 0) health -= 25
+ # ── 辅助指标 (25) ──
+ if (oi > 20) health -= 10 # Issue 积压
+ if (op > 10) health -= 10 # PR 积压
+ if (age < 0 || age > 30) health -= 5 # 无近期发布 (>30天或无)
+ if (health < 0) health = 0
+ if (health > 100) health = 100
+ printf "%d", health
+ }'
+}
+
+# Map score → (grade, css class) — 5 bands per gitlink-health skill
+health_grade() {
+ local score="$1"
+ if [[ "$score" -ge 90 ]]; then echo "优秀|excellent"
+ elif [[ "$score" -ge 70 ]]; then echo "良好|good"
+ elif [[ "$score" -ge 50 ]]; then echo "一般|moderate"
+ elif [[ "$score" -ge 30 ]]; then echo "需关注|attention"
+ else echo "严重|critical"
+ fi
+}
+
+# ── Step 1: List Repositories ────────────────────────────────────────
+log_title "多仓库协同看板 (Multi-Repo Collaboration Dashboard)"
+
+log_step "获取组织 $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"
+REPOS_DATA_PATH='(.data.projects // .data | if type == "array" then . else [] end)'
+ALL_REPO_COUNT=$(echo "$REPOS_JSON" | jq "$REPOS_DATA_PATH | length")
+log_ok "组织下共有 $ALL_REPO_COUNT 个仓库"
# 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[*]}"
+ log_info "仅处理指定仓库: ${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"
+if [[ "${#REPO_LIST[@]}" -eq 0 ]]; then
+ log_err "没有可处理的仓库。请检查 --org 是否正确,或用 --repos 指定。"
+ exit 1
+fi
+log_ok "将处理 ${#REPO_LIST[@]} 个仓库"
-# ── Step 2-3: Collect Issues and PRs from each repo ──────────────────
-log_title "Collecting Data Across Repos"
+# ── Step 2-4: Collect data across repos ──────────────────────────────
+log_title "跨仓库数据采集"
-# Data arrays for dashboard
DASHBOARD_ROWS=""
-TOTAL_ISSUES=0
-TOTAL_PRS=0
+REPO_DETAILS=""
+TOTAL_ACTIVITY=0
TOTAL_OPEN_ISSUES=0
TOTAL_OPEN_PRS=0
+TOTAL_MERGED_PRS=0
+HEALTH_SUM=0
+
+ISSUE_DATA_PATH='(.data.issues // .data | if type == "array" then . else [] end)'
+PR_DATA_PATH='(.data.issues // .data.pulls // .data | if type == "array" then . else [] end)'
for repo in "${REPO_LIST[@]}"; do
divider
- log_step "Processing $ORG/$repo..."
+ log_step "处理 $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")
+ OPEN_ISSUES=$(echo "$ISSUES_JSON" | jq "$ISSUE_DATA_PATH | length" 2>/dev/null || echo "0")
+ [[ "$OPEN_ISSUES" =~ ^[0-9]+$ ]] || OPEN_ISSUES=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")
+ CLOSED_ISSUES=$(echo "$CLOSED_JSON" | jq "$ISSUE_DATA_PATH | length" 2>/dev/null || echo "0")
+ [[ "$CLOSED_ISSUES" =~ ^[0-9]+$ ]] || CLOSED_ISSUES=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")
+ OPEN_PRS=$(echo "$PRS_JSON" | jq "$PR_DATA_PATH | length" 2>/dev/null || echo "0")
+ [[ "$OPEN_PRS" =~ ^[0-9]+$ ]] || OPEN_PRS=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")
+ MERGED_PRS=$(echo "$MERGED_JSON" | jq "$PR_DATA_PATH | length" 2>/dev/null || echo "0")
+ [[ "$MERGED_PRS" =~ ^[0-9]+$ ]] || MERGED_PRS=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)
+ REL_ARR='(.data.releases // .data | if type == "array" then . else [] end)'
+ LATEST_RELEASE=$(echo "$RELEASES_JSON" | jq -r "$REL_ARR[0].tag_name // $REL_ARR[0].name // \"none\"" 2>/dev/null)
+ [[ -z "$LATEST_RELEASE" || "$LATEST_RELEASE" == "null" ]] && LATEST_RELEASE="none"
- log_ok "$repo: Issues(open:$OPEN_ISSUES closed:$CLOSED_ISSUES) PRs(open:$OPEN_PRS merged:$MERGED_PRS) Release:$LATEST_RELEASE"
+ # Release age in days (-1 if none / unparseable)
+ RELEASE_AGE=-1
+ if [[ "$LATEST_RELEASE" != "none" ]]; then
+ REL_DATE=$(echo "$RELEASES_JSON" | jq -r "$REL_ARR[0].created_at // $REL_ARR[0].created_on // $REL_ARR[0].published_at // empty" 2>/dev/null)
+ REL_DATE="${REL_DATE%% *}"; REL_DATE="${REL_DATE%%T*}"
+ if [[ "$REL_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
+ RELEASE_AGE=$(days_between "$REL_DATE" "$(date_today)")
+ else
+ RELEASE_AGE=0 # has release but no parseable date → treat as recent
+ fi
+ fi
- # Get PR details for open PRs
- PR_DETAILS=""
- PR_DATA_PATH='(.data.issues // .data.pulls // .data | if type == "array" then . else [] end)'
- if [[ "$OPEN_PRS" -gt 0 ]] && [[ "$OPEN_PRS" != "null" ]]; then
- for pi in $(seq 0 $((OPEN_PRS > 5 ? 4 : OPEN_PRS - 1))); do
- PR_ID=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].pull_request_number // $PR_DATA_PATH[$pi].number // $PR_DATA_PATH[$pi].id // empty")
- PR_TITLE=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].subject // $PR_DATA_PATH[$pi].title // empty")
- PR_AUTHOR=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].author_login // $PR_DATA_PATH[$pi].author.login // \"unknown\"")
- PR_DETAILS+="| #$PR_ID | $PR_TITLE | @$PR_AUTHOR | open |
"
+ # Health score (aligned with gitlink-health skill)
+ HEALTH=$(compute_health "$OPEN_ISSUES" "$CLOSED_ISSUES" "$OPEN_PRS" "$MERGED_PRS" "$RELEASE_AGE")
+ IFS='|' read -r GRADE_LABEL GRADE_CLASS <<< "$(health_grade "$HEALTH")"
+
+ log_ok "$repo: Issue(开:$OPEN_ISSUES 闭:$CLOSED_ISSUES) PR(开:$OPEN_PRS 合:$MERGED_PRS) Release:$LATEST_RELEASE 健康度:$HEALTH($GRADE_LABEL)"
+
+ # ── Collect open issue details ──
+ ISSUE_ROWS=""
+ if [[ "$OPEN_ISSUES" -gt 0 ]]; then
+ limit=$(( OPEN_ISSUES < DETAIL_LIMIT ? OPEN_ISSUES : DETAIL_LIMIT ))
+ for ii in $(seq 0 $((limit - 1))); do
+ I_ID=$(echo "$ISSUES_JSON" | jq -r "$ISSUE_DATA_PATH[$ii].id // $ISSUE_DATA_PATH[$ii].number // empty")
+ I_TITLE=$(echo "$ISSUES_JSON" | jq -r "$ISSUE_DATA_PATH[$ii].name // $ISSUE_DATA_PATH[$ii].subject // $ISSUE_DATA_PATH[$ii].title // empty")
+ I_AUTHOR=$(echo "$ISSUES_JSON" | jq -r "$ISSUE_DATA_PATH[$ii].author_name // $ISSUE_DATA_PATH[$ii].author_login // $ISSUE_DATA_PATH[$ii].author.login // \"unknown\"")
+ ISSUE_ROWS+="| #$(html_escape "$I_ID") | $(html_escape "$I_TITLE") | @$(html_escape "$I_AUTHOR") |
"
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))
+ # ── Collect open PR details ──
+ PR_ROWS=""
+ if [[ "$OPEN_PRS" -gt 0 ]]; then
+ limit=$(( OPEN_PRS < DETAIL_LIMIT ? OPEN_PRS : DETAIL_LIMIT ))
+ for pi in $(seq 0 $((limit - 1))); do
+ P_ID=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].pull_request_number // $PR_DATA_PATH[$pi].number // $PR_DATA_PATH[$pi].id // empty")
+ P_TITLE=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].subject // $PR_DATA_PATH[$pi].title // $PR_DATA_PATH[$pi].name // empty")
+ P_AUTHOR=$(echo "$PRS_JSON" | jq -r "$PR_DATA_PATH[$pi].author_name // $PR_DATA_PATH[$pi].author_login // $PR_DATA_PATH[$pi].author.login // \"unknown\"")
+ PR_ROWS+="| #$(html_escape "$P_ID") | $(html_escape "$P_TITLE") | @$(html_escape "$P_AUTHOR") |
"
+ done
+ fi
- # Add to dashboard rows
- STATUS_COLOR="green"
- [[ "$OPEN_ISSUES" -gt 10 ]] && STATUS_COLOR="orange"
- [[ "$OPEN_ISSUES" -gt 20 ]] && STATUS_COLOR="red"
+ # ── Per-repo collapsible detail section ──
+ [[ -z "$ISSUE_ROWS" ]] && ISSUE_ROWS="| 无未关闭 Issue |
"
+ [[ -z "$PR_ROWS" ]] && PR_ROWS="| 无未合并 PR |
"
+ REPO_DETAILS+="$HEALTH $(html_escape "$repo") — Issue 开 $OPEN_ISSUES / PR 开 $OPEN_PRS
+
+
未关闭 Issue(最多 $DETAIL_LIMIT 条)
+
+
未合并 PR(最多 $DETAIL_LIMIT 条)
+
+
+ "
+
+ # Accumulate totals
+ TOTAL_ACTIVITY=$((TOTAL_ACTIVITY + OPEN_ISSUES + CLOSED_ISSUES + OPEN_PRS + MERGED_PRS))
+ TOTAL_OPEN_ISSUES=$((TOTAL_OPEN_ISSUES + OPEN_ISSUES))
+ TOTAL_OPEN_PRS=$((TOTAL_OPEN_PRS + OPEN_PRS))
+ TOTAL_MERGED_PRS=$((TOTAL_MERGED_PRS + MERGED_PRS))
+ HEALTH_SUM=$((HEALTH_SUM + HEALTH))
+
+ REL_DISPLAY="$LATEST_RELEASE"
+ [[ "$REL_DISPLAY" == "none" ]] && REL_DISPLAY="—"
DASHBOARD_ROWS+="
- | $repo |
+ $(html_escape "$repo") |
$OPEN_ISSUES |
$CLOSED_ISSUES |
$OPEN_PRS |
$MERGED_PRS |
- $LATEST_RELEASE |
- $(
- [[ "$OPEN_ISSUES" -le 5 ]] && echo "Healthy" || \
- [[ "$OPEN_ISSUES" -le 15 ]] && echo "Moderate" || echo "Needs Attention"
- ) |
+ $(html_escape "$REL_DISPLAY") |
+ $HEALTH $GRADE_LABEL |
"
done
-# ── Step 4: Generate HTML Dashboard ──────────────────────────────────
-log_title "Generating Dashboard"
+# Average health
+AVG_HEALTH=0
+[[ "${#REPO_LIST[@]}" -gt 0 ]] && AVG_HEALTH=$((HEALTH_SUM / ${#REPO_LIST[@]}))
-log_step "Creating HTML dashboard..."
+# ── Step 5: Generate HTML Dashboard ──────────────────────────────────
+log_title "生成 HTML 看板"
+log_step "写入 $OUTPUT_FILE..."
-cat > "$OUTPUT_FILE" << 'HTMLEOF'
+TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
+
+cat > "$OUTPUT_FILE" << HTMLEOF
-
+
-Multi-Repo Collaboration Dashboard
+多仓库协同看板 - $ORG
-
Multi-Repo Collaboration Dashboard
-
Generated: TIMESTAMP_PLACEHOLDER | Organization: ORG_PLACEHOLDER
+
多仓库协同看板
+
生成时间:$TIMESTAMP | 组织:$ORG
-
-
Open Issues
OPEN_ISSUES_COUNT
-
-
Total Activity
TOTAL_ACTIVITY
+
+
未关闭 Issue
$TOTAL_OPEN_ISSUES
+
+
+
+
+
仓库概览
-| Repository | Open Issues | Closed Issues | Open PRs | Merged PRs | Latest Release | Health |
-DASHBOARD_ROWS_PLACEHOLDER
+| 仓库 | 未关闭 Issue | 已关闭 Issue | 未合并 PR | 已合并 PR | 最新 Release | 健康度 |
+$DASHBOARD_ROWS
+
+
仓库详情
+$REPO_DETAILS
HTMLEOF
-# Replace placeholders using temp file approach for complex content
-TEMP_HTML=$(mktemp)
-TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
+log_ok "看板已保存: $OUTPUT_FILE"
-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 ──────────────────────────────────────
+# ── Step 6: Coordinated Release ──────────────────────────────────────
if [[ -n "$RELEASE_TAG" ]]; then
- log_title "Coordinated Release: $RELEASE_TAG"
+ log_title "协调发版: $RELEASE_TAG"
- RELEASE_BODY="# Coordinated Release: $RELEASE_TAG
+ if [[ "$DRY_RUN" == "true" ]]; then
+ log_warn "[DRY RUN] 将为以下 ${#REPO_LIST[@]} 个仓库创建 Release $RELEASE_TAG:"
+ for repo in "${REPO_LIST[@]}"; do
+ log_warn " [DRY RUN] $ORG/$repo → $RELEASE_TAG"
+ done
+ else
+ RELEASE_OK=0
+ RELEASE_FAIL=0
+ for repo in "${REPO_LIST[@]}"; do
+ log_step "为 $ORG/$repo 创建 Release..."
+ 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
-## 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*"
+ if [[ "$(json_ok "$RELEASE_RESULT")" == "true" ]]; then
+ log_ok "Release $RELEASE_TAG 已在 $repo 创建"
+ RELEASE_OK=$((RELEASE_OK + 1))
+ else
+ log_warn "Release 创建失败: $repo (tag 可能已存在)"
+ RELEASE_FAIL=$((RELEASE_FAIL + 1))
+ fi
+ done
+ log_info "协调发版结果: 成功 $RELEASE_OK / 失败 $RELEASE_FAIL"
+ fi
fi
# ─────────────────────────────────────────────────────────────────────
-log_title "Multi-Repo Dashboard Complete"
+log_title "多仓库看板完成"
# ─────────────────────────────────────────────────────────────────────
-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 -e "${GREEN}汇总:${NC}"
+echo " 处理仓库: ${#REPO_LIST[@]}"
+echo " 未关闭 Issue: $TOTAL_OPEN_ISSUES"
+echo " 未合并 PR: $TOTAL_OPEN_PRS"
+echo " 已合并 PR: $TOTAL_MERGED_PRS"
+echo " 平均健康度: $AVG_HEALTH / 100"
+echo " 看板文件: $OUTPUT_FILE"
+[[ -n "$RELEASE_TAG" ]] && echo " 协调发版: $RELEASE_TAG$([[ "$DRY_RUN" == "true" ]] && echo " (dry-run)")"
echo ""
-echo -e "${CYAN}Open dashboard:${NC}"
+echo -e "${CYAN}打开看板:${NC}"
+echo " start $OUTPUT_FILE # Windows"
echo " xdg-open $OUTPUT_FILE # Linux"
echo " open $OUTPUT_FILE # macOS"
echo ""