#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 { 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)" } } } # ── 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)"