Files
local-ci-cd-system/scripts/Invoke-RetentionPolicy.ps1
Simone 7dff7d3dba feat(sprint2): fix critical and high-priority bugs
C1: composite action selects Linux/Windows template from GITEA_CI_LINUX_TEMPLATE_PATH
    based on resolved guest OS; build-nsis.yml adds belt-and-suspenders guest-os param.
C2: redact ExtraGuestEnv secrets in Invoke-RemoteBuild.ps1 (envPrefix never logged).
H2: composite action outputs block corrected; duplicate artifact-name input removed.
H3: Linux Mode 2 PAT now uses http.extraHeader (mirrors Windows branch, drops GIT_CLONE_URL).
H6: Setup-Host.ps1 directory array updated to canonical layout (adds State, ip-leases, keys,
    Cache\pip, WinBuild2025/2022/LinuxBuild2404; removes obsolete WinBuild).
H7: Backup-CITemplate.ps1 default TemplatePath -> WinBuild2025; -AllTemplates switch added.
H8: Setup-Host.ps1 adds StrictHostKeyChecking=accept-new and Step 8 to replicate SSH config
    to LocalSystem profile for act_runner service account.
H11: runner/config.yaml adds GITEA_CI_SCRIPT_ROOT env var; action.yml uses it in place of
     hardcoded N:\ path.
Also: Invoke-RemoteBuild.ps1 skips 7-Zip/Compress-Archive when artifact source dir does not
     exist (fixes burn-in smoke runs with exit-0 build command and no output).
2026-05-12 21:12:07 +02:00

145 lines
6.3 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Purges old CI artifacts, logs, and stale IP leases based on retention policy.
.DESCRIPTION
Removes per-job subdirectories under ArtifactDir and LogDir whose last-write
time is older than RetentionDays. If the monitored drive is below MinFreeGB,
switches automatically to AggressiveRetentionDays to reclaim space faster.
Also removes stale IP lease files under F:\CI\State\ip-leases\ that are
older than 12 hours (defensive cleanup for jobs that crashed without a finally).
Designed to run as a Windows Scheduled Task (see Register-CIScheduledTasks.ps1).
Safe to run while CI jobs are active — only deletes directories whose
last-write time predates the cutoff.
.PARAMETER ArtifactDir
Directory containing per-job artifact subdirectories. Default: F:\CI\Artifacts
.PARAMETER LogDir
Directory containing per-job log subdirectories. Default: F:\CI\Logs
.PARAMETER RetentionDays
Normal retention threshold in days. Default: 30
.PARAMETER AggressiveRetentionDays
Retention threshold used when drive free space drops below MinFreeGB. Default: 7
.PARAMETER MinFreeGB
If drive free space is below this value, AggressiveRetentionDays is used instead
of RetentionDays and a warning is emitted. Default: 50
.PARAMETER DriveLetter
Single drive letter (without colon) to check for free space. Default: F
.EXAMPLE
# Dry run — lists what would be deleted
.\Invoke-RetentionPolicy.ps1 -WhatIf
# Live run
.\Invoke-RetentionPolicy.ps1
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[string] $ArtifactDir = 'F:\CI\Artifacts',
[string] $LogDir = 'F:\CI\Logs',
[ValidateRange(1, 365)]
[int] $RetentionDays = 30,
[ValidateRange(1, 365)]
[int] $AggressiveRetentionDays = 7,
[ValidateRange(1, 1000)]
[int] $MinFreeGB = 50,
[ValidatePattern('^[A-Za-z]$')]
[string] $DriveLetter = 'F'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Continue' # Don't abort on individual file errors
# ── Determine effective retention threshold ───────────────────────────────────
$drive = Get-PSDrive -Name $DriveLetter -ErrorAction SilentlyContinue
$freeGB = if ($drive) { [math]::Round($drive.Free / 1GB, 1) } else { [double]::MaxValue }
$aggressiveMode = $freeGB -lt $MinFreeGB
$effectiveDays = if ($aggressiveMode) { $AggressiveRetentionDays } else { $RetentionDays }
$cutoff = (Get-Date).AddDays(-$effectiveDays)
Write-Host "[RetentionPolicy] Drive ${DriveLetter}: free ${freeGB} GB"
if ($aggressiveMode) {
Write-Warning "[RetentionPolicy] Free space below ${MinFreeGB} GB — aggressive retention: ${AggressiveRetentionDays}d"
} else {
Write-Host "[RetentionPolicy] Normal retention: ${effectiveDays}d (cutoff: $($cutoff.ToString('yyyy-MM-dd')))"
}
# ── Helper: purge old subdirectories under a base dir ─────────────────────────
function Remove-OldJobDirs {
[CmdletBinding(SupportsShouldProcess)]
param([string]$BaseDir, [string]$Label)
if (-not (Test-Path $BaseDir)) {
Write-Host "[RetentionPolicy] $Label dir not found: $BaseDir — skipping."
return
}
$old = Get-ChildItem -Path $BaseDir -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt $cutoff }
if (-not @($old).Count) {
Write-Host "[RetentionPolicy] ${Label}: nothing to purge (cutoff: $($cutoff.ToString('yyyy-MM-dd')))."
return
}
foreach ($dir in $old) {
$agedays = [int]((Get-Date) - $dir.LastWriteTime).TotalDays
if ($PSCmdlet.ShouldProcess($dir.FullName, "Remove $Label dir (age: ${agedays}d)")) {
Remove-Item $dir.FullName -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "[RetentionPolicy] Purged $Label (${agedays}d): $($dir.Name)"
}
}
}
Remove-OldJobDirs -BaseDir $ArtifactDir -Label 'artifact'
Remove-OldJobDirs -BaseDir $LogDir -Label 'log'
# ── Stale IP lease cleanup (§2.1 defensive) ───────────────────────────────────
# Lease files are normally deleted by Invoke-CIJob.ps1 finally block.
# If the host crashed mid-job the lease file is never removed. Remove leases
# older than 12 hours (well beyond any plausible build duration).
$leasesDir = 'F:\CI\State\ip-leases'
$leaseCutoff = (Get-Date).AddHours(-12)
if (Test-Path $leasesDir) {
$staleLeases = Get-ChildItem -Path $leasesDir -File -Filter '*.lease' -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt $leaseCutoff }
foreach ($lf in @($staleLeases)) {
if ($PSCmdlet.ShouldProcess($lf.FullName, 'Remove stale IP lease')) {
Remove-Item $lf.FullName -Force -ErrorAction SilentlyContinue
Write-Host "[RetentionPolicy] Removed stale IP lease: $($lf.Name)"
}
}
}
# ── Stale vm-start.lock cleanup ─────────────────────────────────────────────
# The lock is released by Invoke-CIJob.ps1 normally. On host crash it persists;
# the OS-level lock is gone after reboot but the file blocks subsequent IP-acquire
# retries for 10 minutes and confuses operators. Remove if older than 30 minutes.
$lockFile = 'F:\CI\State\vm-start.lock'
$lockCutoff = (Get-Date).AddMinutes(-30)
if ((Test-Path $lockFile) -and (Get-Item $lockFile).LastWriteTime -lt $lockCutoff) {
if ($PSCmdlet.ShouldProcess($lockFile, 'Remove stale vm-start.lock')) {
$lockAgeMin = [int]((Get-Date) - (Get-Item $lockFile).LastWriteTime).TotalMinutes
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
Write-Host "[RetentionPolicy] Removed stale vm-start.lock (age: ${lockAgeMin} min)"
}
}
# ── Summary ───────────────────────────────────────────────────────────────────
$freeGBAfter = if ($drive) { [math]::Round((Get-PSDrive -Name $DriveLetter -ErrorAction SilentlyContinue).Free / 1GB, 1) } else { $freeGB }
$delta = [math]::Round($freeGBAfter - $freeGB, 1)
Write-Host "[RetentionPolicy] Done. Drive ${DriveLetter}: free ${freeGBAfter} GB (delta: +${delta} GB)"