实现多仓库协同自动化工作流(04)
串联 repo/issue/pr/release +list 命令完成端到端任务链: 逐仓库采集数据 → 对齐 gitlink-health 的健康度评分 → 生成中文 HTML 看板 → 协调发版(支持真 --dry-run 预览)
This commit is contained in:
parent
e893efa817
commit
8a2c4d3bd9
|
|
@ -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 += "<tr><td>#$(Html-Escape ([string]$iid))</td><td>$(Html-Escape ([string]$ititle))</td><td>@$(Html-Escape ([string]$iauthor))</td></tr>"
|
||||
}
|
||||
if (-not $issueRows) { $issueRows = '<tr><td colspan="3" class="empty">无未关闭 Issue</td></tr>' }
|
||||
|
||||
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 += "<tr><td>#$(Html-Escape ([string]$pid))</td><td>$(Html-Escape ([string]$ptitle))</td><td>@$(Html-Escape ([string]$pauthor))</td></tr>"
|
||||
}
|
||||
if (-not $prRows) { $prRows = '<tr><td colspan="3" class="empty">无未合并 PR</td></tr>' }
|
||||
|
||||
$statusColor = "green"
|
||||
$healthText = "Healthy"
|
||||
if ($openIssues -gt 10) { $statusColor = "orange"; $healthText = "Moderate" }
|
||||
if ($openIssues -gt 20) { $statusColor = "red"; $healthText = "Needs Attention" }
|
||||
$repoDetails += @"
|
||||
<details><summary><span class="badge $($grade.Class)">$health</span> $(Html-Escape $repo) — Issue 开 $openIssues / PR 开 $openPRs</summary>
|
||||
<div class="detail-grid">
|
||||
<div><h4>未关闭 Issue(最多 $DetailLimit 条)</h4>
|
||||
<table class="detail-table"><thead><tr><th>编号</th><th>标题</th><th>作者</th></tr></thead><tbody>$issueRows</tbody></table></div>
|
||||
<div><h4>未合并 PR(最多 $DetailLimit 条)</h4>
|
||||
<table class="detail-table"><thead><tr><th>编号</th><th>标题</th><th>作者</th></tr></thead><tbody>$prRows</tbody></table></div>
|
||||
</div>
|
||||
</details>
|
||||
"@
|
||||
|
||||
$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
|
||||
$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 += @"
|
||||
<tr>
|
||||
<td><a href="https://gitlink.org.cn/$Org/$repo" target="_blank">$(Html-Escape $repo)</a></td>
|
||||
<td>$openIssues</td><td>$closedIssues</td>
|
||||
<td>$openPRs</td><td>$mergedPRs</td><td>$relDisplay</td>
|
||||
<td><span class="badge $($grade.Class)">$health</span> <span class="grade $($grade.Class)">$($grade.Label)</span></td>
|
||||
</tr>
|
||||
"@
|
||||
}
|
||||
|
||||
# -- 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 = '<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
$html = @"
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Multi-Repo Collaboration Dashboard</title>
|
||||
<title>多仓库协同看板 - $Org</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; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif; background: #f0f2f5; padding: 24px; color: #1f2329; }
|
||||
.container { max-width: 1280px; margin: 0 auto; }
|
||||
h1 { color: #1f2329; margin-bottom: 6px; font-size: 26px; }
|
||||
.timestamp { color: #8a919f; font-size: 13px; margin-bottom: 24px; }
|
||||
.summary { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 28px; }
|
||||
.card { background: #fff; border-radius: 10px; padding: 18px 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.card h3 { color: #8a919f; font-size: 13px; margin-bottom: 8px; font-weight: 500; }
|
||||
.card .value { font-size: 30px; font-weight: 700; }
|
||||
.card.blue .value { color: #2f80ed; }
|
||||
.card.orange .value { color: #f2994a; }
|
||||
.card.purple .value { color: #9b51e0; }
|
||||
.card.green .value { color: #27ae60; }
|
||||
.card.teal .value { color: #17a2b8; }
|
||||
h2 { font-size: 18px; margin: 24px 0 14px; color: #1f2329; }
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
th { background: #2f80ed; color: #fff; padding: 12px 16px; text-align: left; font-weight: 500; font-size: 14px; }
|
||||
td { padding: 11px 16px; border-bottom: 1px solid #eef0f3; font-size: 14px; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: #f7f9fc; }
|
||||
a { color: #2f80ed; text-decoration: none; font-weight: 500; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.timestamp { color: #999; font-size: 14px; margin-bottom: 20px; }
|
||||
.badge { display: inline-block; min-width: 34px; text-align: center; padding: 2px 8px; border-radius: 12px; color: #fff; font-weight: 700; font-size: 13px; }
|
||||
.grade { font-size: 13px; margin-left: 4px; }
|
||||
.badge.excellent { background: #27ae60; } .grade.excellent { color: #27ae60; }
|
||||
.badge.good { background: #2f80ed; } .grade.good { color: #2f80ed; }
|
||||
.badge.moderate { background: #f2c94c; } .grade.moderate { color: #d9a400; }
|
||||
.badge.attention { background: #f2994a; } .grade.attention { color: #f2994a; }
|
||||
.badge.critical { background: #eb5757; } .grade.critical { color: #eb5757; }
|
||||
details { background: #fff; border-radius: 10px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; }
|
||||
summary { padding: 14px 18px; cursor: pointer; font-weight: 600; font-size: 15px; user-select: none; }
|
||||
summary:hover { background: #f7f9fc; }
|
||||
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 0 18px 18px; }
|
||||
.detail-table th { background: #eef2f8; color: #4a5568; padding: 8px 12px; font-size: 13px; }
|
||||
.detail-table td { padding: 8px 12px; font-size: 13px; }
|
||||
.detail-grid h4 { font-size: 14px; margin-bottom: 8px; color: #4a5568; }
|
||||
.empty { color: #b0b7c3; text-align: center; font-style: italic; }
|
||||
@media (max-width: 900px) { .summary { grid-template-columns: repeat(2, 1fr); } .detail-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Multi-Repo Collaboration Dashboard</h1>
|
||||
<p class="timestamp">Generated: ' + $timestamp + ' | Organization: ' + $Org + '</p>
|
||||
<h1>多仓库协同看板</h1>
|
||||
<p class="timestamp">生成时间:$timestamp | 组织:$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 class="card blue"><h3>仓库总数</h3><div class="value">$repoCount</div></div>
|
||||
<div class="card orange"><h3>未关闭 Issue</h3><div class="value">$totalOpenIssues</div></div>
|
||||
<div class="card purple"><h3>未合并 PR</h3><div class="value">$totalOpenPRs</div></div>
|
||||
<div class="card green"><h3>已合并 PR</h3><div class="value">$totalMergedPRs</div></div>
|
||||
<div class="card teal"><h3>平均健康度</h3><div class="value">$avgHealth</div></div>
|
||||
</div>
|
||||
|
||||
<h2>仓库概览</h2>
|
||||
<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>
|
||||
<thead><tr><th>仓库</th><th>未关闭 Issue</th><th>已关闭 Issue</th><th>未合并 PR</th><th>已合并 PR</th><th>最新 Release</th><th>健康度</th></tr></thead>
|
||||
<tbody>
|
||||
' + $dashboardRows + '
|
||||
$dashboardRows
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>仓库详情</h2>
|
||||
$repoDetails
|
||||
</div>
|
||||
</body>
|
||||
</html>'
|
||||
</html>
|
||||
"@
|
||||
|
||||
$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"
|
||||
|
|
|
|||
|
|
@ -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+="<tr><td>#$PR_ID</td><td>$PR_TITLE</td><td>@$PR_AUTHOR</td><td>open</td></tr>"
|
||||
# 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+="<tr><td>#$(html_escape "$I_ID")</td><td>$(html_escape "$I_TITLE")</td><td>@$(html_escape "$I_AUTHOR")</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))
|
||||
# ── 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+="<tr><td>#$(html_escape "$P_ID")</td><td>$(html_escape "$P_TITLE")</td><td>@$(html_escape "$P_AUTHOR")</td></tr>"
|
||||
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="<tr><td colspan=\"3\" class=\"empty\">无未关闭 Issue</td></tr>"
|
||||
[[ -z "$PR_ROWS" ]] && PR_ROWS="<tr><td colspan=\"3\" class=\"empty\">无未合并 PR</td></tr>"
|
||||
REPO_DETAILS+="<details><summary><span class=\"badge $GRADE_CLASS\">$HEALTH</span> $(html_escape "$repo") — Issue 开 $OPEN_ISSUES / PR 开 $OPEN_PRS</summary>
|
||||
<div class=\"detail-grid\">
|
||||
<div><h4>未关闭 Issue(最多 $DETAIL_LIMIT 条)</h4>
|
||||
<table class=\"detail-table\"><thead><tr><th>编号</th><th>标题</th><th>作者</th></tr></thead><tbody>$ISSUE_ROWS</tbody></table></div>
|
||||
<div><h4>未合并 PR(最多 $DETAIL_LIMIT 条)</h4>
|
||||
<table class=\"detail-table\"><thead><tr><th>编号</th><th>标题</th><th>作者</th></tr></thead><tbody>$PR_ROWS</tbody></table></div>
|
||||
</div>
|
||||
</details>"
|
||||
|
||||
# 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+="<tr>
|
||||
<td><a href=\"https://gitlink.org.cn/$ORG/$repo\">$repo</a></td>
|
||||
<td><a href=\"https://gitlink.org.cn/$ORG/$repo\" target=\"_blank\">$(html_escape "$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>
|
||||
<td>$(html_escape "$REL_DISPLAY")</td>
|
||||
<td><span class=\"badge $GRADE_CLASS\">$HEALTH</span> <span class=\"grade $GRADE_CLASS\">$GRADE_LABEL</span></td>
|
||||
</tr>"
|
||||
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
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Multi-Repo Collaboration Dashboard</title>
|
||||
<title>多仓库协同看板 - $ORG</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; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif; background: #f0f2f5; padding: 24px; color: #1f2329; }
|
||||
.container { max-width: 1280px; margin: 0 auto; }
|
||||
h1 { color: #1f2329; margin-bottom: 6px; font-size: 26px; }
|
||||
.timestamp { color: #8a919f; font-size: 13px; margin-bottom: 24px; }
|
||||
.summary { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 28px; }
|
||||
.card { background: #fff; border-radius: 10px; padding: 18px 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.card h3 { color: #8a919f; font-size: 13px; margin-bottom: 8px; font-weight: 500; }
|
||||
.card .value { font-size: 30px; font-weight: 700; }
|
||||
.card.blue .value { color: #2f80ed; }
|
||||
.card.orange .value { color: #f2994a; }
|
||||
.card.purple .value { color: #9b51e0; }
|
||||
.card.green .value { color: #27ae60; }
|
||||
.card.teal .value { color: #17a2b8; }
|
||||
h2 { font-size: 18px; margin: 24px 0 14px; color: #1f2329; }
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
th { background: #2f80ed; color: #fff; padding: 12px 16px; text-align: left; font-weight: 500; font-size: 14px; }
|
||||
td { padding: 11px 16px; border-bottom: 1px solid #eef0f3; font-size: 14px; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: #f7f9fc; }
|
||||
a { color: #2f80ed; text-decoration: none; font-weight: 500; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.timestamp { color: #999; font-size: 14px; margin-bottom: 20px; }
|
||||
.badge { display: inline-block; min-width: 34px; text-align: center; padding: 2px 8px; border-radius: 12px; color: #fff; font-weight: 700; font-size: 13px; }
|
||||
.grade { font-size: 13px; margin-left: 4px; }
|
||||
.excellent, .badge.excellent { background: #27ae60; } .grade.excellent { color: #27ae60; background: none; }
|
||||
.good, .badge.good { background: #2f80ed; } .grade.good { color: #2f80ed; background: none; }
|
||||
.moderate, .badge.moderate { background: #f2c94c; } .grade.moderate { color: #d9a400; background: none; }
|
||||
.attention, .badge.attention { background: #f2994a; } .grade.attention { color: #f2994a; background: none; }
|
||||
.critical, .badge.critical { background: #eb5757; } .grade.critical { color: #eb5757; background: none; }
|
||||
details { background: #fff; border-radius: 10px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; }
|
||||
summary { padding: 14px 18px; cursor: pointer; font-weight: 600; font-size: 15px; user-select: none; }
|
||||
summary:hover { background: #f7f9fc; }
|
||||
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 0 18px 18px; }
|
||||
.detail-table th { background: #eef2f8; color: #4a5568; padding: 8px 12px; font-size: 13px; }
|
||||
.detail-table td { padding: 8px 12px; font-size: 13px; }
|
||||
.detail-table h4, .detail-grid h4 { font-size: 14px; margin-bottom: 8px; color: #4a5568; }
|
||||
.empty { color: #b0b7c3; text-align: center; font-style: italic; }
|
||||
@media (max-width: 900px) { .summary { grid-template-columns: repeat(2, 1fr); } .detail-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Multi-Repo Collaboration Dashboard</h1>
|
||||
<p class="timestamp">Generated: TIMESTAMP_PLACEHOLDER | Organization: ORG_PLACEHOLDER</p>
|
||||
<h1>多仓库协同看板</h1>
|
||||
<p class="timestamp">生成时间:$TIMESTAMP | 组织:$ORG</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 class="card blue"><h3>仓库总数</h3><div class="value">${#REPO_LIST[@]}</div></div>
|
||||
<div class="card orange"><h3>未关闭 Issue</h3><div class="value">$TOTAL_OPEN_ISSUES</div></div>
|
||||
<div class="card purple"><h3>未合并 PR</h3><div class="value">$TOTAL_OPEN_PRS</div></div>
|
||||
<div class="card green"><h3>已合并 PR</h3><div class="value">$TOTAL_MERGED_PRS</div></div>
|
||||
<div class="card teal"><h3>平均健康度</h3><div class="value">$AVG_HEALTH</div></div>
|
||||
</div>
|
||||
|
||||
<h2>仓库概览</h2>
|
||||
<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>
|
||||
<thead><tr><th>仓库</th><th>未关闭 Issue</th><th>已关闭 Issue</th><th>未合并 PR</th><th>已合并 PR</th><th>最新 Release</th><th>健康度</th></tr></thead>
|
||||
<tbody>$DASHBOARD_ROWS</tbody>
|
||||
</table>
|
||||
|
||||
<h2>仓库详情</h2>
|
||||
$REPO_DETAILS
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
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 ""
|
||||
|
|
|
|||
Loading…
Reference in New Issue