gitlink-cli/workflows/academic/10-research-progress.ps1

282 lines
13 KiB
PowerShell

# GitLink Research - Scenario 5: Progress Tracking & Alerting (PowerShell)
param(
[string]$Owner, [string]$Repo, [string]$Org = "",
[int]$Weeks = 4, [string]$Output = "",
[switch]$NoWiki, [switch]$DryRun
)
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Import-Module "$ScriptDir\..\lib\common.psm1" -Force -WarningAction SilentlyContinue
$ErrorActionPreference = "Continue"
Check-Auth
if ($Org) { $Owner = $Org; $Repo = "__org__" }
else { $resolved = Resolve-OwnerRepo -Owner $Owner -Repo $Repo; $Owner = $resolved.Owner; $Repo = $resolved.Repo }
$Today = Get-Date -Format "yyyy-MM-dd"
$OutputDir = Join-Path $ScriptDir "..\output"
if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }
$OutputFile = if ($Output) { $Output } else { Join-Path $OutputDir "progress-weekly-${Owner}-${Today}.html" }
Log-Title "GitLink Research - Progress Tracking"
# 1. Issues
Log-Step "1/5 Collecting Issue data..."
$openIssues = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "100")
$closedIssues = Invoke-GL @("issue", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "closed", "--limit", "100")
$oiData = if ($openIssues.data.issues) { $openIssues.data.issues } else { $openIssues.data }
$totalOpen = if ($openIssues.ok) { @($oiData).Count } else { 0 }
$ciData = if ($closedIssues.data.issues) { $closedIssues.data.issues } else { $closedIssues.data }
$totalClosed = if ($closedIssues.ok) { @($ciData).Count } else { 0 }
Log-Info " Issues: $totalOpen open / $totalClosed closed"
# 2. PRs
Log-Step "2/5 Collecting PR data..."
$mergedPrs = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "merged", "--limit", "100")
$openPrs = Invoke-GL @("pr", "+list", "--owner", $Owner, "--repo", $Repo, "--state", "open", "--limit", "50")
$mData = if ($mergedPrs.data.issues) { $mergedPrs.data.issues } elseif ($mergedPrs.data.pulls) { $mergedPrs.data.pulls } else { $mergedPrs.data }
$totalMerged = if ($mergedPrs.ok) { @($mData).Count } else { 0 }
$opData = if ($openPrs.data.issues) { $openPrs.data.issues } elseif ($openPrs.data.pulls) { $openPrs.data.pulls } else { $openPrs.data }
$totalOpenPrs = if ($openPrs.ok) { @($opData).Count } else { 0 }
Log-Info " PRs: $totalOpenPrs open / $totalMerged merged"
# 3. Releases
Log-Step "3/5 Collecting Release data..."
$releases = Invoke-GL @("release", "+list", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
$releaseCount = if ($releases.ok) { @($releases.data).Count } else { 0 }
$lastReleaseDate = ""
if ($releases.ok -and $releaseCount -gt 0) {
$relData = if ($releases.data.releases) { $releases.data.releases } else { $releases.data }
if ($relData -is [array] -and $relData.Count -gt 0) {
$lastReleaseDate = if ($relData[0].created_at) { $relData[0].created_at } else { "" }
if ($lastReleaseDate.Length -ge 10) { $lastReleaseDate = $lastReleaseDate.Substring(0, 10) }
}
}
Log-Info " Releases: $releaseCount"
# 4. CI
Log-Step "4/5 Collecting CI data..."
$ci = Invoke-GL @("ci", "+builds", "--owner", $Owner, "--repo", $Repo, "--limit", "20")
$ciTotal = if ($ci.ok) { @($ci.data).Count } else { 0 }
$ciOk = 0
if ($ci.ok) {
foreach ($b in @($ci.data)) {
$status = if ($b.status) { $b.status } else { "" }
if ($status -eq "success" -or $status -eq "completed") { $ciOk++ }
}
}
Log-Info " CI: $ciOk/$ciTotal passed"
# 5. Health score
Log-Step "5/5 Calculating health score and anomaly detection..."
$prevClosed = [Math]::Floor($totalClosed / 2)
# Issue velocity (normalized)
$iv = [Math]::Min($totalClosed / ($Weeks * 7.0), 1.0)
# PR merge rate
$prMR = if (($totalMerged + $totalOpenPrs) -gt 0) { $totalMerged / ($totalMerged + $totalOpenPrs) } else { 0 }
# Release cadence
$rc = if ($releaseCount -ge 3) { 1.0 } elseif ($releaseCount -ge 1) { 0.5 } else { 0.2 }
# CI pass rate
$ciScore = if ($ciTotal -gt 0) { $ciOk / $ciTotal } else { 0 }
# Activity trend
$diff = if ($prevClosed -gt 0) { ($totalClosed - $prevClosed) / $prevClosed } else { 0 }
$trend = [Math]::Min([Math]::Max($diff + 0.5, 0.0), 1.0)
$health = [Math]::Round(($iv * 0.30 + $prMR * 0.25 + $rc * 0.25 + $ciScore * 0.10 + $trend * 0.10) * 100, 1)
# Anomalies
$anomalies = @()
if ($totalOpen -gt 20) { $anomalies += @{type="stalled_issue"; severity="Warning"; detail="Open issues ($totalOpen) high, may be stalled"} }
if ($totalOpenPrs -gt 5) { $anomalies += @{type="pr_bottleneck"; severity="Warning"; detail="$totalOpenPrs open PRs backlogged"} }
if ($releaseCount -eq 0) { $anomalies += @{type="no_release"; severity="Info"; detail="No release record"} }
if ($prevClosed -gt 0 -and $totalClosed -lt $prevClosed * 0.5) { $anomalies += @{type="activity_decline"; severity="Warning"; detail="Activity dropped >50%"} }
if ($ciTotal -gt 0 -and $ciOk / $ciTotal -lt 0.5) { $anomalies += @{type="ci_failure"; severity="Warning"; detail="CI pass rate <50%"} }
$anomalyCount = $anomalies.Count
$healthLabel = if ($health -ge 80) { "Healthy" } elseif ($health -ge 60) { "Normal" } elseif ($health -ge 40) { "Needs Attention" } else { "At Risk" }
$healthColor = if ($health -ge 80) { "#2e7d32" } elseif ($health -ge 60) { "#558b2f" } elseif ($health -ge 40) { "#f57c00" } else { "#c62828" }
Log-Ok "Health score: $health/100 ($healthLabel)"
if ($anomalyCount -gt 0) { Log-Warn "Detected $anomalyCount anomalies" }
# ===== Generate HTML Report =====
Log-Step "Generating weekly HTML report..."
# Build anomaly table rows
$anomalyRows = ""
if ($anomalyCount -gt 0) {
foreach ($a in $anomalies) {
$sevClass = if ($a.severity -eq "Critical") { "sev-Critical" } elseif ($a.severity -eq "Warning") { "sev-Warning" } else { "sev-Info" }
$anomalyRows += "<tr><td>$($a.type)</td><td class=`"$sevClass`">$($a.severity)</td><td>$($a.detail)</td></tr>`n"
}
}
$issueTrend = if ($totalClosed -gt $totalOpen) { "<span class=`"ok`">Improving</span>" } else { "<span class=`"warn`">Backlog growing</span>" }
$issueAdvice = if ($totalOpen -gt $totalClosed) { "Schedule an Issue cleanup day" } else { "-" }
$prTrend = if ($totalOpenPrs -le 5) { "<span class=`"ok`">Normal</span>" } else { "<span class=`"warn`">Backlogged</span>" }
$prAdvice = if ($totalOpenPrs -gt 5) { "Increase code review frequency" } else { "-" }
$releaseTrend = if ($releaseCount -ge 3) { "<span class=`"ok`">Active</span>" } else { "<span class=`"warn`">Inactive</span>" }
$releaseAdvice = if ($releaseCount -eq 0) { "Recommend publishing v0.1.0" } else { "-" }
$ciPassRate = if ($ciTotal -gt 0) { [Math]::Round($ciOk / $ciTotal * 100) } else { 0 }
$ciTrend = if ($ciTotal -gt 0 -and $ciPassRate -ge 80) { "<span class=`"ok`">Stable</span>" } else { "<span class=`"warn`">Needs improvement</span>" }
$ciAdvice = if ($ciTotal -eq 0) { "Configure GitLink CI" } else { "-" }
$lastRelDisplay = if ($lastReleaseDate) { $lastReleaseDate } else { "None" }
$lastRelTrend = if ($lastReleaseDate) { "<span class=`"ok`">Released</span>" } else { "<span class=`"warn`">No record</span>" }
$htmlContent = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$Owner/$Repo &mdash;Progress Weekly Report $Today</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
.header { background: linear-gradient(135deg, #1a237e 0%, #3949ab 100%); color: #fff; padding: 40px 30px; }
.header h1 { font-size: 26px; margin-bottom: 6px; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin-bottom: 24px; }
.card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,.08); text-align: center; }
.card.health { background: $healthColor; color: #fff; }
.card .value { font-size: 32px; font-weight: 700; }
.card .label { font-size: 12px; opacity: 0.8; margin-top: 4px; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px; }
.panel { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
.panel h2 { font-size: 18px; margin-bottom: 16px; color: #1a237e; border-bottom: 2px solid #3949ab; padding-bottom: 8px; }
.chart { width: 100%; height: 350px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; font-size: 13px; }
th { background: #f5f7fa; color: #555; font-weight: 600; }
.sev-Critical { color: #c62828; font-weight: 700; }
.sev-Warning { color: #e65100; font-weight: 600; }
.sev-Info { color: #1565c0; }
.ok { color: #2e7d32; font-weight: 600; }
.warn { color: #e65100; font-weight: 600; }
.footer { text-align: center; padding: 24px; color: #999; font-size: 12px; }
@media (max-width: 768px) { .row { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="header">
<h1>$Owner/$Repo &mdash;Research Progress Weekly</h1>
<div style="opacity:0.8;font-size:14px;">$Weeks week(s) review &mdash; $Today</div>
</div>
<div class="container">
<div class="cards">
<div class="card health">
<div class="value">$health</div>
<div class="label">Health Score / 100 &mdash;$healthLabel</div>
</div>
<div class="card"><div class="value">$totalOpen</div><div class="label">Open Issues</div></div>
<div class="card"><div class="value">$totalOpenPrs</div><div class="label">Open PRs</div></div>
<div class="card"><div class="value">$totalMerged</div><div class="label">Merged PRs</div></div>
<div class="card"><div class="value">$releaseCount</div><div class="label">Releases</div></div>
<div class="card"><div class="value">$anomalyCount</div><div class="label">Anomalies</div></div>
</div>
<div class="row">
<div class="panel">
<h2>Issue / PR Overview</h2>
<div id="overviewChart" class="chart"></div>
</div>
<div class="panel">
<h2>Anomaly Alerts</h2>
"@
if ($anomalyCount -eq 0) {
$htmlContent += "<div style=`"text-align:center;padding:40px;color:#2e7d32;`"><b>No anomalies detected, project is healthy</b></div>`n"
} else {
$htmlContent += "<table><tr><th>Type</th><th>Severity</th><th>Detail</th></tr>$anomalyRows</table>`n"
}
$htmlContent += @"
</div>
</div>
<div class="panel">
<h2>Progress Indicators</h2>
<table>
<tr><th>Indicator</th><th>Value</th><th>Trend</th><th>Suggestion</th></tr>
<tr>
<td>Issue Velocity</td>
<td>$totalOpen open / $totalClosed closed</td>
<td>$issueTrend</td>
<td>$issueAdvice</td>
</tr>
<tr>
<td>PR Merge Rate</td>
<td>$([Math]::Round($prMR * 100))%</td>
<td>$prTrend</td>
<td>$prAdvice</td>
</tr>
<tr>
<td>Release Cadence</td>
<td>$releaseCount release(s)</td>
<td>$releaseTrend</td>
<td>$releaseAdvice</td>
</tr>
<tr>
<td>CI Stability</td>
<td>${ciPassRate}% ($ciOk/$ciTotal)</td>
<td>$ciTrend</td>
<td>$ciAdvice</td>
</tr>
<tr>
<td>Latest Release</td>
<td>$lastRelDisplay</td>
<td>$lastRelTrend</td>
<td>-</td>
</tr>
</table>
</div>
</div>
<div class="footer">Generated by GitLink Research Assistant &mdash;$Today</div>
<script>
var overviewChart = echarts.init(document.getElementById('overviewChart'));
overviewChart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['Open', 'Completed'] },
xAxis: { type: 'category', data: ['Issues', 'Pull Requests', 'Releases', 'CI Builds'] },
yAxis: { type: 'value' },
series: [
{ name: 'Open', type: 'bar', data: [$totalOpen, $totalOpenPrs, 0, 0], itemStyle: { color: '#fac858' } },
{ name: 'Completed', type: 'bar', data: [$totalClosed, $totalMerged, $releaseCount, $ciTotal], itemStyle: { color: '#91cc75' } }
]
});
</script>
</body>
</html>
"@
if (-not $DryRun) {
$htmlContent | Out-File -FilePath $OutputFile -Encoding UTF8
Log-Ok "Weekly report generated: $OutputFile"
} else {
Log-Warn "[DRY RUN] Would generate: $OutputFile"
}
# Console summary
Divider
Write-Host "====== Weekly Progress Summary ======" -ForegroundColor White
Write-Host " Repo: $Owner/$Repo"
Write-Host " Health: $health/100 ($healthLabel)"
Write-Host " Issues: $totalOpen open / $totalClosed closed"
Write-Host " PRs: $totalOpenPrs open / $totalMerged merged"
Write-Host " Releases: $releaseCount | CI: $ciOk/$ciTotal passed"
if ($anomalyCount -gt 0) {
Write-Host " Anomalies ($anomalyCount):" -ForegroundColor Yellow
foreach ($a in $anomalies) { Write-Host " [$($a.severity)] $($a.detail)" }
} else { Write-Host " No anomalies detected" -ForegroundColor Green }
Write-Host " Report: $OutputFile"
Divider
Log-Ok "Analysis complete"