Modules/businessdev.ALbuild.Core/Public/Clear-ALbuildCache.ps1
|
function Clear-ALbuildCache { <# .SYNOPSIS Prunes old, unused ALbuild cache content to reclaim disk space on a build agent. .DESCRIPTION Build agents accumulate large caches that ALbuild never trims on its own: the extracted artifact/symbol cache (ArtifactCacheFolder), the dependency package cache (PackageCacheFolder) and the Business Central artifact cache (BcArtifactCacheFolder) - the last two of which grow a new sub-folder per BC version and per package, unbounded. This cmdlet removes cache entries that have not been written to within a retention window, keeping recent ones. The cache locations are read from Get-ALbuildConfig, so it prunes exactly the folders the tasks use (including any relocated to a roomier drive). Each cache's immediate child entries are the pruning unit; an entry is removed when its last-write age exceeds -KeepDays, except the newest -KeepLatest entries per cache are always retained as a safety floor. Age is measured by LastWriteTime (last-access time is unreliable - Windows disables it by default). Supports -WhatIf/-Confirm (it is destructive), reports the space reclaimed per cache, and logs every removal so a scheduled cleanup is auditable from the pipeline log. With -IncludeDocker it also prunes unused Docker images and stopped containers older than the same window. .PARAMETER KeepDays Retention window in days. Cache entries not written within this many days are removed. Default 30. .PARAMETER KeepLatest Always keep at least this many most-recently-written entries per cache, regardless of age (a safety floor so a cache is never emptied). Default 0. .PARAMETER Cache Which caches to prune: All (default), Artifacts, Packages or BcArtifacts. .PARAMETER IncludeDocker Also run 'docker image prune' (unused images) and 'docker container prune' (stopped containers) filtered to the same -KeepDays window. Skipped with a note if the docker CLI is unavailable. .EXAMPLE Clear-ALbuildCache -WhatIf Show what a default 30-day prune would remove, without deleting anything. .EXAMPLE Clear-ALbuildCache -KeepDays 14 -KeepLatest 2 -IncludeDocker Keep 14 days (but never fewer than the 2 newest per cache) and also prune old Docker content. .OUTPUTS PSCustomObject: per-cache Removed count / FreedBytes / FreedText plus a TotalFreedBytes/Text. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType([PSCustomObject])] param( [ValidateRange(0, [int]::MaxValue)] [int] $KeepDays = 30, [ValidateRange(0, [int]::MaxValue)] [int] $KeepLatest = 0, [ValidateSet('All', 'Artifacts', 'Packages', 'BcArtifacts')] [string] $Cache = 'All', [switch] $IncludeDocker ) $config = Get-ALbuildConfig $targets = [ordered]@{} if ($Cache -in @('All', 'Artifacts')) { $targets['Artifacts'] = $config.ArtifactCacheFolder } if ($Cache -in @('All', 'Packages')) { $targets['Packages'] = $config.PackageCacheFolder } if ($Cache -in @('All', 'BcArtifacts')) { $targets['BcArtifacts'] = $config.BcArtifactCacheFolder } $cutoff = (Get-Date).AddDays(-$KeepDays) Write-ALbuildLog "Pruning ALbuild caches: keep entries written on/after $($cutoff.ToString('yyyy-MM-dd')) (KeepDays=$KeepDays), always keep the newest $KeepLatest per cache." $humanize = { param([long] $Bytes) $units = 'B', 'KB', 'MB', 'GB', 'TB'; $i = 0; $n = [double]$Bytes while ($n -ge 1024 -and $i -lt $units.Count - 1) { $n /= 1024; $i++ } '{0:0.##} {1}' -f $n, $units[$i] } $folderSize = { param([string] $Path) try { [long](Get-ChildItem -LiteralPath $Path -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum } catch { [long]0 } } $results = [System.Collections.Generic.List[object]]::new() $totalFreed = [long]0 foreach ($name in $targets.Keys) { $root = $targets[$name] if ([string]::IsNullOrWhiteSpace($root) -or -not (Test-Path -LiteralPath $root)) { Write-ALbuildLog -Level Verbose " [$name] '$root' does not exist; nothing to prune." $results.Add([PSCustomObject]@{ Cache = $name; Path = $root; Removed = 0; FreedBytes = [long]0; FreedText = '0 B' }) continue } $entries = @(Get-ChildItem -LiteralPath $root -Directory -Force -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending) # Safety floor: never consider the newest -KeepLatest entries for removal. $candidates = if ($KeepLatest -gt 0 -and $entries.Count -gt $KeepLatest) { $entries[$KeepLatest..($entries.Count - 1)] } elseif ($KeepLatest -gt 0) { @() } else { $entries } $stale = @($candidates | Where-Object { $_.LastWriteTime -lt $cutoff }) $removed = 0; $freed = [long]0 foreach ($dir in $stale) { $size = & $folderSize $dir.FullName if ($PSCmdlet.ShouldProcess($dir.FullName, "Remove cached entry (last modified $($dir.LastWriteTime.ToString('yyyy-MM-dd')), $(& $humanize $size))")) { try { Remove-Item -LiteralPath $dir.FullName -Recurse -Force -ErrorAction Stop $removed++; $freed += $size Write-ALbuildLog " [$name] removed '$($dir.Name)' (modified $($dir.LastWriteTime.ToString('yyyy-MM-dd')), freed $(& $humanize $size))." } catch { Write-ALbuildLog -Level Warning " [$name] could not remove '$($dir.FullName)': $($_.Exception.Message)" } } else { $freed += $size } # -WhatIf: still report what would be freed } $totalFreed += $freed $entryWord = if ($stale.Count -eq 1) { 'entry' } else { 'entries' } Write-ALbuildLog -Level Success " [$name] $($stale.Count) stale $entryWord of $($entries.Count); freed $(& $humanize $freed)." $results.Add([PSCustomObject]@{ Cache = $name; Path = $root; Removed = $removed; FreedBytes = $freed; FreedText = (& $humanize $freed) }) } if ($IncludeDocker) { if (Get-Command -Name docker -ErrorAction SilentlyContinue) { $until = "$([int]($KeepDays * 24))h" if ($PSCmdlet.ShouldProcess("docker (until=$until)", 'Prune unused images and stopped containers')) { Write-ALbuildLog " [Docker] pruning stopped containers and unused images older than $until..." try { (& docker container prune -f --filter "until=$until" 2>&1) | ForEach-Object { Write-ALbuildLog -Level Verbose " $_" } (& docker image prune -a -f --filter "until=$until" 2>&1) | ForEach-Object { Write-ALbuildLog " $_" } } catch { Write-ALbuildLog -Level Warning " [Docker] prune failed: $($_.Exception.Message)" } } } else { Write-ALbuildLog -Level Warning ' [Docker] -IncludeDocker requested but the docker CLI was not found; skipping.' } } Write-ALbuildLog -Level Success "ALbuild cache prune complete: reclaimed $(& $humanize $totalFreed) across $($results.Count) cache(s)." [PSCustomObject]@{ Caches = $results.ToArray() TotalFreedBytes = $totalFreed TotalFreedText = (& $humanize $totalFreed) KeepDays = $KeepDays KeepLatest = $KeepLatest } } |