diff --git a/gitea-build-monitor.ps1 b/gitea-build-monitor.ps1
index 13d530a..38101a6 100755
--- a/gitea-build-monitor.ps1
+++ b/gitea-build-monitor.ps1
@@ -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) {
diff --git a/posts/posts-list.org b/posts/posts-list.org
index c02ae25..e10fe37 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:52@@
+- [[file:career/career-list.org][Career List]] @@html:08-05-2026 16:04@@
- [[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/recently-updated.org b/recently-updated.org
index 6fb684c..48f6d79 100755
--- a/recently-updated.org
+++ b/recently-updated.org
@@ -16,7 +16,6 @@
- [[file:blogs/2026/04-april/ending-the-week-17-04-26.org][End of week thoughts]] @@html:2026-04-17 16:17@@
- [[file:blogs/2026/04-april/16-04-26.org][Rambles]] @@html:2026-04-16 16:02@@
- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:2026-04-14 16:36@@
-- [[file:home/categories.sync-conflict-20260417-233429-VT6366A.org][Categories]] @@html:2026-04-14 16:36@@
- [[file:blogs/2026/04-april/comparison-14-04.org][Comparison is the thief of joy]] @@html:2026-04-14 09:55@@
- [[file:blogs/2026/04-april/12-04-week-review.org][[12-04-2026] - Weekly Review]] @@html:2026-04-12 12:00@@
- [[file:blogs/2026/04-april/starting-new-rotation-08-04-26.org][Starting new rotation]] @@html:2026-04-08 16:15@@
@@ -28,3 +27,4 @@
- [[file:blogs/2026/03-march/intellectually-challenging-myself-30-03-26.org][Intellectually challenging oneself]] @@html:2026-03-31 11:14@@
- [[file:blogs/2026/03-march/cooking-dinner-29-03-26.org][Cooking Dinner (num)]] @@html:2026-03-30 11:49@@
- [[file:blogs/2026/03-march/29-03-week-review.org][[29-03-2026] - Weekly Review]] @@html:2026-03-29 12:00@@
+- [[file:blogs/2026/03-march/training-on-friday-afternoon-27-03-26.org][Training time on Friday afternoon]] @@html:2026-03-27 15:09@@
diff --git a/sitemap.org b/sitemap.org
index 69e2620..130ecb8 100755
--- a/sitemap.org
+++ b/sitemap.org
@@ -13,24 +13,11 @@
- [[file:tags/website.org][Tag: website]]
- [[file:tags/life.org][Tag: life]]
- [[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/insights.org][Tag: insights]]
- [[file:tags/reading.org][Tag: reading]]
+ - [[file:tags/emacs.org][Tag: emacs]]
- [[file:tags/maths.org][Tag: maths]]
-- home
- - [[file:home/countdown.org][Countdown]]
- - [[file:home/contact.org][Contact]]
- - [[file:home/backlog.org][Backlog]]
- - [[file:home/notes.org][Notes]]
- - [[file:home/services.org][Service]]
- - [[file:home/status.org][Competency Status Board]]
- - [[file:home/categories.sync-conflict-20260417-233429-VT6366A.org][Categories]]
- - [[file:home/wird-tracker.org][Wird Tracker]]
- - [[file:home/categories.org][Categories]]
- - guide
- - [[file:home/guide/setup.org][Setup]]
- - [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
- posts
- [[file:posts/posts-intro.org][Posts Introduction]]
- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]]
@@ -64,4 +51,16 @@
- clean-code
- [[file:books/clean-code/clean-code-notes.org][Clean Code Notes]]
- lima
- - [[file:lima/lima-list.org][Lima]]
\ No newline at end of file
+ - [[file:lima/lima-list.org][Lima]]
+- home
+ - [[file:home/countdown.org][Countdown]]
+ - [[file:home/contact.org][Contact]]
+ - [[file:home/backlog.org][Backlog]]
+ - [[file:home/notes.org][Notes]]
+ - [[file:home/services.org][Service]]
+ - [[file:home/status.org][Competency Status Board]]
+ - [[file:home/wird-tracker.org][Wird Tracker]]
+ - [[file:home/categories.org][Categories]]
+ - guide
+ - [[file:home/guide/setup.org][Setup]]
+ - [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
\ No newline at end of file