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
This commit is contained in:
@@ -108,7 +108,15 @@ param(
|
|||||||
|
|
||||||
[string] $GuestCredentialTarget = 'BuildVMGuest',
|
[string] $GuestCredentialTarget = 'BuildVMGuest',
|
||||||
|
|
||||||
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe'
|
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
|
||||||
|
|
||||||
|
# Directory for job execution logs. Each job gets its own sub-directory:
|
||||||
|
# $LogDir\$JobId\invoke-ci.log
|
||||||
|
[string] $LogDir = 'F:\CI\Logs',
|
||||||
|
|
||||||
|
# Days to retain job logs. Directories older than this are purged at job end.
|
||||||
|
# Set to 0 to disable retention/purge.
|
||||||
|
[int] $LogRetentionDays = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
@@ -152,6 +160,19 @@ $hostCloneDir = Join-Path $env:TEMP "ci-src-$JobId"
|
|||||||
$cloneVmxPath = $null
|
$cloneVmxPath = $null
|
||||||
$startTime = Get-Date
|
$startTime = Get-Date
|
||||||
|
|
||||||
|
# ── Start transcript ──────────────────────────────────────────────────────────
|
||||||
|
$transcriptStarted = $false
|
||||||
|
try {
|
||||||
|
$jobLogDir = Join-Path $LogDir $JobId
|
||||||
|
if (-not (Test-Path $jobLogDir)) { New-Item -ItemType Directory -Path $jobLogDir -Force | Out-Null }
|
||||||
|
$transcriptPath = Join-Path $jobLogDir 'invoke-ci.log'
|
||||||
|
Start-Transcript -Path $transcriptPath -Force -ErrorAction Stop
|
||||||
|
$transcriptStarted = $true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "[Invoke-CIJob] Could not start transcript: $_"
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "============================================================"
|
Write-Host "============================================================"
|
||||||
Write-Host "[Invoke-CIJob] Job : $JobId"
|
Write-Host "[Invoke-CIJob] Job : $JobId"
|
||||||
Write-Host "[Invoke-CIJob] Repository : $RepoUrl"
|
Write-Host "[Invoke-CIJob] Repository : $RepoUrl"
|
||||||
@@ -276,6 +297,22 @@ finally {
|
|||||||
Write-Host "[Cleanup] Removing host source clone ($hostCloneDir)..."
|
Write-Host "[Cleanup] Removing host source clone ($hostCloneDir)..."
|
||||||
Remove-Item $hostCloneDir -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $hostCloneDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Stop transcript ───────────────────────────────────────────────────
|
||||||
|
if ($transcriptStarted) {
|
||||||
|
try { Stop-Transcript -ErrorAction SilentlyContinue } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Log retention: purge directories older than $LogRetentionDays days ─
|
||||||
|
if ($LogRetentionDays -gt 0 -and (Test-Path $LogDir)) {
|
||||||
|
$cutoff = (Get-Date).AddDays(-$LogRetentionDays)
|
||||||
|
Get-ChildItem -Path $LogDir -Directory -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.LastWriteTime -lt $cutoff } |
|
||||||
|
ForEach-Object {
|
||||||
|
Remove-Item $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "[Cleanup] Purged old log dir: $($_.FullName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|||||||
@@ -132,13 +132,16 @@ try {
|
|||||||
if ($BuildCommand -ne '') {
|
if ($BuildCommand -ne '') {
|
||||||
# ── Custom build command (e.g. python, msbuild, cmake…) ──────────
|
# ── Custom build command (e.g. python, msbuild, cmake…) ──────────
|
||||||
Write-Host "[Invoke-RemoteBuild] Running build command: $BuildCommand"
|
Write-Host "[Invoke-RemoteBuild] Running build command: $BuildCommand"
|
||||||
$buildResult = Invoke-Command -Session $session -ScriptBlock {
|
$buildExit = Invoke-Command -Session $session -ScriptBlock {
|
||||||
param($workDir, $cmd, $artifactZip, $artifactSrc)
|
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
|
Set-Location $workDir
|
||||||
$output = Invoke-Expression "$cmd 2>&1" | ForEach-Object { "$_" }
|
# Stream each output line via Write-Host — WinRM forwards Write-Host in real-time
|
||||||
$buildExit = $LASTEXITCODE
|
& cmd /c "$cmd 2>&1" | ForEach-Object { Write-Host $_ }
|
||||||
|
$exit = $LASTEXITCODE
|
||||||
|
|
||||||
if ($buildExit -eq 0) {
|
if ($exit -eq 0) {
|
||||||
$archiveDir = Split-Path $artifactZip -Parent
|
$archiveDir = Split-Path $artifactZip -Parent
|
||||||
if (-not (Test-Path $archiveDir)) {
|
if (-not (Test-Path $archiveDir)) {
|
||||||
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
|
||||||
@@ -147,38 +150,36 @@ try {
|
|||||||
Compress-Archive -Path $srcPath -DestinationPath $artifactZip -Force
|
Compress-Archive -Path $srcPath -DestinationPath $artifactZip -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
return @{ ExitCode = $buildExit; Output = ($output -join "`n") }
|
return $exit
|
||||||
} -ArgumentList $GuestWorkDir, $BuildCommand, $GuestArtifactZip, $GuestArtifactSource
|
} -ArgumentList $GuestWorkDir, $BuildCommand, $GuestArtifactZip, $GuestArtifactSource
|
||||||
|
|
||||||
Write-Host $buildResult.Output
|
if ($buildExit -ne 0) {
|
||||||
if ($buildResult.ExitCode -ne 0) {
|
throw "Build command failed (exit $buildExit)"
|
||||||
throw "Build command failed (exit $($buildResult.ExitCode))"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
# ── Default: dotnet restore + dotnet build ────────────────────────
|
# ── Default: dotnet restore + dotnet build ────────────────────────
|
||||||
Write-Host "[Invoke-RemoteBuild] Running dotnet restore..."
|
Write-Host "[Invoke-RemoteBuild] Running dotnet restore..."
|
||||||
$restoreResult = Invoke-Command -Session $session -ScriptBlock {
|
$restoreExit = Invoke-Command -Session $session -ScriptBlock {
|
||||||
param($workDir)
|
param($workDir)
|
||||||
Set-Location $workDir
|
Set-Location $workDir
|
||||||
$output = dotnet restore 2>&1
|
dotnet restore 2>&1 | ForEach-Object { Write-Host $_ }
|
||||||
return @{ ExitCode = $LASTEXITCODE; Output = ($output -join "`n") }
|
return $LASTEXITCODE
|
||||||
} -ArgumentList $GuestWorkDir
|
} -ArgumentList $GuestWorkDir
|
||||||
|
|
||||||
Write-Host $restoreResult.Output
|
if ($restoreExit -ne 0) {
|
||||||
if ($restoreResult.ExitCode -ne 0) {
|
throw "dotnet restore failed (exit $restoreExit)"
|
||||||
throw "dotnet restore failed (exit $($restoreResult.ExitCode))"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "[Invoke-RemoteBuild] Running dotnet build (configuration: $Configuration)..."
|
Write-Host "[Invoke-RemoteBuild] Running dotnet build (configuration: $Configuration)..."
|
||||||
$buildResult = Invoke-Command -Session $session -ScriptBlock {
|
$buildExit = Invoke-Command -Session $session -ScriptBlock {
|
||||||
param($workDir, $config, $artifactZip)
|
param($workDir, $config, $artifactZip)
|
||||||
Set-Location $workDir
|
Set-Location $workDir
|
||||||
$outputDir = Join-Path $workDir 'bin\CIOutput'
|
$outputDir = Join-Path $workDir 'bin\CIOutput'
|
||||||
$output = dotnet build --configuration $config --output $outputDir 2>&1
|
dotnet build --configuration $config --output $outputDir 2>&1 | ForEach-Object { Write-Host $_ }
|
||||||
$buildExit = $LASTEXITCODE
|
$exit = $LASTEXITCODE
|
||||||
|
|
||||||
if ($buildExit -eq 0) {
|
if ($exit -eq 0) {
|
||||||
$archiveDir = Split-Path $artifactZip -Parent
|
$archiveDir = Split-Path $artifactZip -Parent
|
||||||
if (-not (Test-Path $archiveDir)) {
|
if (-not (Test-Path $archiveDir)) {
|
||||||
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
|
||||||
@@ -186,12 +187,11 @@ try {
|
|||||||
Compress-Archive -Path "$outputDir\*" -DestinationPath $artifactZip -Force
|
Compress-Archive -Path "$outputDir\*" -DestinationPath $artifactZip -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
return @{ ExitCode = $buildExit; Output = ($output -join "`n") }
|
return $exit
|
||||||
} -ArgumentList $GuestWorkDir, $Configuration, $GuestArtifactZip
|
} -ArgumentList $GuestWorkDir, $Configuration, $GuestArtifactZip
|
||||||
|
|
||||||
Write-Host $buildResult.Output
|
if ($buildExit -ne 0) {
|
||||||
if ($buildResult.ExitCode -ne 0) {
|
throw "dotnet build failed (exit $buildExit)"
|
||||||
throw "dotnet build failed (exit $($buildResult.ExitCode))"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user