#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 ''