Files
local-ci-cd-system/scripts/Invoke-RemoteBuild.ps1
T
Simone e0b45fc71d fix(burn-in): create placeholder artifact when build command produces no output dir
Invoke-RemoteBuild.ps1: when BuildCommand succeeds (exit 0) but GuestArtifactSource
(default: dist) does not exist, instead of silently skipping packaging, now creates a
placeholder 'build-ok.txt' in the artifact output dir and zips it. This ensures
Get-BuildArtifacts.ps1 always finds artifacts.zip regardless of whether the build
produced real output files. Correct for burn-in, smoke, and delay-only build commands.

Start-BurnInTest.ps1: revert default BuildCommand to 'ping 127.0.0.1 -n 31 > nul'
(~30s delay, no artifact output) now that the pipeline handles missing dist gracefully.
The previous 'if not exist dist mkdir dist' workaround is no longer needed.
2026-05-12 21:23:40 +02:00

543 lines
25 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Executes a build inside a guest VM (via WinRM). Supports two clone modes:
(1) Host-side clone: copy pre-cloned source via WinRM zip, or
(2) Guest-side clone: clone repo directly in VM via git (§3.3, requires Git)
.DESCRIPTION
Called by Invoke-CIJob.ps1 to orchestrate the build inside the VM.
**Mode 1: Host-side (default)** — Caller provides pre-cloned source:
1. Compresses the source tree on host, copies archive into VM via WinRM
2. Runs dotnet restore + dotnet build inside VM
3. Packages output as C:\CI\output\artifacts.zip
**Mode 2: Guest-side (§3.3, -UseGitClone in Invoke-CIJob)** — Repo cloned in VM:
1. Clone repo directly into VM via git (eliminates host-zip-transfer overhead)
2. Runs dotnet restore + dotnet build inside VM
3. Packages output same as Mode 1
Supply either -HostSourceDir (Mode 1) or -CloneUrl (Mode 2), not both.
In Mode 2, PAT for private repos is read from Credential Manager inside
this script's WinRM session (§1.5: no argv, no log, cleanup at end).
.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
[Mode 1] Full path on HOST to already-cloned repository (host-side clone mode).
Mutually exclusive with -CloneUrl. When omitted, -CloneUrl must be provided.
Example: C:\Users\user\AppData\Local\Temp\ci-src-run-42
.PARAMETER CloneUrl
[Mode 2, §3.3] Git URL to clone directly in VM (guest-side clone mode).
Mutually exclusive with -HostSourceDir. When provided, repo cloned via git inside VM.
PAT (if needed) read from Credential Manager at runtime.
Example: https://gitea.emulab.it/Simone/myrepo.git
.PARAMETER CloneBranch
[Mode 2] Branch to clone. Ignored in Mode 1. Default: main
.PARAMETER CloneCommit
[Mode 2] Specific commit SHA to checkout after clone. Ignored in Mode 1.
.PARAMETER CloneSubmodules
[Mode 2] Pass --recurse-submodules to git clone. Ignored in Mode 1.
.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()]
[System.Management.Automation.PSCredential] $Credential,
# [Mode 1] Host-side clone — pre-cloned source directory on HOST
[ValidateScript({
if ($_ -and -not (Test-Path $_ -PathType Container)) {
throw "HostSourceDir path does not exist: $_"
}
return $true
})]
[string] $HostSourceDir = '',
# [Mode 2, §3.3] Guest-side clone — Git URL for in-VM clone
[string] $CloneUrl = '',
# [Mode 2] Branch to clone (default: main). Ignored if HostSourceDir provided.
[string] $CloneBranch = 'main',
# [Mode 2] Specific commit SHA to checkout. Ignored if HostSourceDir provided.
[string] $CloneCommit = '',
# [Mode 2] Clone with --recurse-submodules. Ignored if HostSourceDir provided.
[switch] $CloneSubmodules,
# [Mode 2] Credential Manager target for Gitea PAT (private repos).
# Store with: New-StoredCredential -Target 'GiteaPAT' -UserName '<user>' -Password '<pat>' -Persist LocalMachine
# Leave empty for public repos (no auth needed).
[string] $GiteaCredentialTarget = 'GiteaPAT',
[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',
# Redirect dotnet NuGet package store and pip download cache to VMware shared
# folders on the host (F:\CI\Cache\NuGet and F:\CI\Cache\pip).
# Requires the template VMX to have been configured by Set-TemplateSharedFolders.ps1
# and VMware Tools HGFS driver present in the guest.
[switch] $UseSharedCache,
# Guest OS type — Linux uses SSH instead of WinRM.
[ValidateSet('Windows', 'Linux')]
[string] $GuestOS = 'Windows',
# SSH private key path (used when GuestOS=Linux).
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
# SSH username (used when GuestOS=Linux).
[string] $SshUser = 'ci_build',
# Persistent known_hosts file for SSH calls (CI jobs).
# Pass an empty string to disable host-key checking (template provisioning only).
[string] $SshKnownHostsFile = 'F:\CI\State\known_hosts',
# Work directory inside the Linux guest.
[string] $GuestLinuxWorkDir = '/opt/ci/build',
# Output directory inside the Linux guest.
[string] $GuestLinuxOutputDir = '/opt/ci/output',
# Extra environment variables injected into the guest before the build command (§6.5).
# Keys must be valid env var names. Values are plain strings (never logged).
[hashtable] $ExtraGuestEnv = @{}
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force
# ── Validate mutually exclusive modes ─────────────────────────────────────
$hostCloneMode = -not [string]::IsNullOrWhiteSpace($HostSourceDir)
$guestCloneMode = -not [string]::IsNullOrWhiteSpace($CloneUrl)
if ($hostCloneMode -and $guestCloneMode) {
throw "Provide either -HostSourceDir (host-side clone) or -CloneUrl (guest-side clone), not both."
}
if (-not $hostCloneMode -and -not $guestCloneMode) {
throw "Provide either -HostSourceDir (host-side clone) or -CloneUrl (guest-side clone)."
}
if ($guestCloneMode -and -not $CloneBranch) {
throw "CloneBranch is required when using -CloneUrl mode."
}
if ($GuestOS -eq 'Windows' -and $null -eq $Credential) {
throw "Credential is required when GuestOS is Windows."
}
if ($GuestOS -eq 'Linux') {
Import-Module (Join-Path $PSScriptRoot '_Transport.psm1') -Force
Write-Host "[Invoke-RemoteBuild] Linux build mode — SSH transport"
# Step 1: Clean work dir (parent permissions handled by template setup)
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile `
-Command "rm -rf '$GuestLinuxWorkDir' && mkdir -p '$GuestLinuxWorkDir'"
if ($hostCloneMode) {
# ── Mode 1 (Linux): tar host source -> scp -> untar in guest ──
Write-Host "[Invoke-RemoteBuild] Packaging host source ($HostSourceDir) ..."
$tarName = "ci-src-$([Guid]::NewGuid().ToString('N').Substring(0,8)).tar.gz"
$hostTar = Join-Path $env:TEMP $tarName
$guestTar = "/tmp/$tarName"
if (Test-Path $hostTar) { Remove-Item $hostTar -Force }
# bsdtar on Windows 10/11 supports -C and gz natively.
& tar -czf $hostTar -C $HostSourceDir .
if ($LASTEXITCODE -ne 0) {
throw "[Invoke-RemoteBuild] tar failed packaging $HostSourceDir (exit $LASTEXITCODE)"
}
try {
Write-Host "[Invoke-RemoteBuild] Copying source archive to guest:$guestTar ..."
Copy-SshItem -Source $hostTar -Destination $guestTar `
-IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile -Direction ToGuest
Write-Host "[Invoke-RemoteBuild] Extracting source in guest ($GuestLinuxWorkDir) ..."
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile `
-Command "tar -xzf '$guestTar' -C '$GuestLinuxWorkDir' && rm -f '$guestTar'"
}
finally {
if (Test-Path $hostTar) { Remove-Item $hostTar -Force -ErrorAction SilentlyContinue }
}
}
else {
# ── Mode 2 (Linux): in-guest git clone ──
$cloneCmd = "git clone --depth 1 --branch '$CloneBranch'"
if ($CloneSubmodules) { $cloneCmd += ' --recurse-submodules' }
$cloneCmd += " '$CloneUrl' '$GuestLinuxWorkDir'" # non-authed URL for logging
Write-Host "[Invoke-RemoteBuild] Cloning $CloneUrl into guest $GuestLinuxWorkDir ..."
# PAT injection via git http.extraHeader — PAT never appears in argv or /proc/environ
$authHeader = ''
if ($GiteaCredentialTarget -ne '') {
try {
Import-Module CredentialManager -ErrorAction Stop
$credObj = Get-StoredCredential -Target $GiteaCredentialTarget -ErrorAction Stop
if ($credObj) {
$b64 = [Convert]::ToBase64String(
[System.Text.Encoding]::UTF8.GetBytes(
"$($credObj.UserName):$($credObj.GetNetworkCredential().Password)"))
$authHeader = "Authorization: Basic $b64"
}
} catch {
Write-Warning "[Invoke-RemoteBuild] Could not load PAT from Credential Manager ($GiteaCredentialTarget) — cloning without auth (public repo?)."
}
}
if ($authHeader -ne '') {
$authCloneCmd = "git -c credential.helper= -c 'http.extraHeader=$authHeader' clone --depth 1 --branch '$CloneBranch'"
if ($CloneSubmodules) { $authCloneCmd += ' --recurse-submodules' }
$authCloneCmd += " '$CloneUrl' '$GuestLinuxWorkDir'"
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile -Command $authCloneCmd
} else {
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile -Command $cloneCmd
}
# Checkout specific commit if requested
if ($CloneCommit -ne '') {
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile `
-Command "git -C '$GuestLinuxWorkDir' checkout '$CloneCommit'"
}
}
# Step 3: Run build command (always cd into workdir first)
# Prepend extra env exports (§6.5) — scoped to this SSH command only
$envPrefix = ''
foreach ($envKey in $ExtraGuestEnv.Keys) {
$envVal = $ExtraGuestEnv[$envKey] -replace "'", "'\''" # escape ' for POSIX single-quoted string
$envPrefix += "export $envKey='$envVal'; "
}
if ($BuildCommand -ne '') {
$buildCmd = "${envPrefix}cd '$GuestLinuxWorkDir' && $BuildCommand"
} else {
$buildCmd = "${envPrefix}cd '$GuestLinuxWorkDir' && make"
}
$displayCmd = if ($BuildCommand -ne '') { $BuildCommand } else { 'make' }
Write-Host "[Invoke-RemoteBuild] Running build (env vars redacted): cd '$GuestLinuxWorkDir' && $displayCmd"
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile -Command $buildCmd
# Step 4: Stage artifact tree under $GuestLinuxOutputDir for SCP collection
$outDir = $GuestLinuxOutputDir
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile `
-Command "rm -rf '$outDir' && mkdir -p '$outDir'"
$artSrc = if ($GuestArtifactSource -ne '') { $GuestArtifactSource } else { 'dist' }
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
-KnownHostsFile $SshKnownHostsFile `
-Command "if [ -d '$GuestLinuxWorkDir/$artSrc' ]; then cp -r '$GuestLinuxWorkDir/$artSrc/.' '$outDir/'; elif [ -e '$GuestLinuxWorkDir/$artSrc' ]; then cp '$GuestLinuxWorkDir/$artSrc' '$outDir/'; else echo '[Invoke-RemoteBuild] WARNING: artifact source not found: $artSrc' >&2; fi"
Write-Host "[Invoke-RemoteBuild] Linux build complete."
return
}
function Compress-BuildArtifact {
param(
[Parameter(Mandatory)]
[string] $Path,
[Parameter(Mandatory)]
[string] $DestinationPath
)
$7zip = 'C:\Program Files\7-Zip\7z.exe'
if (Test-Path $7zip) {
Write-Host "[Compress-BuildArtifact] Using 7-Zip (multi-threaded)..."
& $7zip a -mmt=on -mx1 $DestinationPath $Path 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -ne 0) {
throw "7-Zip compression failed (exit $LASTEXITCODE)"
}
}
else {
Write-Host "[Compress-BuildArtifact] 7-Zip not found, using Compress-Archive (fallback)..."
Compress-Archive -Path $Path -DestinationPath $DestinationPath -CompressionLevel Fastest -Force
}
}
$sessionOptions = New-CISessionOption
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
# ── Source preparation: Host clone (compress+transfer) or Guest clone (git) ──
if ($hostCloneMode) {
# Mode 1: Copy pre-cloned source from host to guest via WinRM zip
# 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-BuildArtifact -Path "$HostSourceDir\*" -DestinationPath $hostZip
$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
}
}
else {
# Mode 2 (§3.3): Clone repo directly in guest via git
# PAT (if needed) read from Credential Manager inside session, never logged
Write-Host "[Invoke-RemoteBuild] Cloning repository in guest (§3.3)..."
# Read PAT from host Credential Manager (§1.5: never in argv or logs)
$gitPatUser = $null
$gitPatPass = $null
if (-not [string]::IsNullOrWhiteSpace($GiteaCredentialTarget)) {
try {
$stored = Get-StoredCredential -Target $GiteaCredentialTarget -ErrorAction Stop
$gitPatUser = $stored.UserName
$gitPatPass = $stored.GetNetworkCredential().Password
Write-Host "[Invoke-RemoteBuild] PAT loaded from Credential Manager ($GiteaCredentialTarget)."
} catch {
Write-Warning "[Invoke-RemoteBuild] Could not read PAT from Credential Manager '$GiteaCredentialTarget' — proceeding without auth (public repo only)."
}
}
# Clone in guest via WinRM
Invoke-Command -Session $session -ScriptBlock {
param($cloneUrl, $branch, $commit, $workDir, $patUser, $patPass, $submodules)
Set-Location (Split-Path $workDir -Parent)
$gitArgs = @('clone', '--depth', '1', '--branch', $branch)
if ($submodules) { $gitArgs += '--recurse-submodules' }
$gitArgs += @($cloneUrl, (Split-Path $workDir -Leaf))
# Disable interactive credential prompts — WinRM has no TTY
$env:GIT_TERMINAL_PROMPT = '0'
# Build git config args:
# - credential.helper= (empty) clears GCM and any system credential helper
# - http.extraHeader injects Basic auth without credential helpers, temp files,
# or modifying the clone URL (§1.5: token never in URL, argv display, or logs)
$gitConfigArgs = @('-c', 'credential.helper=')
if ($patUser -and $patPass) {
$b64 = [Convert]::ToBase64String(
[Text.Encoding]::UTF8.GetBytes("${patUser}:${patPass}"))
$gitConfigArgs += @('-c', "http.extraHeader=Authorization: Basic $b64")
}
$gitArgs = $gitConfigArgs + $gitArgs
Write-Host "[Guest] Cloning $cloneUrl (branch: $branch)..."
& git @gitArgs 2>&1 | ForEach-Object { Write-Host "[Guest Clone] $_" }
if ($LASTEXITCODE -ne 0) {
throw "git clone failed in guest (exit $LASTEXITCODE)"
}
# Checkout specific commit if provided
if ($commit) {
Write-Host "[Guest] Checking out commit: $commit"
Set-Location $workDir
& git checkout $commit 2>&1 | ForEach-Object { Write-Host "[Guest Checkout] $_" }
if ($LASTEXITCODE -ne 0) {
throw "git checkout $commit failed in guest (exit $LASTEXITCODE)"
}
}
} -ArgumentList $CloneUrl, $CloneBranch, $CloneCommit, $GuestWorkDir, $gitPatUser, $gitPatPass, $CloneSubmodules.IsPresent
Write-Host "[Invoke-RemoteBuild] Guest clone complete."
}
# ── 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, $useCache, $extraEnv)
# Unbuffered Python output — lines appear in real-time instead of being held in buffer
$env:PYTHONUNBUFFERED = '1'
if ($useCache) {
$env:PIP_CACHE_DIR = '\\vmware-host\Shared Folders\pip-cache'
}
# Inject extra env vars (§6.5 secret injection)
foreach ($pair in $extraEnv.GetEnumerator()) {
[System.Environment]::SetEnvironmentVariable($pair.Key, $pair.Value, 'Process')
}
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
if (-not (Test-Path $srcPath)) {
# No artifact source — create a placeholder zip so that
# Get-BuildArtifacts succeeds and the full pipeline is exercised.
# This is expected for burn-in / smoke builds that produce no output.
Write-Host "[Build] Artifact source '$srcPath' not found — creating placeholder artifact."
$placeholder = Join-Path $archiveDir 'build-ok.txt'
'No artifact source directory. Build command completed successfully.' |
Out-File -FilePath $placeholder -Encoding UTF8
Compress-Archive -Path $placeholder -DestinationPath $artifactZip -Force
}
else {
$7zip = 'C:\Program Files\7-Zip\7z.exe'
if (Test-Path $7zip) {
Write-Host "[Build] Compressing artifacts with 7-Zip..."
& $7zip a -mmt=on -mx1 $artifactZip $srcPath 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed" }
}
else {
Compress-Archive -Path $srcPath -DestinationPath $artifactZip -Force
}
}
}
return $exit
} -ArgumentList $GuestWorkDir, $BuildCommand, $GuestArtifactZip, $GuestArtifactSource, $UseSharedCache.IsPresent, $ExtraGuestEnv
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, $useCache)
if ($useCache) {
$env:NUGET_PACKAGES = '\\vmware-host\Shared Folders\nuget-cache'
}
Set-Location $workDir
dotnet restore 2>&1 | ForEach-Object { Write-Host $_ }
return $LASTEXITCODE
} -ArgumentList $GuestWorkDir, $UseSharedCache.IsPresent
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, $useCache)
if ($useCache) {
$env:NUGET_PACKAGES = '\\vmware-host\Shared Folders\nuget-cache'
}
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
}
if (-not (Test-Path $outputDir)) {
Write-Host "[Build] Output dir '$outputDir' not found — skipping packaging."
}
else {
$7zip = 'C:\Program Files\7-Zip\7z.exe'
if (Test-Path $7zip) {
Write-Host "[Build] Compressing output with 7-Zip..."
& $7zip a -mmt=on -mx1 $artifactZip "$outputDir\*" 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed" }
}
else {
Compress-Archive -Path "$outputDir\*" -DestinationPath $artifactZip -Force
}
}
}
return $exit
} -ArgumentList $GuestWorkDir, $Configuration, $GuestArtifactZip, $UseSharedCache.IsPresent
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
}