Files
local-ci-cd-system/scripts/Get-CIJobSummary.ps1
T
Simone c04a2f25aa feat(sprint5): quality pass, smoke test workflow, Get-CIJobSummary
Task 5.1: Delete runner/Install-Runner.ps1 (superseded by Setup-Host.ps1).
Task 5.2: Replace Unicode/Discord emoji in Watch-DiskSpace.ps1, Watch-RunnerHealth.ps1,
          Test-E2E-Section3.3.ps1 with plain-text prefixes ([WARNING], [ALERT], [WARN]).
Task 5.3: docs/TEST-PLAN-v1.3-to-HEAD.md: ForEach-Object -Parallel replaced with Start-Job;
          -RepositoryUrl replaced with -RepoUrl (3 occurrences); PS7 syntax removed.
Task 5.4: scripts/Get-CIJobSummary.ps1 (new): scans JSONL logs in F:\CI\Logs, prints summary
          table per job, -JobId detail view, -Failed filter, -Last N.
Task 3.5: gitea/workflows/self-test.yml (new): workflow_dispatch, smoke-windows + smoke-linux.
          scripts/Test-Smoke.ps1 (new): invokes Invoke-CIJob.ps1 with marker-file build,
          asserts artifact dir and JSONL success event.
Task 5.6: docs/BEST-PRACTICES.md Section 10 added: SHA256 pinning policy, priority table,
          PS 5.1 implementation pattern, update guidance.
2026-05-12 21:13:15 +02:00

182 lines
6.8 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Displays a summary table of CI job results parsed from JSONL log files.
.DESCRIPTION
Scans F:\CI\Logs\ (or a custom path) for invoke-ci.jsonl files and
prints a compact table with job ID, status, total elapsed time, and the
last error message (if any).
Each row corresponds to one completed (or failed) CI job.
Use -JobId to inspect a single job in detail.
.PARAMETER LogDir
Base directory containing per-job log subdirectories.
Default: F:\CI\Logs
.PARAMETER Last
Show only the N most recent jobs (sorted by job-start timestamp).
Default: 20
.PARAMETER JobId
When specified, shows a detailed phase-by-phase breakdown for that job.
.PARAMETER Failed
When specified, filters to failed jobs only.
.EXAMPLE
# Show last 20 jobs
.\Get-CIJobSummary.ps1
.EXAMPLE
# Show last 5 failed jobs
.\Get-CIJobSummary.ps1 -Last 5 -Failed
.EXAMPLE
# Detailed breakdown for a specific job
.\Get-CIJobSummary.ps1 -JobId 'run-12345'
#>
[CmdletBinding()]
param(
[string] $LogDir = 'F:\CI\Logs',
[ValidateRange(1, 1000)]
[int] $Last = 20,
[string] $JobId = '',
[switch] $Failed
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ── Helpers ────────────────────────────────────────────────────────────────────
function Format-SecToHMS {
param([int] $Seconds)
if ($Seconds -lt 0) { return '?' }
$h = [math]::Floor($Seconds / 3600)
$m = [math]::Floor(($Seconds % 3600) / 60)
$s = $Seconds % 60
if ($h -gt 0) { return '{0}h{1:00}m{2:00}s' -f $h, $m, $s }
return '{0}m{1:00}s' -f $m, $s
}
function Read-JobEvents {
param([string] $JsonlPath)
$events = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($line in (Get-Content $JsonlPath -Encoding UTF8 -ErrorAction SilentlyContinue)) {
$line = $line.Trim()
if (-not $line) { continue }
try {
$events.Add(($line | ConvertFrom-Json))
} catch {
Write-Debug "[Get-CIJobSummary] Skipping malformed JSONL line in $JsonlPath"
}
}
return $events
}
# ── Validate log dir ────────────────────────────────────────────────────────
if (-not (Test-Path $LogDir -PathType Container)) {
Write-Warning "Log directory not found: $LogDir"
exit 1
}
# ── Single-job detail mode ──────────────────────────────────────────────────
if ($JobId -ne '') {
$jsonlPath = Join-Path $LogDir (Join-Path $JobId 'invoke-ci.jsonl')
if (-not (Test-Path $jsonlPath)) {
Write-Warning "No JSONL log found for job '$JobId' at: $jsonlPath"
exit 1
}
$events = Read-JobEvents -JsonlPath $jsonlPath
if ($events.Count -eq 0) {
Write-Warning "JSONL file is empty or unreadable: $jsonlPath"
exit 1
}
Write-Host "`nJob: $JobId"
Write-Host ('=' * 60)
Write-Host ('{0,-35} {1,-10} {2}' -f 'Phase', 'Status', 'Timestamp')
Write-Host ('{0,-35} {1,-10} {2}' -f ('-'*35), ('-'*10), ('-'*24))
foreach ($ev in $events) {
$ts = if ($ev.ts) { $ev.ts } else { '' }
Write-Host ('{0,-35} {1,-10} {2}' -f $ev.phase, $ev.status, $ts)
}
# Print error if present
$failEvent = $events | Where-Object { $_.status -eq 'failure' } | Select-Object -Last 1
if ($failEvent -and $failEvent.data -and $failEvent.data.error) {
Write-Host "`nError: $($failEvent.data.error)"
}
exit 0
}
# ── Scan all job JSONL files ─────────────────────────────────────────────────
$jsonlFiles = Get-ChildItem -Path $LogDir -Recurse -Filter 'invoke-ci.jsonl' -ErrorAction SilentlyContinue |
Sort-Object { $_.LastWriteTime } -Descending
if ($jsonlFiles.Count -eq 0) {
Write-Host "No invoke-ci.jsonl files found under $LogDir"
exit 0
}
$rows = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($file in $jsonlFiles) {
$events = Read-JobEvents -JsonlPath $file.FullName
if ($events.Count -eq 0) { continue }
$jobEvent = $events | Where-Object { $_.phase -eq 'job' -and $_.status -eq 'start' } | Select-Object -First 1
$successEvent= $events | Where-Object { $_.phase -eq 'job' -and $_.status -eq 'success' } | Select-Object -First 1
$failEvent = $events | Where-Object { $_.phase -eq 'job' -and $_.status -eq 'failure' } | Select-Object -First 1
$jId = if ($events[0].jobId) { $events[0].jobId } else { (Split-Path (Split-Path $file.FullName -Parent) -Leaf) }
$started = if ($jobEvent -and $jobEvent.ts) { $jobEvent.ts } else { '' }
if ($successEvent) {
$status = 'success'
$elSec = if ($successEvent.data -and $null -ne $successEvent.data.elapsedSec) { [int]$successEvent.data.elapsedSec } else { -1 }
$errMsg = ''
} elseif ($failEvent) {
$status = 'FAILED'
$elSec = if ($failEvent.data -and $null -ne $failEvent.data.elapsedSec) { [int]$failEvent.data.elapsedSec } else { -1 }
$errMsg = if ($failEvent.data -and $failEvent.data.error) { "$($failEvent.data.error)" } else { '' }
# Truncate error for table display
if ($errMsg.Length -gt 60) { $errMsg = $errMsg.Substring(0, 57) + '...' }
} else {
$status = 'in-progress'
$elSec = -1
$errMsg = ''
}
$rows.Add([PSCustomObject]@{
JobId = $jId
Status = $status
Elapsed = Format-SecToHMS -Seconds $elSec
Started = $started
Error = $errMsg
})
}
# ── Apply filters + limit ────────────────────────────────────────────────────
if ($Failed) {
$rows = @($rows | Where-Object { $_.Status -eq 'FAILED' })
}
$rows = @($rows | Select-Object -First $Last)
if ($rows.Count -eq 0) {
Write-Host "No matching jobs found."
exit 0
}
# ── Print table ──────────────────────────────────────────────────────────────
Write-Host "`nCI Job Summary (last $($rows.Count) jobs):"
Write-Host ('{0,-40} {1,-12} {2,-10} {3,-26} {4}' -f 'JobId', 'Status', 'Elapsed', 'Started', 'Error')
Write-Host ('{0,-40} {1,-12} {2,-10} {3,-26} {4}' -f ('-'*40), ('-'*12), ('-'*10), ('-'*26), ('-'*30))
foreach ($r in $rows) {
Write-Host ('{0,-40} {1,-12} {2,-10} {3,-26} {4}' -f $r.JobId, $r.Status, $r.Elapsed, $r.Started, $r.Error)
}
Write-Host ''