Files
local-ci-cd-system/scripts/Invoke-RemoteBuild.ps1
T
Simone b64d6c2c5d feat: stream build output in real-time + log capture and retention
Invoke-RemoteBuild.ps1:
- Custom build command: replace buffered collect-then-print with
  Write-Host streaming inside Invoke-Command (WinRM forwards Write-Host
  in real-time to the host console)
- Set PYTHONUNBUFFERED=1 in the remote session to prevent Python from
  holding lines in its internal buffer
- Same streaming approach for dotnet restore and dotnet build paths
- Return value is now just int exit code instead of hashtable

Invoke-CIJob.ps1:
- Add -LogDir parameter (default F:\CI\Logs)
- Add -LogRetentionDays parameter (default 30)
- Start-Transcript at job start -> F:\CI\Logs\<JobId>\invoke-ci.log
- Stop-Transcript in finally block (guaranteed execution)
- Auto-purge log directories older than LogRetentionDays after each job
2026-05-08 23:56:10 +02:00

203 lines
8.2 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('^(\d{1,3}\.){3}\d{1,3}$')]
[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 `
-ErrorAction Stop
try {
# ── Ensure working directories exist on guest ────────────────────────
Invoke-Command -Session $session -ScriptBlock {
param($workDir, $outputDir)
foreach ($dir in @($workDir, (Split-Path $outputDir -Parent))) {
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
}
# Clean previous build if present
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
}