feat(sprint2): §2.1/2.2/2.3/2.5 — IP lock/lease, scheduled tasks, retention, versioned snapshot
§2.1 — IP race with capacity>1 (Invoke-CIJob.ps1)
Phases 2-3b (clone+start+IP) now run inside a file-based exclusive mutex
(F:\CI\State\vm-start.lock via FileShare.None). Only one job at a time is
in the VM-start phase, preventing concurrent vmrun starts from racing for
the same DHCP lease. After IP is confirmed unique it is written to an IP
lease file (F:\CI\State\ip-leases\<IP>.lease). Lock is released immediately
so build phases run fully in parallel. Lease released in finally block.
Collision detection: if a lease file already exists for the detected IP,
the job fails fast with a clear error rather than silently sharing a WinRM target.
§2.5 — Versioned snapshot (Invoke-CIJob.ps1 + runner/config.yaml)
$SnapshotName now defaults to GITEA_CI_SNAPSHOT_NAME env var, falling back
to 'BaseClean'. config.yaml documents the commented-out variable for snapshot
refresh workflow (BaseClean_<yyyyMMdd> naming convention).
§2.3 — Artifact/log retention (scripts/Invoke-RetentionPolicy.ps1)
Purges per-job subdirs under F:\CI\Artifacts and F:\CI\Logs older than
RetentionDays (default 30). Switches to AggressiveRetentionDays (default 7)
when F: free space drops below MinFreeGB (default 50 GB). Also removes stale
IP lease files older than 12 hours (defensive cleanup for crashed jobs).
SupportsShouldProcess (-WhatIf) for dry-run.
§2.2 — Scheduled task registration (scripts/Register-CIScheduledTasks.ps1)
Registers two tasks under \CI\ task folder as SYSTEM/HighestPrivilege:
CI-CleanupOrphans — every 6h + AtStartup, -MaxAgeHours 4
CI-RetentionPolicy — daily 03:00 (+30min random) + AtStartup (+15min random)
Idempotent (-Force), SupportsShouldProcess (-WhatIf), validates script paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
#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)"
|
||||
Reference in New Issue
Block a user