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 条)

+ $issueRows
编号标题作者
+

未合并 PR(最多 $DetailLimit 条)

+ $prRows
编号标题作者
+
+
+"@ - $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 + '
+

仓库总数

$repoCount
+

未关闭 Issue

$totalOpenIssues
+

未合并 PR

$totalOpenPRs
+

已合并 PR

$totalMergedPRs
+

平均健康度

$avgHealth
+ +

仓库概览

- + -' + $dashboardRows + ' +$dashboardRows
RepositoryOpen IssuesClosed IssuesOpen PRsMerged PRsLatest ReleaseHealth
仓库未关闭 Issue已关闭 Issue未合并 PR已合并 PR最新 Release健康度
+ +

仓库详情

+$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//\"/"}" + 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_AUTHORopen" + # 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 条)

+ $ISSUE_ROWS
编号标题作者
+

未合并 PR(最多 $DETAIL_LIMIT 条)

+ $PR_ROWS
编号标题作者
+
+
" + + # 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

-

Total Repos

REPOS_COUNT
-

Open Issues

OPEN_ISSUES_COUNT
-

Open PRs

OPEN_PRS_COUNT
-

Total Activity

TOTAL_ACTIVITY
+

仓库总数

${#REPO_LIST[@]}
+

未关闭 Issue

$TOTAL_OPEN_ISSUES
+

未合并 PR

$TOTAL_OPEN_PRS
+

已合并 PR

$TOTAL_MERGED_PRS
+

平均健康度

$AVG_HEALTH
+ +

仓库概览

- -DASHBOARD_ROWS_PLACEHOLDER + +$DASHBOARD_ROWS
RepositoryOpen IssuesClosed IssuesOpen PRsMerged PRsLatest ReleaseHealth
仓库未关闭 Issue已关闭 Issue未合并 PR已合并 PR最新 Release健康度
+ +

仓库详情

+$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 ""