#Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS Creates a timestamped backup of the CI template VM directory. .DESCRIPTION Run before any template refresh (toolchain update, KMS re-activation, snapshot rebuild). The act_runner service is stopped for the duration so no vmrun clone races the file copy, then restarted automatically. Restore procedure (after a bad refresh): Stop-Service act_runner Remove-Item 'F:\CI\Templates\WinBuild2025' -Recurse -Force Copy-Item 'F:\CI\Backups\Template_' 'F:\CI\Templates\WinBuild2025' -Recurse Start-Service act_runner .PARAMETER TemplatePath Directory containing the template VM files (.vmx, .vmdk, snapshots). Default: F:\CI\Templates\WinBuild2025 .PARAMETER AllTemplates Back up every subdirectory under F:\CI\Templates\ in a single pass. When set, TemplatePath is ignored. Backup folder names include the template directory name: Template__. .PARAMETER BackupBaseDir Directory under which timestamped backup folders are created. Default: F:\CI\Backups .PARAMETER KeepCount Number of most-recent backups to retain. Older ones are deleted. Default: 3 .PARAMETER SkipRunnerStop Skip stopping/restarting act_runner. Use when the service is already stopped or when called from a maintenance window where the runner is offline. .EXAMPLE # Standard pre-refresh backup (run elevated) .\Backup-CITemplate.ps1 # Dry run to preview what would happen .\Backup-CITemplate.ps1 -WhatIf # Keep 5 backups .\Backup-CITemplate.ps1 -KeepCount 5 # Back up all three templates .\Backup-CITemplate.ps1 -AllTemplates #> [CmdletBinding(SupportsShouldProcess)] param( [string] $TemplatePath = 'F:\CI\Templates\WinBuild2025', [string] $BackupBaseDir = 'F:\CI\Backups', [ValidateRange(1, 20)] [int] $KeepCount = 3, [switch] $SkipRunnerStop, [switch] $AllTemplates ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' if (-not (Test-Path $BackupBaseDir)) { New-Item -ItemType Directory -Path $BackupBaseDir -Force | Out-Null } # ── Step 1: Stop runner ────────────────────────────────────────────────────── $runnerWasRunning = $false if (-not $SkipRunnerStop) { $svc = Get-Service -Name act_runner -ErrorAction SilentlyContinue if ($svc -and $svc.Status -eq 'Running') { if ($PSCmdlet.ShouldProcess('act_runner', 'Stop service before backup')) { Write-Host "[Backup-CITemplate] Stopping act_runner..." Stop-Service -Name act_runner -Force $runnerWasRunning = $true Write-Host "[Backup-CITemplate] act_runner stopped." } } } try { # ── Steps 2+3: Backup and prune one or all templates ────────────────────── $templatesToBackup = @() if ($AllTemplates) { $templatesToBackup = @(Get-ChildItem 'F:\CI\Templates' -Directory -ErrorAction SilentlyContinue) if ($templatesToBackup.Count -eq 0) { throw 'No template directories found under F:\CI\Templates' } } else { if (-not (Test-Path $TemplatePath -PathType Container)) { throw "Template directory not found: $TemplatePath" } $templatesToBackup = @(Get-Item $TemplatePath) } foreach ($tmpl in $templatesToBackup) { $nameTag = if ($AllTemplates) { "$($tmpl.Name)_$timestamp" } else { $timestamp } $backupDest = Join-Path $BackupBaseDir "Template_$nameTag" # ── Step 2: Copy template directory ─────────────────────────────────── if ($PSCmdlet.ShouldProcess($tmpl.FullName, "Copy to $backupDest")) { Write-Host "[Backup-CITemplate] Copying $($tmpl.FullName) -> $backupDest ..." $sw = [System.Diagnostics.Stopwatch]::StartNew() Copy-Item -Path $tmpl.FullName -Destination $backupDest -Recurse -Force $sw.Stop() # Size sum without Measure-Object.Sum (Set-StrictMode safe) $backupFiles = Get-ChildItem $backupDest -Recurse -File -ErrorAction SilentlyContinue $totalBytes = [long]0 if ($backupFiles) { foreach ($f in $backupFiles) { $totalBytes += $f.Length } } $sizeMB = [math]::Round($totalBytes / 1MB, 0) Write-Host "[Backup-CITemplate] Backup complete: $backupDest ($sizeMB MB, $($sw.Elapsed.TotalSeconds.ToString('F0'))s)" } # ── Step 3: Prune old backups for this template ──────────────────────── $prunePattern = if ($AllTemplates) { "Template_$($tmpl.Name)_????????_??????" } else { 'Template_????????_??????' } $existing = Get-ChildItem $BackupBaseDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -like $prunePattern } | Sort-Object LastWriteTime -Descending if ($existing.Count -gt $KeepCount) { $toRemove = $existing | Select-Object -Skip $KeepCount foreach ($old in $toRemove) { if ($PSCmdlet.ShouldProcess($old.FullName, 'Remove old backup')) { Write-Host "[Backup-CITemplate] Pruning old backup: $($old.Name)" Remove-Item $old.FullName -Recurse -Force } } Write-Host "[Backup-CITemplate] Retained $KeepCount backup(s) for $($tmpl.Name)." } } } finally { # ── Step 4: Restart runner ──────────────────────────────────────────────── if ($runnerWasRunning) { if ($PSCmdlet.ShouldProcess('act_runner', 'Start service after backup')) { Write-Host "[Backup-CITemplate] Restarting act_runner..." Start-Service -Name act_runner $newStatus = (Get-Service -Name act_runner).Status Write-Host "[Backup-CITemplate] act_runner status: $newStatus" } } }