fe4439b95f
Root cause: ssh-keygen -R prints 'Host X not found in F:\CI\State\known_hosts' to stderr when the IP has no prior entry. In PS5.1 with ErrorActionPreference=Stop, native stderr is converted to ErrorRecords before 2>$null can discard them, making the command a terminating error caught by the job's catch block. This caused all 4 concurrent burn-in jobs to fail after ~15s with: Error: Host 192.168.79.XXX not found in F:\CI\State\known_hosts Additionally, 4 concurrent SSH connections writing to the same known_hosts file with no advisory locking risk file corruption under concurrent load. Fix: set SshKnownHostsFile default to '' in Invoke-CIJob, Invoke-RemoteBuild, and Get-BuildArtifacts. When KnownHostsFile='' _Transport.psm1 uses StrictHostKeyChecking=no + UserKnownHostsFile=NUL (permissive mode), consistent with Wait-VMReady and Prepare-LinuxBuild2404.ps1. Ephemeral CI VMs on a local NAT network have no meaningful MITM risk. Remove the ssh-keygen purge block from Invoke-CIJob.ps1 (no longer needed). AGENTS.md: document PS5.1 native stderr / 2>$null interaction (error #12).
188 lines
6.8 KiB
PowerShell
188 lines
6.8 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Copies build artifacts from an ephemeral VM to the host artifact directory.
|
|
|
|
.DESCRIPTION
|
|
Opens a WinRM session to the build VM and uses Copy-Item -FromSession
|
|
to transfer the artifact archive and optional log files.
|
|
Validates that at least one file was successfully transferred.
|
|
Throws on failure so the caller's try/finally can clean up the VM.
|
|
|
|
.PARAMETER IPAddress
|
|
IP of the build VM.
|
|
|
|
.PARAMETER Credential
|
|
PSCredential for the guest build user.
|
|
|
|
.PARAMETER GuestArtifactPath
|
|
Full path to the artifact file or directory inside the VM.
|
|
Example: C:\CI\output\artifacts.zip
|
|
|
|
.PARAMETER HostArtifactDir
|
|
Directory on the host where artifacts will be stored.
|
|
The directory is created if it does not exist.
|
|
Example: F:\CI\Artifacts\run-42
|
|
|
|
.PARAMETER IncludeLogs
|
|
If set, also copies *.log files from the guest work directory
|
|
(C:\CI\build\*.log) alongside the artifact archive.
|
|
|
|
.EXAMPLE
|
|
$cred = Get-Credential
|
|
.\Get-BuildArtifacts.ps1 `
|
|
-IPAddress "192.168.11.101" `
|
|
-Credential $cred `
|
|
-GuestArtifactPath "C:\CI\output\artifacts.zip" `
|
|
-HostArtifactDir "F:\CI\Artifacts\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,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $GuestArtifactPath,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string] $HostArtifactDir,
|
|
|
|
[switch] $IncludeLogs,
|
|
|
|
# Guest OS type — Linux uses SCP 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).
|
|
# Empty string (default) = permissive mode (StrictHostKeyChecking=no, UserKnownHostsFile=NUL).
|
|
[string] $SshKnownHostsFile = ''
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force
|
|
|
|
if ($GuestOS -eq 'Windows' -and $null -eq $Credential) {
|
|
throw "Credential is required when GuestOS is Windows."
|
|
}
|
|
|
|
# ── Linux branch: use scp to transfer artifacts ─────────────────────────────────────────────
|
|
if ($GuestOS -eq 'Linux') {
|
|
Import-Module (Join-Path $PSScriptRoot '_Transport.psm1') -Force
|
|
|
|
if (-not (Test-Path $HostArtifactDir)) {
|
|
New-Item -ItemType Directory -Path $HostArtifactDir -Force | Out-Null
|
|
Write-Host "[Get-BuildArtifacts] Created host artifact directory: $HostArtifactDir"
|
|
}
|
|
|
|
Write-Host "[Get-BuildArtifacts] Transferring artifacts via SCP from $GuestArtifactPath ..."
|
|
|
|
Copy-SshItem `
|
|
-Source "$GuestArtifactPath/." `
|
|
-Destination $HostArtifactDir `
|
|
-IP $IPAddress `
|
|
-User $SshUser `
|
|
-KeyPath $SshKeyPath `
|
|
-KnownHostsFile $SshKnownHostsFile `
|
|
-Direction 'FromGuest' `
|
|
-Recurse
|
|
|
|
$transferred = @(Get-ChildItem -Path $HostArtifactDir -Recurse -File -ErrorAction SilentlyContinue)
|
|
if ($transferred.Count -eq 0) {
|
|
throw "[Get-BuildArtifacts] No files found in $HostArtifactDir after SCP transfer."
|
|
}
|
|
Write-Host "[Get-BuildArtifacts] $($transferred.Count) file(s) collected from Linux guest."
|
|
return
|
|
}
|
|
|
|
# Ensure destination exists on host
|
|
if (-not (Test-Path $HostArtifactDir)) {
|
|
New-Item -ItemType Directory -Path $HostArtifactDir -Force | Out-Null
|
|
Write-Host "[Get-BuildArtifacts] Created host artifact directory: $HostArtifactDir"
|
|
}
|
|
|
|
$sessionOptions = New-CISessionOption
|
|
|
|
Write-Host "[Get-BuildArtifacts] Connecting to VM at $IPAddress..."
|
|
$session = $null
|
|
$attempts = 3
|
|
$delay = 10 # seconds between attempts
|
|
for ($i = 1; $i -le $attempts; $i++) {
|
|
try {
|
|
$session = New-PSSession `
|
|
-ComputerName $IPAddress `
|
|
-Credential $Credential `
|
|
-SessionOption $sessionOptions `
|
|
-UseSSL `
|
|
-Port 5986 `
|
|
-Authentication Basic `
|
|
-ErrorAction Stop
|
|
break
|
|
} catch {
|
|
if ($i -eq $attempts) { throw }
|
|
Write-Warning "[Get-BuildArtifacts] WinRM connect attempt $i/$attempts failed — retrying in ${delay}s... ($_)"
|
|
Start-Sleep -Seconds $delay
|
|
}
|
|
}
|
|
|
|
try {
|
|
# ── Verify artifact exists in guest ──────────────────────────────────
|
|
$guestExists = Invoke-Command -Session $session -ScriptBlock {
|
|
param($path)
|
|
Test-Path $path
|
|
} -ArgumentList $GuestArtifactPath
|
|
|
|
if (-not $guestExists) {
|
|
throw "Artifact not found in guest at: $GuestArtifactPath"
|
|
}
|
|
|
|
# ── Copy main artifact ────────────────────────────────────────────────
|
|
$artifactFileName = Split-Path $GuestArtifactPath -Leaf
|
|
$hostDestPath = Join-Path $HostArtifactDir $artifactFileName
|
|
|
|
Write-Host "[Get-BuildArtifacts] Copying $artifactFileName from guest..."
|
|
Copy-Item -Path $GuestArtifactPath -Destination $hostDestPath -FromSession $session -Force
|
|
|
|
# ── Copy logs if requested ────────────────────────────────────────────
|
|
if ($IncludeLogs) {
|
|
$logFiles = Invoke-Command -Session $session -ScriptBlock {
|
|
Get-ChildItem -Path 'C:\CI\build' -Filter '*.log' -Recurse -ErrorAction SilentlyContinue |
|
|
Select-Object -ExpandProperty FullName
|
|
}
|
|
|
|
foreach ($log in $logFiles) {
|
|
$logDest = Join-Path $HostArtifactDir (Split-Path $log -Leaf)
|
|
Write-Host "[Get-BuildArtifacts] Copying log: $(Split-Path $log -Leaf)"
|
|
Copy-Item -Path $log -Destination $logDest -FromSession $session -Force
|
|
}
|
|
}
|
|
|
|
# ── Validate at least the main artifact landed ────────────────────────
|
|
if (-not (Test-Path $hostDestPath)) {
|
|
throw "Artifact was not found at expected host path after copy: $hostDestPath"
|
|
}
|
|
|
|
$fileItem = Get-Item $hostDestPath
|
|
if ($fileItem.Length -eq 0) {
|
|
throw "Artifact file exists but is empty: $hostDestPath"
|
|
}
|
|
$sizeKB = [math]::Round($fileItem.Length / 1KB, 1)
|
|
|
|
Write-Host "[Get-BuildArtifacts] Artifact collected: $hostDestPath ($sizeKB KB)"
|
|
return $hostDestPath
|
|
}
|
|
finally {
|
|
Remove-PSSession $session -ErrorAction SilentlyContinue
|
|
}
|