This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Monitor recent Gitea Actions build jobs and notify Discord about failures, warnings, errors, or slow builds.
|
||||
Monitor recent Gitea Actions build runs and notify Discord about build status.
|
||||
|
||||
.DESCRIPTION
|
||||
This script is intended to run after the main build pipeline. It queries a self-hosted Gitea instance,
|
||||
analyzes recent Actions jobs for one or more repositories, parses logs where available, caches processed
|
||||
job IDs locally, and sends grouped Discord embed notifications.
|
||||
analyzes recent Actions runs for one or more repositories, parses logs where available, caches processed
|
||||
run IDs locally, and sends grouped Discord embed notifications.
|
||||
|
||||
Required token permissions depend on your Gitea version and repository visibility. For private repos,
|
||||
use a personal access token that can read repository Actions/jobs and logs.
|
||||
@@ -36,9 +36,11 @@ param(
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$GiteaApiToken = if (-not [string]::IsNullOrWhiteSpace($env:API_KEY)) { $env:API_KEY } else { $env:GITEA_API_TOKEN }
|
||||
|
||||
$Config = @{
|
||||
GiteaBaseUrl = 'http://127.0.0.1:3000'
|
||||
ApiToken = $env:API_KEY
|
||||
ApiToken = $GiteaApiToken
|
||||
DiscordWebhookUrl = $env:DISCORD_WEBHOOK_URL
|
||||
|
||||
# Supports multiple repositories.
|
||||
@@ -55,7 +57,7 @@ $Config = @{
|
||||
MaxDiscordEmbeds = 10
|
||||
RetryCount = 3
|
||||
RetryDelaySeconds = 2
|
||||
AuthoringTestsEnabled = $true
|
||||
AuthoringTestsEnabled = $false
|
||||
AuthoringTestsPattern = 'test_authoring_server.py'
|
||||
AuthoringTestsDirectory = Join-Path $PSScriptRoot 'tests'
|
||||
AuthoringTestTimeoutSec = 120
|
||||
@@ -71,6 +73,9 @@ $Config = @{
|
||||
'(?im)\bdeprecated\b'
|
||||
)
|
||||
|
||||
# When true, completed successful runs are reported too.
|
||||
NotifySuccessfulBuilds = $true
|
||||
|
||||
# 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.
|
||||
NotifyWarningsAndSlow = $false
|
||||
@@ -244,11 +249,31 @@ function Get-BuildRuns {
|
||||
|
||||
$owner = [uri]::EscapeDataString((Get-RepositoryOwner -Repository $Repository))
|
||||
$repo = [uri]::EscapeDataString((Get-RepositoryName -Repository $Repository))
|
||||
$path = "/api/v1/repos/$owner/$repo/actions/jobs?page=1&limit=$($Config.MaxJobsPerRepository)"
|
||||
$response = Invoke-GiteaApi -Path $path
|
||||
$limit = [int]$Config.MaxJobsPerRepository
|
||||
|
||||
$jobs = Get-ObjectValue -Object $response -Name 'jobs'
|
||||
try {
|
||||
$runsResponse = Invoke-GiteaApi -Path "/api/v1/repos/$owner/$repo/actions/runs?page=1&limit=$limit"
|
||||
$runs = if ($runsResponse -is [array]) { $runsResponse } else { Get-ObjectValue -Object $runsResponse -Name 'workflow_runs' }
|
||||
if (-not $runs) {
|
||||
$runs = Get-ObjectValue -Object $runsResponse -Name 'runs'
|
||||
}
|
||||
if ($runs) {
|
||||
foreach ($run in @($runs)) {
|
||||
Add-Member -InputObject $run -NotePropertyName '_monitor_kind' -NotePropertyValue 'run' -Force
|
||||
}
|
||||
return @($runs)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level 'WARN' -Message "Could not fetch workflow runs for $(Get-RepositorySlug -Repository $Repository); falling back to jobs endpoint: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$jobsResponse = Invoke-GiteaApi -Path "/api/v1/repos/$owner/$repo/actions/jobs?page=1&limit=$limit"
|
||||
$jobs = if ($jobsResponse -is [array]) { $jobsResponse } else { Get-ObjectValue -Object $jobsResponse -Name 'jobs' }
|
||||
if ($jobs) {
|
||||
foreach ($job in @($jobs)) {
|
||||
Add-Member -InputObject $job -NotePropertyName '_monitor_kind' -NotePropertyValue 'job' -Force
|
||||
}
|
||||
return @($jobs)
|
||||
}
|
||||
|
||||
@@ -311,6 +336,55 @@ function Get-JobLogText {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-BuildLogText {
|
||||
param(
|
||||
[Parameter(Mandatory)][hashtable]$Repository,
|
||||
[Parameter(Mandatory)][object]$Build
|
||||
)
|
||||
|
||||
$kind = [string](Get-ObjectValue -Object $Build -Name '_monitor_kind' -Default 'job')
|
||||
if ($kind -ne 'run') {
|
||||
return Get-JobLogText -Repository $Repository -Job $Build
|
||||
}
|
||||
|
||||
try {
|
||||
$owner = [uri]::EscapeDataString((Get-RepositoryOwner -Repository $Repository))
|
||||
$repo = [uri]::EscapeDataString((Get-RepositoryName -Repository $Repository))
|
||||
$runId = [uri]::EscapeDataString([string](Get-ObjectValue -Object $Build -Name 'id'))
|
||||
$response = Invoke-GiteaApi -Path "/api/v1/repos/$owner/$repo/actions/runs/$runId/jobs"
|
||||
$jobs = Get-ObjectValue -Object $response -Name 'jobs'
|
||||
if (-not $jobs) {
|
||||
return ''
|
||||
}
|
||||
|
||||
$logParts = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($job in @($jobs)) {
|
||||
$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' }
|
||||
if ($status.ToLowerInvariant() -notin @('failure', 'failed', 'cancelled', 'canceled', 'timed_out')) {
|
||||
continue
|
||||
}
|
||||
|
||||
$jobName = [string](Get-ObjectValue -Object $job -Name 'name' -Default (Get-ObjectValue -Object $job -Name 'id'))
|
||||
$jobLog = Get-JobLogText -Repository $Repository -Job $job
|
||||
if (-not [string]::IsNullOrWhiteSpace($jobLog)) {
|
||||
$logParts.Add(("Job {0}:`n{1}" -f $jobName, $jobLog))
|
||||
}
|
||||
}
|
||||
|
||||
$text = ($logParts -join "`n`n")
|
||||
if ($text.Length -gt [int]$Config.MaxLogBytesToAnalyze) {
|
||||
return $text.Substring([Math]::Max(0, $text.Length - [int]$Config.MaxLogBytesToAnalyze))
|
||||
}
|
||||
return $text
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level 'WARN' -Message "Could not fetch logs for run $(Get-ObjectValue -Object $Build -Name 'id'): $($_.Exception.Message)"
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Duration {
|
||||
param([Parameter(Mandatory)][object]$Job)
|
||||
|
||||
@@ -383,6 +457,12 @@ function Test-IsSuccessfulStatus {
|
||||
return $Status.ToLowerInvariant() -in @('success', 'succeeded', 'skipped', 'neutral')
|
||||
}
|
||||
|
||||
function Test-IsCompletedStatus {
|
||||
param([Parameter(Mandatory)][string]$Status)
|
||||
|
||||
return $Status.ToLowerInvariant() -in @('success', 'succeeded', 'skipped', 'neutral', 'failure', 'failed', 'cancelled', 'canceled', 'timed_out', 'completed')
|
||||
}
|
||||
|
||||
function Get-LogSummary {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$LogText,
|
||||
@@ -455,7 +535,7 @@ function Get-Severity {
|
||||
)
|
||||
|
||||
$normalized = $Status.ToLowerInvariant()
|
||||
if ($normalized -in @('failure', 'failed', 'cancelled', 'timed_out')) {
|
||||
if ($normalized -in @('failure', 'failed', 'cancelled', 'canceled', 'timed_out')) {
|
||||
return 'Critical'
|
||||
}
|
||||
if ((-not (Test-IsSuccessfulStatus -Status $Status)) -and $HasErrors) {
|
||||
@@ -481,9 +561,9 @@ function Analyze-Build {
|
||||
$isSlow = $duration.TotalMinutes -ge [double]$Config.SlowBuildThresholdMins
|
||||
$logText = ''
|
||||
|
||||
$definitelyProblematicStatus = $status.ToLowerInvariant() -in @('failure', 'failed', 'cancelled', 'timed_out')
|
||||
$definitelyProblematicStatus = $status.ToLowerInvariant() -in @('failure', 'failed', 'cancelled', 'canceled', 'timed_out')
|
||||
if ($definitelyProblematicStatus -or $Config.NotifyWarningsAndSlow) {
|
||||
$logText = Get-JobLogText -Repository $Repository -Job $Job
|
||||
$logText = Get-BuildLogText -Repository $Repository -Build $Job
|
||||
}
|
||||
|
||||
$logSummary = Get-LogSummary -LogText $logText -IgnoreErrors:$isSuccessfulStatus
|
||||
@@ -493,6 +573,9 @@ function Analyze-Build {
|
||||
if (-not $Config.NotifyWarningsAndSlow -and $severity -eq 'Warning') {
|
||||
$shouldAlert = $false
|
||||
}
|
||||
if (-not $shouldAlert -and $Config.NotifySuccessfulBuilds -and (Test-IsCompletedStatus -Status $status)) {
|
||||
$shouldAlert = $true
|
||||
}
|
||||
|
||||
$sha = [string](Get-ObjectValue -Object $Job -Name 'head_sha' -Default '')
|
||||
$commitMessage = Get-CommitMessage -Repository $Repository -Sha $sha
|
||||
@@ -530,16 +613,27 @@ function Analyze-Build {
|
||||
function New-DiscordEmbed {
|
||||
param([Parameter(Mandatory)][object]$Analysis)
|
||||
|
||||
$emoji = switch ($Analysis.Severity) {
|
||||
'Critical' { '❌' }
|
||||
'Warning' { '⚠️' }
|
||||
default { 'ℹ️' }
|
||||
$isSuccess = Test-IsSuccessfulStatus -Status ([string]$Analysis.Status)
|
||||
$emoji = if ($isSuccess) {
|
||||
'✅'
|
||||
}
|
||||
else {
|
||||
switch ($Analysis.Severity) {
|
||||
'Critical' { '❌' }
|
||||
'Warning' { '⚠️' }
|
||||
default { 'ℹ️' }
|
||||
}
|
||||
}
|
||||
|
||||
$color = switch ($Analysis.Severity) {
|
||||
'Critical' { 15158332 }
|
||||
'Warning' { 16776960 }
|
||||
default { 3447003 }
|
||||
$color = if ($isSuccess) {
|
||||
3066993
|
||||
}
|
||||
else {
|
||||
switch ($Analysis.Severity) {
|
||||
'Critical' { 15158332 }
|
||||
'Warning' { 16776960 }
|
||||
default { 3447003 }
|
||||
}
|
||||
}
|
||||
|
||||
$shortSha = if ($Analysis.CommitSha.Length -ge 8) { $Analysis.CommitSha.Substring(0, 8) } else { $Analysis.CommitSha }
|
||||
@@ -554,7 +648,14 @@ function New-DiscordEmbed {
|
||||
if ($Analysis.HasErrors) { $flags += 'errors found in logs' }
|
||||
if ($Analysis.HasWarnings) { $flags += 'warnings found in logs' }
|
||||
if ($Analysis.IsSlow) { $flags += "duration exceeded $($Config.SlowBuildThresholdMins)m" }
|
||||
if ($flags.Count -eq 0) { $flags += 'problematic status' }
|
||||
if ($flags.Count -eq 0) {
|
||||
if ($isSuccess) {
|
||||
$flags += 'build completed successfully'
|
||||
}
|
||||
else {
|
||||
$flags += 'build status changed'
|
||||
}
|
||||
}
|
||||
|
||||
$summary = $Analysis.ErrorSummary
|
||||
if ($summary.Length -gt 950) {
|
||||
@@ -562,7 +663,7 @@ function New-DiscordEmbed {
|
||||
}
|
||||
|
||||
return @{
|
||||
title = "$emoji $($Analysis.Severity): $($Analysis.Repository) build #$($Analysis.RunId)"
|
||||
title = "$emoji $($Analysis.Status): $($Analysis.Repository) build #$($Analysis.RunId)"
|
||||
url = $Analysis.BuildUrl
|
||||
color = $color
|
||||
description = ($flags -join ', ')
|
||||
@@ -597,10 +698,23 @@ function Send-DiscordNotification {
|
||||
$chunk = @($alerting | Select-Object -Skip $offset -First $maxEmbeds)
|
||||
$criticalCount = @($chunk | Where-Object { $_.Severity -eq 'Critical' }).Count
|
||||
$warningCount = @($chunk | Where-Object { $_.Severity -eq 'Warning' }).Count
|
||||
$successCount = @($chunk | Where-Object { Test-IsSuccessfulStatus -Status ([string]$_.Status) }).Count
|
||||
$completedCount = $chunk.Count
|
||||
$embeds = @($chunk | ForEach-Object { New-DiscordEmbed -Analysis $_ })
|
||||
$payload = @{
|
||||
username = 'Gitea Build Monitor'
|
||||
content = if ($criticalCount -gt 0) { "❌ Gitea build monitor found $criticalCount critical issue(s) and $warningCount warning(s)." } else { "⚠️ Gitea build monitor found $warningCount warning(s)." }
|
||||
content = if ($criticalCount -gt 0) {
|
||||
"❌ Gitea build monitor found $criticalCount failed build(s), $warningCount warning(s), and $successCount successful build(s)."
|
||||
}
|
||||
elseif ($warningCount -gt 0) {
|
||||
"⚠️ Gitea build monitor found $warningCount warning(s) and $successCount successful build(s)."
|
||||
}
|
||||
elseif ($successCount -gt 0) {
|
||||
"✅ Gitea build monitor found $successCount successful build(s)."
|
||||
}
|
||||
else {
|
||||
"ℹ️ Gitea build monitor found $completedCount completed build(s)."
|
||||
}
|
||||
embeds = $embeds
|
||||
}
|
||||
|
||||
@@ -776,18 +890,35 @@ function Test-IsRecentBuild {
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-IsCompletedBuild {
|
||||
param([Parameter(Mandatory)][object]$Build)
|
||||
|
||||
$conclusion = Get-ObjectValue -Object $Build -Name 'conclusion'
|
||||
if ($conclusion) {
|
||||
return Test-IsCompletedStatus -Status ([string]$conclusion)
|
||||
}
|
||||
|
||||
$status = Get-ObjectValue -Object $Build -Name 'status'
|
||||
if ($status) {
|
||||
return Test-IsCompletedStatus -Status ([string]$status)
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-CacheKey {
|
||||
param(
|
||||
[Parameter(Mandatory)][hashtable]$Repository,
|
||||
[Parameter(Mandatory)][object]$Job
|
||||
)
|
||||
|
||||
$kind = Get-ObjectValue -Object $Job -Name '_monitor_kind' -Default 'job'
|
||||
$jobId = Get-ObjectValue -Object $Job -Name 'id'
|
||||
$conclusion = Get-ObjectValue -Object $Job -Name 'conclusion'
|
||||
$status = Get-ObjectValue -Object $Job -Name 'status' -Default 'unknown'
|
||||
$state = if ($conclusion) { $conclusion } else { $status }
|
||||
|
||||
return "$(Get-RepositorySlug -Repository $Repository):job:$jobId`:state:$state"
|
||||
return "$(Get-RepositorySlug -Repository $Repository):$kind`:$jobId`:state:$state"
|
||||
}
|
||||
|
||||
function Start-BuildMonitor {
|
||||
@@ -812,13 +943,13 @@ function Start-BuildMonitor {
|
||||
$analyses = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
foreach ($repository in $Config.Repositories) {
|
||||
Write-Log -Message "Fetching recent Gitea Actions jobs for $(Get-RepositorySlug -Repository $repository)."
|
||||
Write-Log -Message "Fetching recent Gitea Actions runs for $(Get-RepositorySlug -Repository $repository)."
|
||||
|
||||
try {
|
||||
$jobs = Get-BuildRuns -Repository $repository
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level 'ERROR' -Message "Failed to retrieve jobs for $(Get-RepositorySlug -Repository $repository): $($_.Exception.Message)"
|
||||
Write-Log -Level 'ERROR' -Message "Failed to retrieve builds for $(Get-RepositorySlug -Repository $repository): $($_.Exception.Message)"
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -826,6 +957,9 @@ function Start-BuildMonitor {
|
||||
if (-not (Test-IsRecentBuild -Job $job)) {
|
||||
continue
|
||||
}
|
||||
if (-not (Test-IsCompletedBuild -Build $job)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$cacheKey = Get-CacheKey -Repository $repository -Job $job
|
||||
if ($cache.ContainsKey($cacheKey)) {
|
||||
|
||||
Reference in New Issue
Block a user