diff --git a/.github/workflows/host-release-2020-vipm-lifecycle-drill.yml b/.github/workflows/host-release-2020-vipm-lifecycle-drill.yml index 2b15408..a9e12d3 100644 --- a/.github/workflows/host-release-2020-vipm-lifecycle-drill.yml +++ b/.github/workflows/host-release-2020-vipm-lifecycle-drill.yml @@ -17,6 +17,11 @@ on: required: false default: false type: boolean + allow_system_account: + description: Allow running on NT AUTHORITY\\SYSTEM for diagnostic-only execution. + required: false + default: false + type: boolean nsis_root: description: Optional NSIS root override. Defaults to repository variable NSIS_ROOT or C:\Program Files (x86)\NSIS. required: false @@ -59,12 +64,29 @@ jobs: run: | $ErrorActionPreference = 'Stop' $reportPath = Join-Path $env:RUNNER_TEMP 'host-release-2020-vipm-machine-preflight.json' - & pwsh -NoProfile -File ./scripts/Assert-InstallerHarnessMachinePreflight.ps1 ` - -ExpectedLabviewYear '2020' ` - -DockerContext 'desktop-linux' ` - -DockerCheckSeverity warning ` - -RequireNonSystemAccount ` - -OutputPath $reportPath + $allowSystemAccountText = [string]'${{ inputs.allow_system_account }}' + $allowSystemAccount = $false + if (-not [string]::IsNullOrWhiteSpace($allowSystemAccountText)) { + try { + $allowSystemAccount = [System.Convert]::ToBoolean($allowSystemAccountText) + } catch { + throw "allow_system_account must be boolean. actual='$allowSystemAccountText'" + } + } + + $preflightArgs = @( + '-NoProfile', + '-File', './scripts/Assert-InstallerHarnessMachinePreflight.ps1', + '-ExpectedLabviewYear', '2020', + '-DockerContext', 'desktop-linux', + '-DockerCheckSeverity', 'warning', + '-OutputPath', $reportPath + ) + if (-not $allowSystemAccount) { + $preflightArgs += '-RequireNonSystemAccount' + } + + & pwsh @preflightArgs if ($LASTEXITCODE -ne 0) { if (Test-Path -LiteralPath $reportPath -PathType Leaf) { Get-Content -LiteralPath $reportPath -Raw | Write-Host @@ -124,17 +146,106 @@ jobs: throw "Host-release 2020 VIPM lifecycle drill failed." } - - name: Upload host-release 2020 VIPM lifecycle artifacts + - name: Prepare host-release 2020 VIPM lifecycle artifact bundle if: always() + id: prepare_host_release_bundle + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + function Copy-PathIfExists { + param( + [Parameter(Mandatory = $true)][string]$SourcePath, + [Parameter(Mandatory = $true)][string]$BundleRoot, + [Parameter(Mandatory = $true)][string]$RelativePath, + [Parameter(Mandatory = $true)][ref]$IncludedPaths, + [Parameter(Mandatory = $true)][ref]$MissingPaths + ) + + if (-not (Test-Path -LiteralPath $SourcePath)) { + $MissingPaths.Value += $SourcePath + return + } + + $targetPath = Join-Path $BundleRoot $RelativePath + $targetParent = Split-Path -Parent $targetPath + if (-not [string]::IsNullOrWhiteSpace($targetParent) -and -not (Test-Path -LiteralPath $targetParent -PathType Container)) { + New-Item -ItemType Directory -Path $targetParent -Force | Out-Null + } + + if (Test-Path -LiteralPath $SourcePath -PathType Container) { + Copy-Item -LiteralPath $SourcePath -Destination $targetPath -Recurse -Force + } else { + Copy-Item -LiteralPath $SourcePath -Destination $targetPath -Force + } + $IncludedPaths.Value += $SourcePath + } + + $bundleRoot = Join-Path $env:RUNNER_TEMP 'host-release-2020-vipm-lifecycle-drill-bundle' + if (Test-Path -LiteralPath $bundleRoot -PathType Container) { + Remove-Item -LiteralPath $bundleRoot -Recurse -Force + } + New-Item -ItemType Directory -Path $bundleRoot -Force | Out-Null + + $reportPath = Join-Path $env:RUNNER_TEMP 'host-release-2020-vipm-lifecycle-drill-report.json' + $preflightPath = Join-Path $env:RUNNER_TEMP 'host-release-2020-vipm-machine-preflight.json' + $iterationRoot = Join-Path $env:RUNNER_TEMP 'host-release-2020-vipm-lifecycle' + $smokeRoot = Join-Path $env:RUNNER_TEMP 'dev-smoke-lvie-2020' + + $includedPaths = @() + $missingPaths = @() + + Copy-PathIfExists -SourcePath $reportPath -BundleRoot $bundleRoot -RelativePath 'host-release-2020-vipm-lifecycle-drill-report.json' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + Copy-PathIfExists -SourcePath $preflightPath -BundleRoot $bundleRoot -RelativePath 'host-release-2020-vipm-machine-preflight.json' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + Copy-PathIfExists -SourcePath $iterationRoot -BundleRoot $bundleRoot -RelativePath 'host-release-2020-vipm-lifecycle' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + Copy-PathIfExists -SourcePath (Join-Path $smokeRoot 'artifacts/workspace-install-latest.json') -BundleRoot $bundleRoot -RelativePath 'dev-smoke-lvie-2020/artifacts/workspace-install-latest.json' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + Copy-PathIfExists -SourcePath (Join-Path $smokeRoot 'labview-icon-editor/builds/status') -BundleRoot $bundleRoot -RelativePath 'dev-smoke-lvie-2020/labview-icon-editor/builds/status' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + Copy-PathIfExists -SourcePath (Join-Path $smokeRoot 'labview-icon-editor/builds/logs') -BundleRoot $bundleRoot -RelativePath 'dev-smoke-lvie-2020/labview-icon-editor/builds/logs' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + Copy-PathIfExists -SourcePath (Join-Path $smokeRoot 'labview-icon-editor/builds/status/workspace-installer-vip-build.json') -BundleRoot $bundleRoot -RelativePath 'dev-smoke-lvie-2020/labview-icon-editor/builds/status/workspace-installer-vip-build.json' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + Copy-PathIfExists -SourcePath (Join-Path $smokeRoot 'labview-icon-editor/builds/logs/vipm-build.log') -BundleRoot $bundleRoot -RelativePath 'dev-smoke-lvie-2020/labview-icon-editor/builds/logs/vipm-build.log' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + Copy-PathIfExists -SourcePath (Join-Path $smokeRoot 'labview-icon-editor/builds/logs/vipm') -BundleRoot $bundleRoot -RelativePath 'dev-smoke-lvie-2020/labview-icon-editor/builds/logs/vipm' -IncludedPaths ([ref]$includedPaths) -MissingPaths ([ref]$missingPaths) + + $manifestPath = Join-Path $bundleRoot 'artifact-manifest.json' + [ordered]@{ + schema_version = '1.0' + generated_at_utc = (Get-Date).ToUniversalTime().ToString('o') + run_id = '${{ github.run_id }}' + run_attempt = '${{ github.run_attempt }}' + branch = '${{ github.ref_name }}' + included_paths = @($includedPaths) + missing_paths = @($missingPaths) + } | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $manifestPath -Encoding utf8 + + "bundle_root=$bundleRoot" >> $env:GITHUB_OUTPUT + Get-Content -LiteralPath $manifestPath -Raw | Write-Host + + - name: Upload host-release 2020 VIPM lifecycle artifacts (primary) + if: always() + id: upload_host_release_primary + continue-on-error: true uses: actions/upload-artifact@v4 with: name: host-release-2020-vipm-lifecycle-drill-report-${{ github.run_id }} - path: | - ${{ runner.temp }}/host-release-2020-vipm-lifecycle-drill-report.json - ${{ runner.temp }}/host-release-2020-vipm-machine-preflight.json - ${{ runner.temp }}/host-release-2020-vipm-lifecycle/** - ${{ runner.temp }}/dev-smoke-lvie-2020/artifacts/workspace-install-latest.json - ${{ runner.temp }}/dev-smoke-lvie-2020/labview-icon-editor/builds/status/workspace-installer-vip-build.json - ${{ runner.temp }}/dev-smoke-lvie-2020/labview-icon-editor/builds/logs/vipm-build.log - ${{ runner.temp }}/dev-smoke-lvie-2020/labview-icon-editor/builds/logs/vipm/** + path: ${{ steps.prepare_host_release_bundle.outputs.bundle_root }} + if-no-files-found: error + + - name: Upload host-release 2020 VIPM lifecycle artifacts (retry) + if: always() && steps.upload_host_release_primary.outcome == 'failure' + id: upload_host_release_retry + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: host-release-2020-vipm-lifecycle-drill-report-${{ github.run_id }}-retry-${{ github.run_attempt }} + path: ${{ steps.prepare_host_release_bundle.outputs.bundle_root }} if-no-files-found: error + + - name: Assert host-release artifact upload completed + if: always() + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $primaryOutcome = [string]'${{ steps.upload_host_release_primary.outcome }}' + $retryOutcome = [string]'${{ steps.upload_host_release_retry.outcome }}' + if ($primaryOutcome -ne 'success' -and $retryOutcome -ne 'success') { + throw "Artifact upload failed on primary and retry attempts." + } diff --git a/scripts/Install-WorkspaceFromManifest.ps1 b/scripts/Install-WorkspaceFromManifest.ps1 index 36a1eaf..e8e2d8d 100644 --- a/scripts/Install-WorkspaceFromManifest.ps1 +++ b/scripts/Install-WorkspaceFromManifest.ps1 @@ -758,9 +758,38 @@ function Invoke-RunnerCliPplCapabilityCheck { ) $result.command = @($commandArgs) - $runnerCliOutputLines = @( - & $RunnerCliPath @commandArgs 2>&1 | ForEach-Object { [string]$_ } - ) + $runnerCliOutputLines = @() + $runnerCliInvocationError = $null + $nativePreferenceAvailable = ($null -ne (Get-Variable -Name 'PSNativeCommandUseErrorActionPreference' -ErrorAction SilentlyContinue)) + $nativePreferenceSnapshot = $null + $errorActionSnapshot = $ErrorActionPreference + if ($nativePreferenceAvailable) { + $nativePreferenceSnapshot = [bool]$PSNativeCommandUseErrorActionPreference + $PSNativeCommandUseErrorActionPreference = $false + } + $ErrorActionPreference = 'Continue' + try { + try { + $runnerCliOutputLines = @( + & $RunnerCliPath @commandArgs 2>&1 | ForEach-Object { [string]$_ } + ) + } catch { + $runnerCliInvocationError = [string]$_.Exception.Message + } + } finally { + $ErrorActionPreference = $errorActionSnapshot + if ($nativePreferenceAvailable) { + $PSNativeCommandUseErrorActionPreference = $nativePreferenceSnapshot + } + } + + $result.exit_code = if ($null -eq $LASTEXITCODE) { if ($null -ne $runnerCliInvocationError) { 1 } else { 0 } } else { [int]$LASTEXITCODE } + if (-not [string]::IsNullOrWhiteSpace($runnerCliInvocationError)) { + $runnerCliOutputLines += ("runner-cli invocation error: {0}" -f $runnerCliInvocationError) + } + if (@($runnerCliOutputLines).Count -eq 0) { + $runnerCliOutputLines = @("runner-cli emitted no output. exit_code=$($result.exit_code)") + } foreach ($line in @($runnerCliOutputLines)) { Write-Host $line } @@ -772,9 +801,9 @@ function Invoke-RunnerCliPplCapabilityCheck { if (@($runnerCliOutputLines).Count -gt 0) { $result.runner_cli_output_tail = @($runnerCliOutputLines | Select-Object -Last 40) } - $result.exit_code = $LASTEXITCODE if ($result.exit_code -ne 0) { - throw "runner-cli ppl build failed with exit code $($result.exit_code)." + $firstOutput = [string]($runnerCliOutputLines | Select-Object -First 1) + throw "runner-cli ppl build failed with exit code $($result.exit_code). first_output=$firstOutput" } if (-not (Test-Path -LiteralPath $result.output_ppl_path -PathType Leaf)) { diff --git a/scripts/Invoke-WorkspaceInstallerIteration.ps1 b/scripts/Invoke-WorkspaceInstallerIteration.ps1 index 84592af..4382e0c 100644 --- a/scripts/Invoke-WorkspaceInstallerIteration.ps1 +++ b/scripts/Invoke-WorkspaceInstallerIteration.ps1 @@ -124,6 +124,7 @@ function Invoke-IterationRun { $startUtc = (Get-Date).ToUniversalTime() $runExitCode = 0 $status = 'unknown' + $failureMessage = '' try { & pwsh @argList | Out-Host @@ -135,7 +136,7 @@ function Invoke-IterationRun { } catch { $status = 'failed' $runExitCode = if ($runExitCode -ne 0) { $runExitCode } else { 1 } - throw + $failureMessage = [string]$_.Exception.Message } $endUtc = (Get-Date).ToUniversalTime() @@ -148,6 +149,7 @@ function Invoke-IterationRun { end_utc = $endUtc.ToString('o') exit_code = $runExitCode status = $status + failure_message = $failureMessage } } @@ -160,10 +162,18 @@ if (-not (Test-Path -LiteralPath $exerciseScript -PathType Leaf)) { $resolvedOutputRoot = [System.IO.Path]::GetFullPath($OutputRoot) Ensure-Directory -Path $resolvedOutputRoot -$runResults = @() +$runResults = [System.Collections.Generic.List[object]]::new() +$failureDetected = $false +$failureMessage = '' if (-not $Watch) { for ($i = 1; $i -le $Iterations; $i++) { - $runResults += Invoke-IterationRun -RunIndex $i -ResolvedOutputRoot $resolvedOutputRoot -ModeValue $Mode -SmokeRootBase $SmokeWorkspaceRoot -KeepSmoke ([bool]$KeepSmokeWorkspace) -NsisRootValue $NsisRoot -ExerciseScriptPath $exerciseScript + $runResult = Invoke-IterationRun -RunIndex $i -ResolvedOutputRoot $resolvedOutputRoot -ModeValue $Mode -SmokeRootBase $SmokeWorkspaceRoot -KeepSmoke ([bool]$KeepSmokeWorkspace) -NsisRootValue $NsisRoot -ExerciseScriptPath $exerciseScript + $runResults.Add($runResult) | Out-Null + if ([string]$runResult.status -ne 'succeeded') { + $failureDetected = $true + $failureMessage = if (-not [string]::IsNullOrWhiteSpace([string]$runResult.failure_message)) { [string]$runResult.failure_message } else { "Iteration run $([string]$runResult.run) failed." } + break + } } } else { $runIndex = 0 @@ -178,7 +188,14 @@ if (-not $Watch) { if ($runIndex -eq 0) { $runIndex++ - $runResults += Invoke-IterationRun -RunIndex $runIndex -ResolvedOutputRoot $resolvedOutputRoot -ModeValue $Mode -SmokeRootBase $SmokeWorkspaceRoot -KeepSmoke ([bool]$KeepSmokeWorkspace) -NsisRootValue $NsisRoot -ExerciseScriptPath $exerciseScript + $runResult = Invoke-IterationRun -RunIndex $runIndex -ResolvedOutputRoot $resolvedOutputRoot -ModeValue $Mode -SmokeRootBase $SmokeWorkspaceRoot -KeepSmoke ([bool]$KeepSmokeWorkspace) -NsisRootValue $NsisRoot -ExerciseScriptPath $exerciseScript + $runResults.Add($runResult) | Out-Null + if ([string]$runResult.status -ne 'succeeded') { + $failureDetected = $true + $failureMessage = if (-not [string]::IsNullOrWhiteSpace([string]$runResult.failure_message)) { [string]$runResult.failure_message } else { "Iteration run $([string]$runResult.run) failed." } + break + } + $fingerprint = Get-WorkspaceFingerprint -RepoRoot $repoRoot continue } @@ -192,12 +209,19 @@ if (-not $Watch) { Write-Host "Change detected. Re-running installer iteration." $fingerprint = $newFingerprint $runIndex++ - $runResults += Invoke-IterationRun -RunIndex $runIndex -ResolvedOutputRoot $resolvedOutputRoot -ModeValue $Mode -SmokeRootBase $SmokeWorkspaceRoot -KeepSmoke ([bool]$KeepSmokeWorkspace) -NsisRootValue $NsisRoot -ExerciseScriptPath $exerciseScript + $runResult = Invoke-IterationRun -RunIndex $runIndex -ResolvedOutputRoot $resolvedOutputRoot -ModeValue $Mode -SmokeRootBase $SmokeWorkspaceRoot -KeepSmoke ([bool]$KeepSmokeWorkspace) -NsisRootValue $NsisRoot -ExerciseScriptPath $exerciseScript + $runResults.Add($runResult) | Out-Null + if ([string]$runResult.status -ne 'succeeded') { + $failureDetected = $true + $failureMessage = if (-not [string]::IsNullOrWhiteSpace([string]$runResult.failure_message)) { [string]$runResult.failure_message } else { "Iteration run $([string]$runResult.run) failed." } + break + } + $fingerprint = Get-WorkspaceFingerprint -RepoRoot $repoRoot } } -$latest = $runResults | Select-Object -Last 1 +$latest = @($runResults) | Select-Object -Last 1 $summaryPath = Join-Path $resolvedOutputRoot 'iteration-summary.json' [ordered]@{ timestamp_utc = (Get-Date).ToUniversalTime().ToString('o') @@ -207,8 +231,15 @@ $summaryPath = Join-Path $resolvedOutputRoot 'iteration-summary.json' requested_iterations = $Iterations max_runs = $MaxRuns executed_runs = @($runResults).Count + status = if ($failureDetected) { 'failed' } else { 'succeeded' } + failure_message = $failureMessage latest = $latest - runs = $runResults + runs = @($runResults) } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $summaryPath -Encoding utf8 Write-Host "Iteration summary: $summaryPath" + +if ($failureDetected) { + Write-Error $failureMessage + exit 1 +} diff --git a/tests/HostRelease2020VipmLifecycleDrillWorkflowContract.Tests.ps1 b/tests/HostRelease2020VipmLifecycleDrillWorkflowContract.Tests.ps1 index 0041e58..c9b73e1 100644 --- a/tests/HostRelease2020VipmLifecycleDrillWorkflowContract.Tests.ps1 +++ b/tests/HostRelease2020VipmLifecycleDrillWorkflowContract.Tests.ps1 @@ -26,16 +26,22 @@ Describe 'Host release 2020 VIPM lifecycle drill workflow contract' { $script:workflowContent | Should -Match 'ref:' $script:workflowContent | Should -Match 'selected_ppl_bitness:' $script:workflowContent | Should -Match 'keep_smoke_workspace:' + $script:workflowContent | Should -Match 'allow_system_account:' $script:workflowContent | Should -Match 'nsis_root:' } It 'runs on installer-harness self-hosted windows labels and publishes deterministic artifacts' { $script:workflowContent | Should -Match 'runs-on:\s*\[self-hosted,\s*windows,\s*self-hosted-windows-lv,\s*installer-harness\]' $script:workflowContent | Should -Match 'Assert-InstallerHarnessMachinePreflight\.ps1' - $script:workflowContent | Should -Match "ExpectedLabviewYear '2020'" + $script:workflowContent | Should -Match 'allow_system_account must be boolean' + $script:workflowContent | Should -Match 'ExpectedLabviewYear' $script:workflowContent | Should -Match '-RequireNonSystemAccount' $script:workflowContent | Should -Match 'Invoke-HostRelease2020VipmLifecycleDrill\.ps1' $script:workflowContent | Should -Match "TargetLabviewYear', '2020'" + $script:workflowContent | Should -Match 'Prepare host-release 2020 VIPM lifecycle artifact bundle' + $script:workflowContent | Should -Match 'artifact-manifest\.json' + $script:workflowContent | Should -Match 'Upload host-release 2020 VIPM lifecycle artifacts \(retry\)' + $script:workflowContent | Should -Match 'Assert host-release artifact upload completed' $script:workflowContent | Should -Match 'host-release-2020-vipm-lifecycle-drill-report-\$\{\{\s*github\.run_id\s*\}\}' $script:workflowContent | Should -Match 'host-release-2020-vipm-lifecycle-drill-report\.json' }