diff --git a/gitea-build-monitor.ps1 b/gitea-build-monitor.ps1
index b03e7fe..13d530a 100755
--- a/gitea-build-monitor.ps1
+++ b/gitea-build-monitor.ps1
@@ -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)) {
diff --git a/home/categories.sync-conflict-20260417-233429-VT6366A.org b/home/categories.sync-conflict-20260417-233429-VT6366A.org
deleted file mode 100755
index 023e4cd..0000000
--- a/home/categories.sync-conflict-20260417-233429-VT6366A.org
+++ /dev/null
@@ -1,16 +0,0 @@
-#+TITLE: Categories
-#+OPTIONS: toc:nil num:nil title:nil
-
-* Categories (Includes both blogs and posts)
-- [[file:../tags/education.org][@@html:education@@]] (1)
-- [[file:../tags/emacs.org][@@html:emacs@@]] (2)
-- [[file:../tags/insights.org][@@html:insights@@]] (4)
-- [[file:../tags/introduction.org][@@html:introduction@@]] (3)
-- [[file:../tags/learning.org][@@html:learning@@]] (16)
-- [[file:../tags/life.org][@@html:life@@]] (21)
-- [[file:../tags/maths.org][@@html:maths@@]] (1)
-- [[file:../tags/notes.org][@@html:notes@@]] (17)
-- [[file:../tags/reading.org][@@html:reading@@]] (1)
-- [[file:../tags/review.org][@@html:review@@]] (26)
-- [[file:../tags/update.org][@@html:update@@]] (2)
-- [[file:../tags/website.org][@@html:website@@]] (2)
\ No newline at end of file
diff --git a/posts/posts-list.org b/posts/posts-list.org
index f7dd9c8..c02ae25 100755
--- a/posts/posts-list.org
+++ b/posts/posts-list.org
@@ -4,7 +4,7 @@
See the categories: @@html:Categories@@
* Posts:
-- [[file:career/career-list.org][Career List]] @@html:08-05-2026 15:43@@
+- [[file:career/career-list.org][Career List]] @@html:08-05-2026 15:52@@
- [[file:posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:14-04-2026 16:36@@
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:11-03-2026 17:18@@ @@html:learning@@ @@html:notes@@
- [[file:career/javascript.org][Understands the Javascript language]] @@html:11-03-2026 16:52@@ @@html:learning@@ @@html:notes@@
diff --git a/sitemap.org b/sitemap.org
index a3e59e4..69e2620 100755
--- a/sitemap.org
+++ b/sitemap.org
@@ -12,10 +12,10 @@
- [[file:tags/review.org][Tag: review]]
- [[file:tags/website.org][Tag: website]]
- [[file:tags/life.org][Tag: life]]
- - [[file:tags/education.org][Tag: education]]
- [[file:tags/update.org][Tag: update]]
- [[file:tags/insights.org][Tag: insights]]
- [[file:tags/emacs.org][Tag: emacs]]
+ - [[file:tags/education.org][Tag: education]]
- [[file:tags/reading.org][Tag: reading]]
- [[file:tags/maths.org][Tag: maths]]
- home