refactor(template): rename Setup-TemplateVM.ps1 -> Setup-WinBuild2025.ps1
- git mv Setup-TemplateVM.ps1 Setup-WinBuild2025.ps1
- git mv Prepare-TemplateSetup.ps1 -> Prepare-WinBuild2025.ps1 (name already in place)
- Updated all references across the workspace:
template/Prepare-WinBuild2025.ps1 (script copy, assertions, comments)
template/Setup-WinBuild2025.ps1 (self-references in docblock and final message)
README.md (directory tree)
TODO.md (all inline links and task descriptions)
docs/WINDOWS-TEMPLATE-SETUP.md (table, step descriptions, phase header, troubleshooting)
docs/LINUX-TEMPLATE-SETUP.md (Windows equivalent reference)
This commit is contained in:
@@ -0,0 +1,621 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
HOST-side script: delivers and runs Setup-WinBuild2025.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/5985 reachable
|
||||
2. Configures host WinRM client (AllowUnencrypted, TrustedHosts) — restored on exit
|
||||
3. Validates host WinRM client settings applied correctly
|
||||
4. Tests WinRM connectivity (Test-WSMan) — fails fast with actionable message
|
||||
5. Opens PSSession to the VM
|
||||
6. Ensures C:\CI exists on guest; validates creation
|
||||
7. Copies Setup-WinBuild2025.ps1 into the VM; validates file present + size match
|
||||
8. Runs Setup-WinBuild2025.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 WinRM client settings 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 '<your-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 Setup-WinBuild2025.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 Setup-WinBuild2025.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 '<pwd>' -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-WinBuild2025.ps1 `
|
||||
-VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' `
|
||||
-BuildPassword 'StrongCIPass!2026' -StoreCredential
|
||||
|
||||
# Skip Windows Update (already applied):
|
||||
.\Prepare-WinBuild2025.ps1 `
|
||||
-VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' `
|
||||
-SkipWindowsUpdate -StoreCredential
|
||||
|
||||
# Explicit IP (manual start, no VMware Tools required for IP detection):
|
||||
.\Prepare-WinBuild2025.ps1 -VMIPAddress 192.168.79.130 `
|
||||
-BuildPassword 'StrongCIPass!2026' -StoreCredential
|
||||
|
||||
# Both supplied — uses VMXPath for power-on/snapshot, explicit IP skips getGuestIPAddress:
|
||||
.\Prepare-WinBuild2025.ps1 `
|
||||
-VMXPath 'F:\CI\Templates\WinBuild\WinBuild.vmx' -VMIPAddress 192.168.79.130 `
|
||||
-BuildPassword 'StrongCIPass!2026' -StoreCredential
|
||||
#>
|
||||
[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',
|
||||
|
||||
[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'
|
||||
|
||||
# ── 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' 'Setup-WinBuild2025.ps1 present alongside this script' {
|
||||
Test-Path (Join-Path $scriptDir 'Setup-WinBuild2025.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`:5985 (timeout ${winrmTimeoutSec}s)..."
|
||||
$winrmDeadline = [datetime]::UtcNow.AddSeconds($winrmTimeoutSec)
|
||||
$winrmReady = $false
|
||||
while ([datetime]::UtcNow -lt $winrmDeadline) {
|
||||
if (Test-NetConnection -ComputerName $VMIPAddress -Port 5985 `
|
||||
-InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) {
|
||||
$winrmReady = $true; break
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
Write-Host " ... waiting for WinRM..."
|
||||
}
|
||||
Assert-Step 'PreFlight' "VMIPAddress $VMIPAddress reachable on TCP/5985" { $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 (required for Basic auth over HTTP) ──────
|
||||
# AllowUnencrypted is needed for HTTP/5985 Basic auth. We save the prior value
|
||||
# and restore it in the finally block so this script doesn't permanently change
|
||||
# the host security posture.
|
||||
Write-Host "[Prepare] Configuring host WinRM client for unencrypted Basic auth (will be restored on exit)..."
|
||||
$prevAllowUnencrypted = $null
|
||||
$prevTrustedHosts = $null
|
||||
try {
|
||||
$prevAllowUnencrypted = (Get-Item WSMan:\localhost\Client\AllowUnencrypted -ErrorAction Stop).Value
|
||||
$prevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction Stop).Value
|
||||
Set-Item WSMan:\localhost\Client\AllowUnencrypted $true -Force -ErrorAction Stop
|
||||
# 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 WinRM client configured."
|
||||
# Validation
|
||||
Assert-Step 'HostWinRM' 'Client/AllowUnencrypted=true' {
|
||||
(Get-Item WSMan:\localhost\Client\AllowUnencrypted).Value -eq 'true'
|
||||
}
|
||||
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 client settings (need elevation?)."
|
||||
Write-Warning "Run once in an elevated PowerShell on the HOST:"
|
||||
Write-Warning " Set-Item WSMan:\localhost\Client\AllowUnencrypted `$true -Force"
|
||||
Write-Warning " Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force"
|
||||
Write-Warning "Then re-run this script."
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
Write-Host "[Prepare] Testing WinRM connectivity to $VMIPAddress..."
|
||||
try {
|
||||
Test-WSMan -ComputerName $VMIPAddress -Credential $credential `
|
||||
-Authentication Basic -ErrorAction Stop | Out-Null
|
||||
Write-Host "[Prepare] WinRM reachable."
|
||||
}
|
||||
catch {
|
||||
Write-Error @"
|
||||
[Prepare] Cannot reach $VMIPAddress via WinRM.
|
||||
|
||||
Ensure:
|
||||
- The VM is running and on VMnet8 (NAT) with internet access
|
||||
- WinRM is enabled inside the VM (run as Administrator in VM):
|
||||
winrm quickconfig -q
|
||||
Enable-PSRemoting -Force -SkipNetworkProfileCheck
|
||||
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
|
||||
- Windows Firewall allows port 5985 inbound
|
||||
- The IP is correct (check ipconfig inside the VM)
|
||||
|
||||
Error: $_
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Open session ──────────────────────────────────────────────────────────────
|
||||
Write-Host "[Prepare] Opening PSSession..."
|
||||
$session = New-PSSession `
|
||||
-ComputerName $VMIPAddress `
|
||||
-Credential $credential `
|
||||
-SessionOption $sessionOptions `
|
||||
-Authentication Basic `
|
||||
-ErrorAction Stop
|
||||
|
||||
try {
|
||||
# ── Copy Setup-WinBuild2025.ps1 into the VM ───────────────────────────────
|
||||
$setupScript = Join-Path $scriptDir 'Setup-WinBuild2025.ps1'
|
||||
$guestScriptPath = 'C:\CI\Setup-WinBuild2025.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 Setup-WinBuild2025.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 Setup-WinBuild2025.ps1 ───────────────────────
|
||||
$setupArgs = @{
|
||||
BuildPassword = $BuildPassword
|
||||
BuildUsername = $BuildUsername
|
||||
DotNetSdkVersion = $DotNetSdkVersion
|
||||
}
|
||||
if ($SkipWindowsUpdate) { $setupArgs['SkipWindowsUpdate'] = $true }
|
||||
if ($SkipCleanup) { $setupArgs['SkipCleanup'] = $true }
|
||||
|
||||
# ── Run Setup-WinBuild2025.ps1 inside the VM ──────────────────────────────
|
||||
Write-Host "[Prepare] Running Setup-WinBuild2025.ps1 inside VM (this will take a while)..."
|
||||
Write-Host "[Prepare] Output from guest:"
|
||||
Write-Host "------------------------------------------------------------"
|
||||
|
||||
$result = 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 $guestScriptPath, $setupArgs
|
||||
|
||||
Write-Host "------------------------------------------------------------"
|
||||
|
||||
# ── Handle reboot required (exit 3010) ───────────────────────────────────
|
||||
if ($result -eq 3010) {
|
||||
Write-Host ""
|
||||
Write-Warning "*** REBOOT REQUIRED after Windows Update. ***"
|
||||
Write-Warning " The VM will reboot. Wait for it to come back up, then run:"
|
||||
Write-Warning " .\Prepare-WinBuild2025.ps1 -VMIPAddress $VMIPAddress -SkipWindowsUpdate"
|
||||
|
||||
# Reboot the VM
|
||||
Write-Host "[Prepare] Sending reboot command to VM..."
|
||||
Invoke-Command -Session $session -ScriptBlock { Restart-Computer -Force }
|
||||
exit 3010
|
||||
}
|
||||
|
||||
if ($result -ne 0) {
|
||||
throw "Setup-WinBuild2025.ps1 exited with code $result"
|
||||
}
|
||||
|
||||
# ── 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')
|
||||
}
|
||||
} -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 }
|
||||
|
||||
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]') {
|
||||
|
||||
# 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-WinBuild2025.ps1 -VMIPAddress $VMIPAddress -StoreCredential"
|
||||
Write-Host " (or manually:)"
|
||||
Write-Host " New-StoredCredential -Target '$CredentialTarget' ``"
|
||||
Write-Host " -UserName '$BuildUsername' -Password '<your-build-password>' ``"
|
||||
Write-Host " -Persist LocalMachine"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-PSSession $session -ErrorAction SilentlyContinue
|
||||
|
||||
# Restore host WinRM client settings to their original values
|
||||
if ($null -ne $prevAllowUnencrypted) {
|
||||
Set-Item WSMan:\localhost\Client\AllowUnencrypted $prevAllowUnencrypted -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($null -ne $prevTrustedHosts) {
|
||||
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $prevTrustedHosts -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Write-Host "[Prepare] Host WinRM client settings restored."
|
||||
}
|
||||
Reference in New Issue
Block a user