#Requires -Version 5.1 <# .SYNOPSIS HOST-side script: delivers and runs Install-CIToolchain-WinBuild2022.ps1 inside the template VM. .DESCRIPTION Automates the template VM provisioning from the host machine: 1. Pre-flight validation: script presence, IP octet range, TCP/5986 reachable 2. Configures host WinRM client (TrustedHosts only, HTTPS needs no AllowUnencrypted) — restored on exit 3. Validates host WinRM TrustedHosts applied correctly 4. Opens PSSession -UseSSL -Port 5986 -SkipCACheck — fails fast with actionable message (PS 5.1 Test-WSMan has no -SessionOption, so PSSession is used as the connectivity probe) 5. PSSession open — step 4 and 5 are now merged 6. Ensures C:\CI exists on guest; validates creation 7. Copies Install-CIToolchain-WinBuild2022.ps1 into the VM; validates file present + size match 8. Runs Install-CIToolchain-WinBuild2022.ps1 inside the VM with the given parameters (Setup performs per-step validation internally — aborts on any failure) 9. Handles the reboot-required case (exit 3010) and optionally re-runs 10. Post-setup remote validation: 9 guest-state checks via a single Invoke-Command (WinRM, user+admin, UAC, firewall, .NET, Python, MSBuild, CI dirs) 11. Restores host TrustedHosts in finally block Every validation step uses Assert-Step: prints [OK] on success, throws on failure. The snapshot should only be taken after this script exits 0. PREREQUISITES (do these manually before running this script): a. VM NIC is set to VMnet8 (NAT) — the VM needs internet for downloads b. Windows Server is installed and booted c. WinRM is enabled inside the VM. From an elevated cmd/PS in the VM run: winrm quickconfig -q Enable-PSRemoting -Force -SkipNetworkProfileCheck d. You know the IP address the VM got from DHCP on VMnet8 (NAT) (check via: ipconfig inside the VM, or VMware → VM → Settings → Network Adapter → Advanced → MAC address, then check DHCP leases) AFTER THIS SCRIPT COMPLETES (exit 0, all validations passed): 1. Shut down the VM 2. Take snapshot: VM → Snapshot → Take Snapshot → name: BaseClean (VM stays on VMnet8 NAT — internet access is required for builds) 3. Store credentials on host (use the same password chosen during provisioning): New-StoredCredential -Target 'BuildVMGuest' -UserName 'ci_build' ` -Password '' -Persist LocalMachine .PARAMETER VMXPath Full path to the template VM's .vmx file (e.g. F:\CI\Templates\WinBuild\WinBuild.vmx). When provided: - If the VM is powered off, the script starts it automatically via vmrun. - The guest IP is detected automatically via vmrun getGuestIPAddress (requires VMware Tools installed in the guest). - At the end, the snapshot is created automatically after provisioning. Either -VMXPath or -VMIPAddress (or both) must be supplied. .PARAMETER VMIPAddress IP address of the template VM on VMnet8 NAT. Optional when -VMXPath is provided (IP is auto-detected via VMware Tools). Required when -VMXPath is omitted. Example: 192.168.79.130 .PARAMETER AdminUsername Administrator username inside the VM. Default: Administrator .PARAMETER AdminPassword Administrator password inside the VM. If not supplied, prompts interactively. .PARAMETER BuildPassword Password to set for the ci_build user inside the VM. Must not be empty — script prompts interactively if not supplied. .PARAMETER BuildUsername Local username for the CI build account inside the VM. Default: ci_build. Passed through to Install-CIToolchain-WinBuild2022.ps1 and used in post-setup validation. .PARAMETER DotNetSdkVersion .NET SDK channel to install in the VM. Default: 10.0 (matches host SDK). .PARAMETER SkipWindowsUpdate Pass through to Install-CIToolchain-WinBuild2022.ps1 to skip the Windows Update step. .PARAMETER SnapshotName Name of the snapshot to create. Default: BaseClean (case-sensitive — used by New-BuildVM.ps1). Only relevant when -TakeSnapshot is set. .PARAMETER StoreCredential After provisioning completes, store BuildUsername/BuildPassword in Windows Credential Manager (target: CredentialTarget). Installs the CredentialManager module (CurrentUser scope) automatically if not present. Equivalent to running manually: New-StoredCredential -Target 'BuildVMGuest' -UserName 'ci_build' ` -Password '' -Persist LocalMachine .PARAMETER CredentialTarget Windows Credential Manager target name used when -StoreCredential is set. Default: BuildVMGuest (matches the target expected by Invoke-CIJob.ps1 and the other CI scripts that load guest credentials via Get-StoredCredential). .EXAMPLE # Fully automated — VMX path only (power-on + IP detection + snapshot automatic): .\Prepare-WinBuild2022.ps1 ` -VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' ` -BuildPassword 'StrongCIPass!2026' -StoreCredential # Skip Windows Update (already applied): .\Prepare-WinBuild2022.ps1 ` -VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' ` -SkipWindowsUpdate -StoreCredential # Explicit IP (manual start, no VMware Tools required for IP detection): .\Prepare-WinBuild2022.ps1 -VMIPAddress 192.168.79.130 ` -BuildPassword 'StrongCIPass!2026' -StoreCredential # Both supplied — uses VMXPath for power-on/snapshot, explicit IP skips getGuestIPAddress: .\Prepare-WinBuild2022.ps1 ` -VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' -VMIPAddress 192.168.79.130 ` -BuildPassword 'StrongCIPass!2026' -StoreCredential #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Template setup script: plain-text password from CI credential store converted to SecureString for WinRM PSCredential. Intentional by design.')] [CmdletBinding()] param( # Path to the VM .vmx — enables auto power-on + IP detection + auto snapshot. # Either -VMXPath or -VMIPAddress (or both) must be supplied. [string] $VMXPath = '', # Guest IP on VMnet8 NAT. Optional when -VMXPath provided (auto-detected via VMware Tools). [string] $VMIPAddress = '', [string] $AdminUsername = 'Administrator', [string] $AdminPassword = '', [string] $BuildPassword = '', [string] $BuildUsername = 'ci_build', [string] $DotNetSdkVersion = '10.0', # Tier-2 Toolchain version pins — forwarded as-is to Install-CIToolchain-WinBuild2022.ps1 [string] $Pwsh7Version = '7.6.1', [string] $NSISVersion = '3.12', [string] $CMakeVersion = '3.31.6', [string] $NodeJSVersion = '22.14.0', [string] $WiXVersion = '4.0.5', [string] $GhCliVersion = '2.92.0', [string] $VcpkgRef = '', # Skip Tier-2 Toolchain — forwarded to Install-CIToolchain-WinBuild2022.ps1 -SkipTier2 [switch] $SkipTier2, [switch] $SkipWindowsUpdate, # Skip disk cleanup inside the VM (cleanmgr + DISM + cache clear) — speeds up re-runs [switch] $SkipCleanup, # Snapshot name (case-sensitive — must match GITEA_CI_SNAPSHOT_NAME in runner/config.yaml) [string] $SnapshotName = 'BaseClean', # Store BuildUsername/BuildPassword in Windows Credential Manager after provisioning [switch] $StoreCredential, # Credential Manager target name (must match what CI scripts use via Get-StoredCredential) [string] $CredentialTarget = 'BuildVMGuest' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # suppress Write-Progress (IWR, etc.) function Test-TcpPort { param([string]$ComputerName, [int]$Port, [int]$TimeoutMs = 3000) try { $tcp = [System.Net.Sockets.TcpClient]::new() $ar = $tcp.BeginConnect($ComputerName, $Port, $null, $null) $ok = $ar.AsyncWaitHandle.WaitOne($TimeoutMs, $false) $connected = $ok -and $tcp.Connected $tcp.Close() return $connected } catch { return $false } } # ── Locate vmrun.exe ────────────────────────────────────────────────────────── $vmrunCandidates = @( 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe', 'C:\Program Files\VMware\VMware Workstation\vmrun.exe' ) $vmrunExe = $vmrunCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $vmrunExe) { $vmrunFound = Get-Command vmrun -ErrorAction SilentlyContinue if ($vmrunFound) { $vmrunExe = $vmrunFound.Source } } if (-not $vmrunExe) { throw "[Prepare] vmrun.exe not found. Install VMware Workstation or add it to PATH." } # ── Validate input: need VMXPath or VMIPAddress ─────────────────────────────── if ($VMXPath -eq '' -and $VMIPAddress -eq '') { throw "[Prepare] Provide -VMXPath (auto power-on + IP detection) or -VMIPAddress (or both)." } # ── Power on VM and detect IP via VMXPath ──────────────────────────────────── $vmWasPoweredOn = $false if ($VMXPath -ne '') { if (-not (Test-Path $VMXPath)) { throw "[Prepare] VMX not found: '$VMXPath'" } # Check if already running $runningList = @(& $vmrunExe list 2>&1 | Where-Object { $_ -match '\.vmx$' } | ForEach-Object { $_.Trim() }) if ($runningList -notcontains $VMXPath) { Write-Host "[Prepare] VM not running — starting: $VMXPath" & $vmrunExe start $VMXPath if ($LASTEXITCODE -ne 0) { throw "[Prepare] vmrun start exited $LASTEXITCODE." } $vmWasPoweredOn = $true Write-Host "[Prepare] VM started. Waiting for guest OS to initialize..." } else { Write-Host "[Prepare] VM already running: $VMXPath" } # Auto-detect IP if not supplied if ($VMIPAddress -eq '') { Write-Host "[Prepare] Detecting guest IP via VMware Tools (getGuestIPAddress -wait)..." $detectedIP = (& $vmrunExe getGuestIPAddress $VMXPath -wait 2>&1).Trim() if ($detectedIP -notmatch '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') { throw "[Prepare] Could not get guest IP. Got: '$detectedIP'. Ensure VMware Tools is installed." } $VMIPAddress = $detectedIP Write-Host "[Prepare] Guest IP: $VMIPAddress" -ForegroundColor Green } } function Assert-Step { param( [Parameter(Mandatory)] [string] $StepName, [Parameter(Mandatory)] [string] $Description, [Parameter(Mandatory)] [scriptblock] $Test ) $ok = $false try { $ok = [bool](& $Test) } catch { throw "[Prepare/$StepName] Validation threw on '$Description': $_" } if (-not $ok) { throw "[Prepare/$StepName] Validation failed: $Description" } Write-Host " [OK] $Description" -ForegroundColor Green } $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path # Pre-flight validation Assert-Step 'PreFlight' 'script directory resolved' { Test-Path $scriptDir -PathType Container } Assert-Step 'PreFlight' 'Install-CIToolchain-WinBuild2022.ps1 present alongside this script' { Test-Path (Join-Path $scriptDir 'Install-CIToolchain-WinBuild2022.ps1') -PathType Leaf } Assert-Step 'PreFlight' "VMIPAddress octets in 0..255" { $parts = $VMIPAddress.Split('.') | ForEach-Object { [int]$_ } ($parts.Count -eq 4) -and -not ($parts | Where-Object { $_ -lt 0 -or $_ -gt 255 }) } # WinRM readiness — retry loop (longer timeout if VM was just powered on by this script) $winrmTimeoutSec = if ($vmWasPoweredOn) { 180 } else { 20 } Write-Host "[Prepare] Waiting for WinRM on $VMIPAddress`:5986 (timeout ${winrmTimeoutSec}s)..." $winrmDeadline = [datetime]::UtcNow.AddSeconds($winrmTimeoutSec) $winrmReady = $false while ([datetime]::UtcNow -lt $winrmDeadline) { if (Test-TcpPort -ComputerName $VMIPAddress -Port 5986 -TimeoutMs 5000) { $winrmReady = $true; break } Start-Sleep -Seconds 5 Write-Host " ... waiting for WinRM..." } Assert-Step 'PreFlight' "VMIPAddress $VMIPAddress reachable on TCP/5986" { $winrmReady } # Prompt for BuildPassword if not supplied — never use a hardcoded default. if ($BuildPassword -eq '') { Write-Host "[Prepare] No -BuildPassword supplied. Enter password for the CI build account inside the VM:" $secPwd = Read-Host -Prompt " BuildPassword" -AsSecureString $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd) $BuildPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } # ── Build PSCredential ──────────────────────────────────────────────────────── if ($AdminPassword -eq '') { Write-Host "[Prepare] Enter administrator credentials for the template VM ($VMIPAddress):" $credential = Get-Credential -UserName $AdminUsername -Message "Template VM administrator ($VMIPAddress)" } else { $secPwd = ConvertTo-SecureString $AdminPassword -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential($AdminUsername, $secPwd) } $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck # ── Configure HOST-side WinRM client (TrustedHosts for HTTPS Basic auth) ────── # HTTPS/5986 does not require AllowUnencrypted on the host side. Only TrustedHosts # needs appending so PS accepts the non-domain VM. Restored in the finally block. $prevTrustedHosts = $null try { $prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value # Add VM IP to TrustedHosts if not already present if ($prevTrustedHosts -ne '*' -and $prevTrustedHosts -notlike "*$VMIPAddress*") { $newTrusted = if ($prevTrustedHosts -eq '') { $VMIPAddress } else { "$prevTrustedHosts,$VMIPAddress" } Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newTrusted -Force -ErrorAction Stop } Write-Host "[Prepare] Host TrustedHosts configured." Assert-Step 'HostWinRM' "TrustedHosts includes $VMIPAddress (or '*')" { $th = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value $th -eq '*' -or $th -like "*$VMIPAddress*" } } catch { Write-Warning "[Prepare] Could not configure host WinRM TrustedHosts (need elevation?)." Write-Warning "Run once in an elevated PowerShell on the HOST:" Write-Warning " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force" Write-Warning "Then re-run this script." exit 1 } # ── Open session (Test-WSMan skipped — PS 5.1 Test-WSMan has no -SessionOption for HTTPS) ── # New-PSSession with -SkipCACheck handles cert validation for self-signed lab cert. Write-Host "[Prepare] Opening PSSession to $VMIPAddress`:5986 (HTTPS)..." $session = try { New-PSSession ` -ComputerName $VMIPAddress ` -Credential $credential ` -SessionOption $sessionOptions ` -UseSSL ` -Port 5986 ` -Authentication Basic ` -ErrorAction Stop } catch { Write-Error @" [Prepare] Cannot open PSSession to $VMIPAddress via WinRM HTTPS (port 5986). Ensure: - The VM is running and on VMnet8 (NAT) with internet access - WinRM HTTPS is enabled inside the VM — Deploy-WinBuild2022.ps1 post-install.ps1 must have run successfully (creates self-signed cert + HTTPS listener on 5986) - Windows Firewall allows port 5986 inbound (or firewall is disabled on the guest) - The IP is correct (check ipconfig inside the VM) Error: $_ "@ exit 1 } try { # ── Copy Install-CIToolchain-WinBuild2022.ps1 into the VM ─────────────────────────────── $setupScript = Join-Path $scriptDir 'Install-CIToolchain-WinBuild2022.ps1' $guestScriptPath = 'C:\CI\Install-CIToolchain-WinBuild2022.ps1' Write-Host "[Prepare] Ensuring C:\CI exists on guest..." Invoke-Command -Session $session -ScriptBlock { if (-not (Test-Path 'C:\CI')) { New-Item -ItemType Directory -Path 'C:\CI' -Force | Out-Null } } Assert-Step 'GuestPrep' 'C:\CI exists on guest' { Invoke-Command -Session $session -ScriptBlock { Test-Path 'C:\CI' -PathType Container } } Write-Host "[Prepare] Copying Install-CIToolchain-WinBuild2022.ps1 -> guest $guestScriptPath (UTF-8 BOM)..." # Do NOT use Copy-Item -ToSession: it binary-copies the file. # If the source file has no UTF-8 BOM, PowerShell 5.1 on the guest reads it as # Windows-1252, misinterpreting multi-byte UTF-8 sequences (e.g. em dash U+2014 # encodes as E2 80 94; byte 0x94 in Win-1252 = curly right-quote, which PS 5.1 # treats as a string terminator → parse errors throughout the script). # # Fix: read content on host as Unicode string, transmit via WinRM (XML/Unicode), # write on guest with explicit UTF-8 BOM via System.Text.UTF8Encoding($true). # PS 5.1 detects the BOM and reads the file as UTF-8 correctly. $scriptContent = [System.IO.File]::ReadAllText($setupScript, [System.Text.Encoding]::UTF8) Invoke-Command -Session $session -ScriptBlock { param($content, $path) $utf8Bom = New-Object System.Text.UTF8Encoding($true) # $true = emit BOM [System.IO.File]::WriteAllText($path, $content, $utf8Bom) } -ArgumentList $scriptContent, $guestScriptPath # Validation — file present and large enough (BOM transfer changes byte count vs host) $hostSize = (Get-Item $setupScript).Length Assert-Step 'GuestPrep' "guest script present at $guestScriptPath" { Invoke-Command -Session $session -ScriptBlock { param($p) Test-Path $p -PathType Leaf } -ArgumentList $guestScriptPath } Assert-Step 'GuestPrep' "guest script size reasonable (host=$hostSize bytes ±6 for BOM)" { $remoteSize = Invoke-Command -Session $session -ScriptBlock { param($p) (Get-Item $p -ErrorAction SilentlyContinue).Length } -ArgumentList $guestScriptPath # UTF-8 BOM adds 3 bytes; allow small variance [int64]$remoteSize -ge ([int64]$hostSize - 6) -and [int64]$remoteSize -le ([int64]$hostSize + 6) } # ── Build argument list for Install-CIToolchain-WinBuild2022.ps1 ─────────────────────── $setupArgs = @{ BuildPassword = $BuildPassword BuildUsername = $BuildUsername DotNetSdkVersion = $DotNetSdkVersion AdminPassword = $AdminPassword Pwsh7Version = $Pwsh7Version NSISVersion = $NSISVersion CMakeVersion = $CMakeVersion NodeJSVersion = $NodeJSVersion WiXVersion = $WiXVersion GhCliVersion = $GhCliVersion VcpkgRef = $VcpkgRef } if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true } if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true } if ($SkipTier2) { $setupArgs['SkipTier2'] = $true } # ── Run Install-CIToolchain-WinBuild2022.ps1 inside the VM ────────────────────────────── # Setup may return exit 3010 (ERROR_SUCCESS_REBOOT_REQUIRED) when Windows # Update applied patches that need a reboot. We loop: reboot guest, wait for # WinRM, reopen session, re-invoke Setup. Steps 1-5 are idempotent (~30 s); # Step 6 picks up where the previous run left off (no leftover updates → # exit 0). Hard cap on iterations to avoid an infinite loop. $maxWuIterations = 10 $rebootDeadlineMin = 20 function Invoke-GuestSetup { param([Parameter(Mandatory)] $Session, [Parameter(Mandatory)] [string] $ScriptPath, [Parameter(Mandatory)] [hashtable] $ScriptArgs) Invoke-Command -Session $Session -ScriptBlock { param($scriptPath, $scriptArgs) Set-ExecutionPolicy Bypass -Scope Process -Force $exitCode = 0 try { & $scriptPath @scriptArgs $exitCode = $LASTEXITCODE if ($null -eq $exitCode) { $exitCode = 0 } } catch { Write-Error $_ $exitCode = 1 } return $exitCode } -ArgumentList $ScriptPath, $ScriptArgs } function Wait-GuestWinRMReady { param([Parameter(Mandatory)] [string] $IP, [Parameter(Mandatory)] $Cred, [Parameter(Mandatory)] $SessionOptions, [int] $TimeoutMin = 20) $deadline = (Get-Date).AddMinutes($TimeoutMin) while ((Get-Date) -lt $deadline) { Start-Sleep -Seconds 10 try { # Test-WSMan has no -SessionOption in PS 5.1 — use New-PSSession as probe. $probe = New-PSSession -ComputerName $IP -Port 5986 -UseSSL ` -SessionOption $SessionOptions -Credential $Cred ` -Authentication Basic -ErrorAction Stop Remove-PSSession $probe -ErrorAction SilentlyContinue return $true } catch { Write-Verbose "WinRM probe attempt failed." } } return $false } Write-Host "[Prepare] Running Install-CIToolchain-WinBuild2022.ps1 inside VM (this will take a while)..." for ($iter = 1; $iter -le $maxWuIterations; $iter++) { Write-Host "[Prepare] Setup iteration $iter/$maxWuIterations" Write-Host "[Prepare] Output from guest:" Write-Host "------------------------------------------------------------" $result = $null try { $result = Invoke-GuestSetup -Session $session -ScriptPath $guestScriptPath -ScriptArgs $setupArgs } catch [System.Management.Automation.Remoting.PSRemotingTransportException] { # VM rebooted mid-update (WU triggered OS restart before Setup could return 3010). # Treat as reboot-required: close dead session, fall through to the reboot-wait block. Write-Host "[Prepare] WinRM transport error — VM rebooted mid-update. Treating as reboot-required..." Remove-PSSession $session -ErrorAction SilentlyContinue $session = $null $result = 3010 } Write-Host "------------------------------------------------------------" Write-Host "[Prepare] Iteration $iter exit code: $result" if ($result -eq 0) { break } if ($result -ne 3010) { throw "Install-CIToolchain-WinBuild2022.ps1 exited with code $result" } # exit 3010 (or transport error treated as 3010) → WU needs reboot. # Reboot guest if session still alive, wait for WinRM, reopen session, loop. if ($iter -eq $maxWuIterations) { throw "Windows Update did not converge after $maxWuIterations iterations (still returning 3010)" } Write-Host "[Prepare] WU applied patches → reboot required. Rebooting guest..." if ($session) { try { Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force } -ErrorAction SilentlyContinue } catch { Write-Verbose "Reboot command sent; session may have already dropped." } Remove-PSSession $session -ErrorAction SilentlyContinue } Start-Sleep -Seconds 30 # let WinRM tear down before polling Write-Host "[Prepare] Waiting for guest WinRM to come back (max $rebootDeadlineMin min)..." if (-not (Wait-GuestWinRMReady -IP $VMIPAddress -Cred $credential ` -SessionOptions $sessionOptions -TimeoutMin $rebootDeadlineMin)) { throw "Guest did not come back online within $rebootDeadlineMin min after WU reboot" } Write-Host "[Prepare] Guest WinRM ready, reopening session..." $session = New-PSSession -ComputerName $VMIPAddress -Credential $credential ` -SessionOption $sessionOptions -UseSSL -Port 5986 ` -Authentication Basic -ErrorAction Stop } # ── Install CI-StaticIp scheduled task (ip_pool guestinfo static IP) ──────── $staticIpScriptPath = Join-Path $scriptDir 'guest-setup\windows\ci-static-ip.ps1' if (Test-Path $staticIpScriptPath -PathType Leaf) { Write-Host "[Prepare] Installing CI-StaticIp scheduled task on guest..." $staticIpContent = Get-Content -LiteralPath $staticIpScriptPath -Raw Invoke-Command -Session $session -ScriptBlock { param([string]$ScriptContent) Set-Content -LiteralPath 'C:\CI\ci-static-ip.ps1' -Value $ScriptContent ` -Encoding UTF8 -Force $action = New-ScheduledTaskAction ` -Execute 'powershell.exe' ` -Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\CI\ci-static-ip.ps1' $trigger = New-ScheduledTaskTrigger -AtStartup $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' ` -LogonType ServiceAccount -RunLevel Highest Register-ScheduledTask -TaskName 'CI-StaticIp' -Action $action ` -Trigger $trigger -Principal $principal -Force | Out-Null } -ArgumentList $staticIpContent Write-Host "[Prepare] CI-StaticIp task registered." -ForegroundColor Green } else { Write-Warning "[Prepare] guest-setup\windows\ci-static-ip.ps1 not found alongside this script — skipping CI-StaticIp task. ip_pool feature will not work for this template." } # ── Post-setup remote validation ────────────────────────────────────────── Write-Host "[Prepare] Running post-setup validation against guest..." $guestState = Invoke-Command -Session $session -ScriptBlock { param($BuildUser) $msbuildPath = 'C:\Program Files (x86)\Microsoft Visual Studio\2026\BuildTools\MSBuild\Current\Bin\MSBuild.exe' $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH','User') [PSCustomObject]@{ WinRMRunning = (Get-Service WinRM -ErrorAction SilentlyContinue).Status -eq 'Running' UserExists = [bool](Get-LocalUser -Name $BuildUser -ErrorAction SilentlyContinue) UserIsAdmin = [bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUser) UACDisabled = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA -eq 0 FirewallOff = -not (Get-NetFirewallProfile | Where-Object Enabled -eq $true) DotNetVersion = (Get-Command dotnet -ErrorAction SilentlyContinue) | ForEach-Object { (& $_.Source --version 2>&1) } PythonExists = Test-Path 'C:\Python\python.exe' -PathType Leaf MSBuildExists = Test-Path $msbuildPath -PathType Leaf CIDirsPresent = (Test-Path 'C:\CI\build') -and (Test-Path 'C:\CI\output') -and (Test-Path 'C:\CI\scripts') StaticIpTask = [bool](Get-ScheduledTask -TaskName 'CI-StaticIp' -ErrorAction SilentlyContinue) StaticIpScript = Test-Path 'C:\CI\ci-static-ip.ps1' -PathType Leaf } } -ArgumentList $BuildUsername Assert-Step 'PostSetup' 'guest WinRM Running' { $guestState.WinRMRunning } Assert-Step 'PostSetup' "guest user '$BuildUsername' exists" { $guestState.UserExists } Assert-Step 'PostSetup' "guest user '$BuildUsername' is admin" { $guestState.UserIsAdmin } Assert-Step 'PostSetup' 'guest UAC disabled' { $guestState.UACDisabled } Assert-Step 'PostSetup' 'guest firewall disabled' { $guestState.FirewallOff } Assert-Step 'PostSetup' 'guest .NET SDK installed' { [bool]$guestState.DotNetVersion } Assert-Step 'PostSetup' 'guest Python installed' { $guestState.PythonExists } Assert-Step 'PostSetup' 'guest MSBuild present' { $guestState.MSBuildExists } Assert-Step 'PostSetup' 'guest C:\CI\{build,output,scripts} present' { $guestState.CIDirsPresent } Assert-Step 'PostSetup' 'guest CI-StaticIp task registered' { $guestState.StaticIpTask } Assert-Step 'PostSetup' 'guest C:\CI\ci-static-ip.ps1 present' { $guestState.StaticIpScript } Write-Host "[Prepare] All post-setup validations passed (.NET $($guestState.DotNetVersion))." -ForegroundColor Green # ── Store credentials in Windows Credential Manager (optional) ──────────── if ($StoreCredential) { Write-Host "[Prepare] Storing CI credentials in Windows Credential Manager (target: '$CredentialTarget')..." if (-not (Get-Module -ListAvailable -Name CredentialManager)) { Write-Host "[Prepare] CredentialManager module not found — installing (CurrentUser scope)..." Install-Module -Name CredentialManager -Scope CurrentUser -Force -ErrorAction Stop } Import-Module CredentialManager -ErrorAction Stop New-StoredCredential ` -Target $CredentialTarget ` -UserName $BuildUsername ` -Password $BuildPassword ` -Persist LocalMachine ` -ErrorAction Stop | Out-Null Write-Host "[Prepare] Credentials stored: target='$CredentialTarget' user='$BuildUsername'." -ForegroundColor Green } Write-Host "[Prepare] All validations passed." -ForegroundColor Green # ── Shutdown + snapshot prompt ───────────────────────────────────────────── # vmrun.exe already located at script start ($vmrunExe) # Auto-discover VMX path via 'vmrun list' while VM is still running (if not known) if ($VMXPath -eq '') { Write-Host "[Prepare] Discovering VMX path from running VMs..." $runningVMs = @(& $vmrunExe list 2>&1 | Where-Object { $_ -match '\.vmx$' } | ForEach-Object { $_.Trim() }) if ($runningVMs.Count -eq 0) { throw "[Prepare] No running VMs found via 'vmrun list'. Is the template VM still up?" } elseif ($runningVMs.Count -eq 1) { $VMXPath = $runningVMs[0] Write-Host "[Prepare] VMX auto-detected: $VMXPath" -ForegroundColor Green } else { # Multiple VMs — prefer one under \CI\Templates\ $ciVMs = @($runningVMs | Where-Object { $_ -like '*\CI\Templates\*' }) if ($ciVMs.Count -eq 1) { $VMXPath = $ciVMs[0] Write-Host "[Prepare] VMX auto-detected (CI template): $VMXPath" -ForegroundColor Green } else { Write-Host "[Prepare] Multiple VMs running — select one:" for ($i = 0; $i -lt $runningVMs.Count; $i++) { Write-Host " [$($i+1)] $($runningVMs[$i])" } $idx = [int](Read-Host "[Prepare] Enter number") - 1 if ($idx -lt 0 -or $idx -ge $runningVMs.Count) { throw "[Prepare] Invalid selection." } $VMXPath = $runningVMs[$idx] } } } if (-not (Test-Path $VMXPath)) { throw "[Prepare] VMX not found on disk: '$VMXPath'" } # Prompt Write-Host "------------------------------------------------------------" Write-Host "" $choice = Read-Host "[Prepare] Shut down VM and create snapshot '$SnapshotName'? [Y/N]" if ($choice -match '^[Yy]') { # Reboot VM to flush any pending install state before final snapshot Write-Host "[Prepare] Rebooting VM to finalise installation state..." try { Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force } -ErrorAction SilentlyContinue } catch { Write-Verbose "Reboot command sent; session may have already dropped." } Remove-PSSession $session -ErrorAction SilentlyContinue $session = $null Start-Sleep -Seconds 30 # let WinRM tear down before polling Write-Host "[Prepare] Waiting for guest WinRM to come back after reboot (max 10 min)..." if (-not (Wait-GuestWinRMReady -IP $VMIPAddress -Cred $credential ` -SessionOptions $sessionOptions -TimeoutMin 10)) { throw "[Prepare] Guest did not come back after reboot within 10 min." } Write-Host "[Prepare] Guest WinRM ready after reboot." -ForegroundColor Green $session = New-PSSession -ComputerName $VMIPAddress -Credential $credential ` -SessionOption $sessionOptions -UseSSL -Port 5986 ` -Authentication Basic -ErrorAction Stop # Re-affirm autologin — OOBE/first-boot tasks can reset AutoAdminLogon=0 during the reboot. Write-Host "[Prepare] Re-affirming autologin registry settings..." Invoke-Command -Session $session -ScriptBlock { param($user, $pass) $wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' Set-ItemProperty -Path $wlKey -Name AutoAdminLogon -Value '1' -Type String -Force Set-ItemProperty -Path $wlKey -Name DefaultUserName -Value $user -Type String -Force Set-ItemProperty -Path $wlKey -Name DefaultDomainName -Value '.' -Type String -Force if ($pass -ne '') { Set-ItemProperty -Path $wlKey -Name DefaultPassword -Value $pass -Type String -Force } } -ArgumentList $AdminUsername, $AdminPassword Write-Host "[Prepare] Autologin re-affirmed." -ForegroundColor Green # Send graceful shutdown Write-Host "[Prepare] Sending graceful shutdown to VM..." try { Invoke-Command -Session $session -ScriptBlock { Stop-Computer -Force } -ErrorAction Stop Write-Host "[Prepare] Shutdown command sent." -ForegroundColor Green } catch { Write-Warning "[Prepare] Shutdown command failed (VM may have already shut down): $_" } # Close session — VM is shutting down, no further WinRM needed Remove-PSSession $session -ErrorAction SilentlyContinue # Wait for VM to power off (poll vmrun list) Write-Host "[Prepare] Waiting for VM to power off (timeout 120 s)..." $shutdownDeadline = [datetime]::UtcNow.AddSeconds(120) $poweredOff = $false while ([datetime]::UtcNow -lt $shutdownDeadline) { $runningList = & $vmrunExe list 2>&1 | Out-String if ($runningList -notmatch [regex]::Escape($VMXPath)) { $poweredOff = $true; break } Start-Sleep -Seconds 5 Write-Host " ... waiting for shutdown..." } if (-not $poweredOff) { throw "[Prepare] VM did not power off within 120 s. Check VMware Workstation." } # Check for existing snapshot with same name $existingSnaps = & $vmrunExe listSnapshots $VMXPath 2>&1 | Out-String $doCreate = $true if ($existingSnaps -match [regex]::Escape($SnapshotName)) { Write-Warning "[Prepare] Snapshot '$SnapshotName' already exists." $delChoice = Read-Host "[Prepare] Delete existing snapshot and recreate? [Y/N]" if ($delChoice -match '^[Yy]') { Write-Host "[Prepare] Deleting existing snapshot '$SnapshotName'..." & $vmrunExe deleteSnapshot $VMXPath $SnapshotName if ($LASTEXITCODE -ne 0) { throw "[Prepare] vmrun deleteSnapshot exited $LASTEXITCODE." } Write-Host "[Prepare] Existing snapshot deleted." -ForegroundColor Green } else { Write-Host "[Prepare] Keeping existing snapshot — skipping creation." -ForegroundColor Yellow $doCreate = $false } } # Create snapshot if ($doCreate) { Write-Host "[Prepare] Creating snapshot '$SnapshotName'..." -ForegroundColor Green & $vmrunExe -T ws snapshot $VMXPath $SnapshotName if ($LASTEXITCODE -ne 0) { throw "[Prepare] vmrun snapshot exited $LASTEXITCODE." } Write-Host "[Prepare] Snapshot '$SnapshotName' created successfully." -ForegroundColor Green } Write-Host "" Write-Host "============================================================" -ForegroundColor Green Write-Host " Template VM provisioning complete!" -ForegroundColor Green Write-Host "============================================================" -ForegroundColor Green if ($StoreCredential) { Write-Host " [DONE] Credentials stored in Credential Manager (target: '$CredentialTarget')." -ForegroundColor Green } Write-Host "" } else { Write-Host "" Write-Host "============================================================" -ForegroundColor Green Write-Host " Template VM provisioning complete!" -ForegroundColor Green Write-Host "============================================================" -ForegroundColor Green Write-Host "" Write-Host " MANUAL STEPS REQUIRED:" Write-Host "" Write-Host " 1. Shut down the VM: Start -> Shut down" Write-Host " (VM stays on VMnet8 NAT for internet access during builds)" Write-Host "" Write-Host " 2. Take snapshot in VMware Workstation:" Write-Host " VM -> Snapshot -> Take Snapshot" Write-Host " Name it EXACTLY: $SnapshotName" Write-Host "" if ($StoreCredential) { Write-Host " 3. [DONE] Credentials stored in Credential Manager (target: '$CredentialTarget')." -ForegroundColor Green } else { Write-Host " 3. Store CI credentials on this HOST (run in elevated PowerShell):" Write-Host " .\Prepare-WinBuild2022.ps1 -VMIPAddress $VMIPAddress -StoreCredential" Write-Host " (or manually:)" Write-Host " New-StoredCredential -Target '$CredentialTarget' ``" Write-Host " -UserName '$BuildUsername' -Password '' ``" Write-Host " -Persist LocalMachine" } Write-Host "" } } finally { Remove-PSSession $session -ErrorAction SilentlyContinue # Restore host TrustedHosts to its original value if ($null -ne $prevTrustedHosts) { Set-Item WSMan:\localhost\Client\TrustedHosts -Value $prevTrustedHosts -Force -ErrorAction SilentlyContinue } Write-Host "[Prepare] Host TrustedHosts restored." }