diff --git a/scripts/Invoke-RemoteBuild.ps1 b/scripts/Invoke-RemoteBuild.ps1 index 088e645..710c243 100644 --- a/scripts/Invoke-RemoteBuild.ps1 +++ b/scripts/Invoke-RemoteBuild.ps1 @@ -1,19 +1,27 @@ #Requires -Version 5.1 <# .SYNOPSIS - Copies pre-cloned source from host into a build VM and runs dotnet build. + 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 - 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 + Called by Invoke-CIJob.ps1 to orchestrate the build 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. + **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). @@ -24,10 +32,25 @@ $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. + [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 @@ -55,9 +78,26 @@ param( [Parameter(Mandatory)] [System.Management.Automation.PSCredential] $Credential, - [Parameter(Mandatory)] - [ValidateScript({ Test-Path $_ -PathType Container })] - [string] $HostSourceDir, + # [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, [string] $Configuration = 'Release', @@ -87,6 +127,19 @@ $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." +} + function Compress-BuildArtifact { param( [Parameter(Mandatory)] @@ -135,26 +188,80 @@ try { 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-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." + # ── 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 + } } - 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 Credential Manager (for private repos) + # This runs in the HOST session, but PAT is passed securely to guest session only + $gitPat = $null + try { + # Try to load PAT from Credential Manager target 'git:https://gitea.emulab.it/...' or similar + # For now, we assume public or ssh-key auth; if needed, caller provides CI_GIT_PAT env + # In future: $gitPat = (Get-StoredCredential -Target 'git:...' -ErrorAction SilentlyContinue).GetNetworkCredential().Password + } catch { + Write-Warning "[Invoke-RemoteBuild] Could not read git PAT from Credential Manager (proceeding without auth)." + } + + # Clone in guest via WinRM + Invoke-Command -Session $session -ScriptBlock { + param($cloneUrl, $branch, $commit, $workDir, $pat, $submodules) + # Set PAT as env var (never in argv or logs) + if ($pat) { + $env:GITLAB_PRIVATE_TOKEN = $pat # or appropriate token name for your git server + } + + Set-Location (Split-Path $workDir -Parent) + $gitArgs = @('clone', '--depth', '1', '--branch', $branch) + if ($submodules) { $gitArgs += '--recurse-submodules' } + $gitArgs += @($cloneUrl, (Split-Path $workDir -Leaf)) + + 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)" + } + } + + # Clear PAT from environment (§1.5: cleanup) + if ($env:GITLAB_PRIVATE_TOKEN) { Remove-Item env:GITLAB_PRIVATE_TOKEN -ErrorAction SilentlyContinue } + } -ArgumentList $CloneUrl, $CloneBranch, $CloneCommit, $GuestWorkDir, $gitPat, $CloneSubmodules.IsPresent + + Write-Host "[Invoke-RemoteBuild] Guest clone complete." } # ── Build ──────────────────────────────────────────────────────────