Files
local-ci-cd-system/scripts/Invoke-RemoteBuild.ps1
T
Simone 68cde01c9d security(sprint1): §1.1/1.3/1.4 — WinRM HTTPS/5986, SHA256 pinning infra, IP regex per-octet
§1.4 — Replace loose IP ValidatePattern with per-octet regex in 4 scripts
  (Invoke-CIJob, Invoke-RemoteBuild, Get-BuildArtifacts, Wait-VMReady)

§1.1 — Migrate all WinRM connections from HTTP/5985 to HTTPS/5986
  - Deploy post-install.ps1: remove AllowUnencrypted=true; self-signed cert + HTTPS listener
    already present; firewall rule restricted to 192.168.79.0/24
  - Setup-WinBuild2025.ps1: assert AllowUnencrypted=false
  - Prepare-WinBuild2025.ps1: preflight uses TCP/5986; Test-WSMan -UseSSL -Port 5986;
    New-PSSession -UseSSL -Port 5986; host AllowUnencrypted save/restore removed (not needed for HTTPS)
  - Invoke-RemoteBuild, Get-BuildArtifacts: New-PSSession -UseSSL -Port 5986 -Authentication Basic
  - Wait-VMReady: New-WSManSessionOption + Test-WSMan -Port 5986 -UseSSL
  - Validate-DeployState, Validate-SetupState: Invoke-Command -UseSSL -Port 5986,
    AllowUnencrypted check → false; AllowUnencrypted host-side removed from both
  - Docs: PLAN, README, ARCHITECTURE, BEST-PRACTICES, HOST-SETUP, WINDOWS-TEMPLATE-SETUP,
    LINUX-TEMPLATE-SETUP, TODO updated

§1.3 — SHA256 hash pinning infrastructure
  - Assert-Hash function + $script:Hashes hashtable added to Setup-WinBuild2025.ps1
  - Assert-Hash called after dotnet-install.ps1, python installer, vs_buildtools.exe downloads
  - Hashes initialized to '' (warn-skip); must be filled before next snapshot refresh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 14:46:41 +02:00

204 lines
8.4 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Copies pre-cloned source from host into a build VM and runs dotnet build.
.DESCRIPTION
The caller (Invoke-CIJob.ps1) is responsible for cloning the repository
on the HOST before calling this script. This script then:
1. Compresses the local source tree on the host, copies the archive into
the VM via WinRM (single file = much faster than recursive Copy-Item),
then expands it inside the VM.
2. Runs dotnet restore and dotnet build inside the VM
3. Packages the output as C:\CI\output\artifacts.zip inside the VM
The build VM does NOT need internet access or git. All network I/O happens
on the host before this script is called.
.PARAMETER IPAddress
IP of the build VM (e.g. 192.168.11.101).
.PARAMETER Credential
PSCredential for the build user inside the VM.
Retrieve from Windows Credential Manager in production:
$cred = Get-StoredCredential -Target 'BuildVMGuest'
.PARAMETER HostSourceDir
Full path on the HOST to the already-cloned repository directory.
This directory is copied into the VM as the build working directory.
Example: C:\Users\user\AppData\Local\Temp\ci-src-run-42
.PARAMETER Configuration
MSBuild/dotnet build configuration. Default: Release
.PARAMETER GuestWorkDir
Working directory inside the VM where source will be placed.
Default: C:\CI\build
.PARAMETER GuestArtifactZip
Full path inside the VM where the artifact archive will be written.
Default: C:\CI\output\artifacts.zip
.EXAMPLE
$cred = Get-Credential
.\Invoke-RemoteBuild.ps1 `
-IPAddress "192.168.11.101" `
-Credential $cred `
-HostSourceDir "C:\Temp\ci-src-run-42"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidatePattern('^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$')]
[string] $IPAddress,
[Parameter(Mandatory)]
[System.Management.Automation.PSCredential] $Credential,
[Parameter(Mandatory)]
[ValidateScript({ Test-Path $_ -PathType Container })]
[string] $HostSourceDir,
[string] $Configuration = 'Release',
# Custom build command to run inside the VM (working dir = GuestWorkDir).
# When empty, defaults to: dotnet restore + dotnet build --configuration $Configuration
# Example: 'python build_plugin.py --final'
[string] $BuildCommand = '',
# Glob pattern (relative to GuestWorkDir) of files to include in the artifact zip.
# Used only when BuildCommand is set. Default: collect everything under 'dist'.
# Example: 'dist\**' or 'Contrib\nsInnoUnp\Build\**'
[string] $GuestArtifactSource = 'dist',
[string] $GuestWorkDir = 'C:\CI\build',
[string] $GuestArtifactZip = 'C:\CI\output\artifacts.zip'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ── Session options: skip SSL certificate checks for self-signed (lab use) ──
$sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
Write-Host "[Invoke-RemoteBuild] Connecting to VM at $IPAddress..."
$session = New-PSSession `
-ComputerName $IPAddress `
-Credential $Credential `
-SessionOption $sessionOptions `
-UseSSL `
-Port 5986 `
-Authentication Basic `
-ErrorAction Stop
try {
# ── Ensure working directories exist on guest ────────────────────────
Invoke-Command -Session $session -ScriptBlock {
param($workDir, $outputDir)
# Ensure artifact output parent exists
$outParent = Split-Path $outputDir -Parent
if (-not (Test-Path $outParent)) {
New-Item -ItemType Directory -Path $outParent -Force | Out-Null
}
# Clean and recreate build dir (ephemeral VM, but guard against partial prior run)
if (Test-Path $workDir) { Remove-Item $workDir -Recurse -Force }
New-Item -ItemType Directory -Path $workDir -Force | Out-Null
} -ArgumentList $GuestWorkDir, $GuestArtifactZip
# ── Copy source tree: compress on host, transfer zip, expand in guest ──
# Single-file transfer over WinRM is far faster than recursive Copy-Item.
$hostZip = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.zip'
try {
Write-Host "[Invoke-RemoteBuild] Compressing source on host..."
Compress-Archive -Path "$HostSourceDir\*" -DestinationPath $hostZip -CompressionLevel Fastest -Force
$zipSize = [math]::Round((Get-Item $hostZip).Length / 1MB, 1)
Write-Host "[Invoke-RemoteBuild] Transferring archive to guest ($zipSize MB)..."
$guestZip = 'C:\CI\src-transfer.zip'
Copy-Item -Path $hostZip -Destination $guestZip -ToSession $session -Force
Write-Host "[Invoke-RemoteBuild] Expanding archive in guest..."
Invoke-Command -Session $session -ScriptBlock {
param($zip, $dest)
Expand-Archive -Path $zip -DestinationPath $dest -Force
Remove-Item $zip -Force
} -ArgumentList $guestZip, $GuestWorkDir
Write-Host "[Invoke-RemoteBuild] Source transfer complete."
}
finally {
Remove-Item $hostZip -Force -ErrorAction SilentlyContinue
}
# ── Build ──────────────────────────────────────────────────────────
if ($BuildCommand -ne '') {
# ── Custom build command (e.g. python, msbuild, cmake…) ──────────
Write-Host "[Invoke-RemoteBuild] Running build command: $BuildCommand"
$buildExit = Invoke-Command -Session $session -ScriptBlock {
param($workDir, $cmd, $artifactZip, $artifactSrc)
# Unbuffered Python output — lines appear in real-time instead of being held in buffer
$env:PYTHONUNBUFFERED = '1'
Set-Location $workDir
# Stream each output line via Write-Host — WinRM forwards Write-Host in real-time
& cmd /c "$cmd 2>&1" | ForEach-Object { Write-Host $_ }
$exit = $LASTEXITCODE
if ($exit -eq 0) {
$archiveDir = Split-Path $artifactZip -Parent
if (-not (Test-Path $archiveDir)) {
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
}
$srcPath = Join-Path $workDir $artifactSrc
Compress-Archive -Path $srcPath -DestinationPath $artifactZip -Force
}
return $exit
} -ArgumentList $GuestWorkDir, $BuildCommand, $GuestArtifactZip, $GuestArtifactSource
if ($buildExit -ne 0) {
throw "Build command failed (exit $buildExit)"
}
}
else {
# ── Default: dotnet restore + dotnet build ────────────────────────
Write-Host "[Invoke-RemoteBuild] Running dotnet restore..."
$restoreExit = Invoke-Command -Session $session -ScriptBlock {
param($workDir)
Set-Location $workDir
dotnet restore 2>&1 | ForEach-Object { Write-Host $_ }
return $LASTEXITCODE
} -ArgumentList $GuestWorkDir
if ($restoreExit -ne 0) {
throw "dotnet restore failed (exit $restoreExit)"
}
Write-Host "[Invoke-RemoteBuild] Running dotnet build (configuration: $Configuration)..."
$buildExit = Invoke-Command -Session $session -ScriptBlock {
param($workDir, $config, $artifactZip)
Set-Location $workDir
$outputDir = Join-Path $workDir 'bin\CIOutput'
dotnet build --configuration $config --output $outputDir 2>&1 | ForEach-Object { Write-Host $_ }
$exit = $LASTEXITCODE
if ($exit -eq 0) {
$archiveDir = Split-Path $artifactZip -Parent
if (-not (Test-Path $archiveDir)) {
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
}
Compress-Archive -Path "$outputDir\*" -DestinationPath $artifactZip -Force
}
return $exit
} -ArgumentList $GuestWorkDir, $Configuration, $GuestArtifactZip
if ($buildExit -ne 0) {
throw "dotnet build failed (exit $buildExit)"
}
}
Write-Host "[Invoke-RemoteBuild] Build succeeded. Artifact at $GuestArtifactZip"
}
finally {
Remove-PSSession $session -ErrorAction SilentlyContinue
}