This commit is contained in:
@@ -57,7 +57,7 @@ $Config = @{
|
||||
MaxDiscordEmbeds = 10
|
||||
RetryCount = 3
|
||||
RetryDelaySeconds = 2
|
||||
AuthoringTestsEnabled = $false
|
||||
AuthoringTestsEnabled = $true
|
||||
AuthoringTestsPattern = 'test_authoring_server.py'
|
||||
AuthoringTestsDirectory = Join-Path $PSScriptRoot 'tests'
|
||||
AuthoringTestTimeoutSec = 120
|
||||
@@ -75,6 +75,7 @@ $Config = @{
|
||||
|
||||
# When true, completed successful runs are reported too.
|
||||
NotifySuccessfulBuilds = $true
|
||||
MaxPreviousFailures = 5
|
||||
|
||||
# When true, successful but slow/warned jobs are reported too.
|
||||
# Keep this false if Discord should only receive notifications for failed/cancelled/timed-out jobs.
|
||||
@@ -547,15 +548,35 @@ function Get-Severity {
|
||||
return 'Info'
|
||||
}
|
||||
|
||||
function Get-BuildStatus {
|
||||
param([Parameter(Mandatory)][object]$Build)
|
||||
|
||||
$conclusion = Get-ObjectValue -Object $Build -Name 'conclusion'
|
||||
if ([string]::IsNullOrWhiteSpace([string]$conclusion)) {
|
||||
$conclusion = Get-ObjectValue -Object $Build -Name 'result'
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace([string]$conclusion)) {
|
||||
$conclusion = Get-ObjectValue -Object $Build -Name 'outcome'
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$conclusion)) {
|
||||
return [string]$conclusion
|
||||
}
|
||||
|
||||
$status = Get-ObjectValue -Object $Build -Name 'status'
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$status)) {
|
||||
return [string]$status
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function Analyze-Build {
|
||||
param(
|
||||
[Parameter(Mandatory)][hashtable]$Repository,
|
||||
[Parameter(Mandatory)][object]$Job
|
||||
)
|
||||
|
||||
$conclusion = Get-ObjectValue -Object $Job -Name 'conclusion'
|
||||
$jobStatus = Get-ObjectValue -Object $Job -Name 'status'
|
||||
$status = if ($conclusion) { [string]$conclusion } elseif ($jobStatus) { [string]$jobStatus } else { 'unknown' }
|
||||
$status = Get-BuildStatus -Build $Job
|
||||
$isSuccessfulStatus = Test-IsSuccessfulStatus -Status $status
|
||||
$duration = Get-Duration -Job $Job
|
||||
$isSlow = $duration.TotalMinutes -ge [double]$Config.SlowBuildThresholdMins
|
||||
@@ -728,6 +749,128 @@ function Send-DiscordNotification {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-BuildStatusEmoji {
|
||||
param([Parameter(Mandatory)][string]$Status)
|
||||
|
||||
if (Test-IsSuccessfulStatus -Status $Status) {
|
||||
return '✅'
|
||||
}
|
||||
|
||||
$normalized = $Status.ToLowerInvariant()
|
||||
if ($normalized -in @('failure', 'failed', 'cancelled', 'canceled', 'timed_out')) {
|
||||
return '❌'
|
||||
}
|
||||
|
||||
return 'ℹ️'
|
||||
}
|
||||
|
||||
function Send-BuildStatusNotification {
|
||||
param([Parameter(Mandatory)][object[]]$Analyses)
|
||||
|
||||
$latestByRepository = @($Analyses |
|
||||
Group-Object Repository |
|
||||
ForEach-Object {
|
||||
$_.Group | Sort-Object StartedAt -Descending | Select-Object -First 1
|
||||
} |
|
||||
Sort-Object Repository)
|
||||
|
||||
if ($latestByRepository.Count -eq 0) {
|
||||
Write-Log -Message "No completed builds found in the last $($Config.LookbackMinutes) minute(s)."
|
||||
return
|
||||
}
|
||||
|
||||
$embeds = New-Object System.Collections.Generic.List[object]
|
||||
$failedLatestCount = 0
|
||||
$successfulLatestCount = 0
|
||||
$previousFailureCount = 0
|
||||
|
||||
foreach ($latest in $latestByRepository) {
|
||||
$latestStatus = [string]$latest.Status
|
||||
$emoji = Get-BuildStatusEmoji -Status $latestStatus
|
||||
$isLatestSuccessful = Test-IsSuccessfulStatus -Status $latestStatus
|
||||
if ($isLatestSuccessful) {
|
||||
$successfulLatestCount++
|
||||
}
|
||||
elseif ($latest.Severity -eq 'Critical') {
|
||||
$failedLatestCount++
|
||||
}
|
||||
|
||||
$previousFailures = @($Analyses |
|
||||
Where-Object {
|
||||
$_.Repository -eq $latest.Repository -and
|
||||
$_.RunId -ne $latest.RunId -and
|
||||
$_.Severity -eq 'Critical'
|
||||
} |
|
||||
Sort-Object StartedAt -Descending |
|
||||
Select-Object -First ([int]$Config.MaxPreviousFailures))
|
||||
|
||||
$previousFailureCount += $previousFailures.Count
|
||||
$previousFailureText = if ($previousFailures.Count -gt 0) {
|
||||
($previousFailures | ForEach-Object {
|
||||
$shortSha = if ($_.CommitSha -and $_.CommitSha.Length -ge 8) { $_.CommitSha.Substring(0, 8) } else { $_.CommitSha }
|
||||
$commitPart = if ($shortSha) { " ``$shortSha``" } else { '' }
|
||||
"#{0} {1}{2}" -f $_.RunId, $_.Status, $commitPart
|
||||
}) -join "`n"
|
||||
}
|
||||
else {
|
||||
'None in this lookback window.'
|
||||
}
|
||||
|
||||
if ($previousFailureText.Length -gt 950) {
|
||||
$previousFailureText = $previousFailureText.Substring(0, 947) + '...'
|
||||
}
|
||||
|
||||
$shortLatestSha = if ($latest.CommitSha -and $latest.CommitSha.Length -ge 8) { $latest.CommitSha.Substring(0, 8) } else { $latest.CommitSha }
|
||||
$latestCommit = if ($shortLatestSha) {
|
||||
if ($latest.CommitMessage) { "``$shortLatestSha`` $($latest.CommitMessage)" } else { "``$shortLatestSha``" }
|
||||
}
|
||||
else {
|
||||
'unknown'
|
||||
}
|
||||
|
||||
$color = if ($isLatestSuccessful) { 3066993 } elseif ($latest.Severity -eq 'Critical') { 15158332 } else { 3447003 }
|
||||
$embeds.Add(@{
|
||||
title = "$emoji Latest build: $($latest.Repository) #$($latest.RunId)"
|
||||
url = $latest.BuildUrl
|
||||
color = $color
|
||||
description = if ($isLatestSuccessful) { 'Latest completed build is successful.' } elseif ($latest.Severity -eq 'Critical') { 'Latest completed build failed.' } else { 'Latest completed build has a non-success status.' }
|
||||
fields = @(
|
||||
@{ name = 'Latest status'; value = "$emoji $latestStatus"; inline = $true },
|
||||
@{ name = 'Branch'; value = $latest.Branch; inline = $true },
|
||||
@{ name = 'Duration'; value = $latest.DurationText; inline = $true },
|
||||
@{ name = 'Commit'; value = $latestCommit; inline = $false },
|
||||
@{ name = 'Previous failed builds'; value = $previousFailureText; inline = $false }
|
||||
)
|
||||
footer = @{
|
||||
text = "Run ID $($latest.RunId)"
|
||||
}
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString('o')
|
||||
})
|
||||
}
|
||||
|
||||
$payload = @{
|
||||
username = 'Gitea Build Monitor'
|
||||
content = if ($failedLatestCount -gt 0) {
|
||||
"❌ Latest Gitea build status: $failedLatestCount failed, $successfulLatestCount successful. Previous failures in window: $previousFailureCount."
|
||||
}
|
||||
elseif ($successfulLatestCount -gt 0) {
|
||||
"✅ Latest Gitea build status: $successfulLatestCount successful. Previous failures in window: $previousFailureCount."
|
||||
}
|
||||
else {
|
||||
"ℹ️ Latest Gitea build status reported. Previous failures in window: $previousFailureCount."
|
||||
}
|
||||
embeds = @($embeds)
|
||||
}
|
||||
|
||||
Invoke-DiscordWebhook -Payload $payload
|
||||
if ($DryRun) {
|
||||
Write-Log -Message "Prepared build status notification with $($embeds.Count) embed(s) in dry-run mode."
|
||||
}
|
||||
else {
|
||||
Write-Log -Message "Sent build status notification with $($embeds.Count) embed(s)."
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PythonCommand {
|
||||
$linuxVenvPython = Join-Path $PSScriptRoot '.venv/bin/python'
|
||||
$windowsVenvPython = Join-Path $PSScriptRoot '.venv/Scripts/python.exe'
|
||||
@@ -921,6 +1064,25 @@ function Get-CacheKey {
|
||||
return "$(Get-RepositorySlug -Repository $Repository):$kind`:$jobId`:state:$state"
|
||||
}
|
||||
|
||||
function Get-BuildSummaryCacheKey {
|
||||
param([Parameter(Mandatory)][object[]]$Analyses)
|
||||
|
||||
$parts = @($Analyses |
|
||||
Group-Object Repository |
|
||||
ForEach-Object {
|
||||
$latest = $_.Group | Sort-Object StartedAt -Descending | Select-Object -First 1
|
||||
$previousFailureIds = @($_.Group |
|
||||
Where-Object { $_.RunId -ne $latest.RunId -and $_.Severity -eq 'Critical' } |
|
||||
Sort-Object StartedAt -Descending |
|
||||
Select-Object -First ([int]$Config.MaxPreviousFailures) |
|
||||
ForEach-Object { $_.RunId })
|
||||
'{0}:latest:{1}:{2}:prevfail:{3}' -f $latest.Repository, $latest.RunId, $latest.Status, ($previousFailureIds -join ',')
|
||||
} |
|
||||
Sort-Object)
|
||||
|
||||
return 'build-summary:' + ($parts -join '|')
|
||||
}
|
||||
|
||||
function Start-BuildMonitor {
|
||||
Assert-Config
|
||||
|
||||
@@ -961,17 +1123,9 @@ function Start-BuildMonitor {
|
||||
continue
|
||||
}
|
||||
|
||||
$cacheKey = Get-CacheKey -Repository $repository -Job $job
|
||||
if ($cache.ContainsKey($cacheKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
$analysis = Analyze-Build -Repository $repository -Job $job
|
||||
$analyses.Add($analysis)
|
||||
if (-not $DryRun) {
|
||||
$cache[$cacheKey] = Get-Date
|
||||
}
|
||||
Write-Log -Message ("Analyzed job {0} for {1}: severity={2}, status={3}, duration={4}" -f (Get-ObjectValue -Object $job -Name 'id'), $analysis.Repository, $analysis.Severity, $analysis.Status, $analysis.DurationText)
|
||||
}
|
||||
catch {
|
||||
@@ -981,10 +1135,19 @@ function Start-BuildMonitor {
|
||||
}
|
||||
|
||||
if ($analyses.Count -gt 0) {
|
||||
Send-DiscordNotification -Analyses $analyses.ToArray()
|
||||
$summaryCacheKey = Get-BuildSummaryCacheKey -Analyses $analyses.ToArray()
|
||||
if ($cache.ContainsKey($summaryCacheKey)) {
|
||||
Write-Log -Message 'Build status summary has already been sent.'
|
||||
}
|
||||
else {
|
||||
Send-BuildStatusNotification -Analyses $analyses.ToArray()
|
||||
if (-not $DryRun) {
|
||||
$cache[$summaryCacheKey] = Get-Date
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Log -Message "No new jobs found in the last $($Config.LookbackMinutes) minute(s)."
|
||||
Write-Log -Message "No completed builds found in the last $($Config.LookbackMinutes) minute(s)."
|
||||
}
|
||||
|
||||
if (-not $DryRun) {
|
||||
|
||||
Reference in New Issue
Block a user