diff --git a/scripts/Install-WorkspaceInstallerFromRelease.ps1 b/scripts/Install-WorkspaceInstallerFromRelease.ps1 index bf8e1a3..8338b18 100644 --- a/scripts/Install-WorkspaceInstallerFromRelease.ps1 +++ b/scripts/Install-WorkspaceInstallerFromRelease.ps1 @@ -83,6 +83,111 @@ function Resolve-Sha256Hex { return (Get-FileHash -LiteralPath $resolved -Algorithm SHA256).Hash.ToLowerInvariant() } +function Resolve-HostValidationProfile { + param([Parameter(Mandatory = $true)]$Policy) + + if ($null -eq $Policy.PSObject.Properties['host_validation_profile']) { + return $null + } + + $profile = $Policy.host_validation_profile + if ($null -eq $profile) { + return $null + } + + $executionProfile = [string]$profile.execution_profile + if ([string]::IsNullOrWhiteSpace($executionProfile)) { + Throw-ReleaseClientError -ReasonCode 'source_blocked' -Message 'host_validation_profile.execution_profile is required when host_validation_profile is defined.' + } + if ($executionProfile -notin @('host-release', 'container-parity')) { + Throw-ReleaseClientError -ReasonCode 'source_blocked' -Message "host_validation_profile.execution_profile '$executionProfile' is invalid. Expected 'host-release' or 'container-parity'." + } + + $executionYear = [string]$profile.runnercli_execution_labview_year + if (-not [string]::IsNullOrWhiteSpace($executionYear) -and $executionYear -notmatch '^\d{4}$') { + Throw-ReleaseClientError -ReasonCode 'source_blocked' -Message "host_validation_profile.runnercli_execution_labview_year '$executionYear' is invalid. Expected a 4-digit year." + } + + $singlePplBitness = [string]$profile.single_ppl_bitness + if (-not [string]::IsNullOrWhiteSpace($singlePplBitness) -and $singlePplBitness -notin @('32', '64')) { + Throw-ReleaseClientError -ReasonCode 'source_blocked' -Message "host_validation_profile.single_ppl_bitness '$singlePplBitness' is invalid. Expected '32' or '64'." + } + + $parityWindowsTag = [string]$profile.parity_windows_tag + if ($executionProfile -eq 'container-parity') { + if ($singlePplBitness -notin @('32', '64')) { + Throw-ReleaseClientError -ReasonCode 'source_blocked' -Message 'host_validation_profile.single_ppl_bitness is required for container-parity execution profile.' + } + if ([string]::IsNullOrWhiteSpace($parityWindowsTag)) { + Throw-ReleaseClientError -ReasonCode 'source_blocked' -Message 'host_validation_profile.parity_windows_tag is required for container-parity execution profile.' + } + } + + return [pscustomobject]@{ + execution_profile = $executionProfile + runnercli_execution_labview_year = $executionYear + single_ppl_bitness = $singlePplBitness + parity_windows_tag = $parityWindowsTag + } +} + +function Get-HostValidationEnvironmentOverrides { + param($HostValidationProfile) + + $overrides = @{} + if ($null -eq $HostValidationProfile) { + return $overrides + } + + $overrides['LVIE_INSTALLER_EXECUTION_PROFILE'] = [string]$HostValidationProfile.execution_profile + if (-not [string]::IsNullOrWhiteSpace([string]$HostValidationProfile.runnercli_execution_labview_year)) { + $overrides['LVIE_RUNNERCLI_EXECUTION_LABVIEW_YEAR'] = [string]$HostValidationProfile.runnercli_execution_labview_year + } + if (-not [string]::IsNullOrWhiteSpace([string]$HostValidationProfile.single_ppl_bitness)) { + $overrides['LVIE_GATE_SINGLE_PPL_BITNESS'] = [string]$HostValidationProfile.single_ppl_bitness + } + if (-not [string]::IsNullOrWhiteSpace([string]$HostValidationProfile.parity_windows_tag)) { + $overrides['LVIE_PARITY_WINDOWS_TAG'] = [string]$HostValidationProfile.parity_windows_tag + } + + return $overrides +} + +function Set-TemporaryEnvironmentVariables { + param([Parameter(Mandatory = $true)][hashtable]$Variables) + + $snapshot = @{} + foreach ($name in $Variables.Keys) { + $entry = Get-Item -Path ("Env:{0}" -f $name) -ErrorAction SilentlyContinue + $snapshot[$name] = [pscustomobject]@{ + exists = ($null -ne $entry) + value = if ($null -ne $entry) { [string]$entry.Value } else { '' } + } + + $value = [string]$Variables[$name] + if ([string]::IsNullOrWhiteSpace($value)) { + Remove-Item -Path ("Env:{0}" -f $name) -ErrorAction SilentlyContinue + } else { + Set-Item -Path ("Env:{0}" -f $name) -Value $value + } + } + + return $snapshot +} + +function Restore-TemporaryEnvironmentVariables { + param([Parameter(Mandatory = $true)][hashtable]$Snapshot) + + foreach ($name in $Snapshot.Keys) { + $entry = $Snapshot[$name] + if ([bool]$entry.exists) { + Set-Item -Path ("Env:{0}" -f $name) -Value ([string]$entry.value) + } else { + Remove-Item -Path ("Env:{0}" -f $name) -ErrorAction SilentlyContinue + } + } +} + function Get-ReasonCodeFromException { param([Parameter(Mandatory = $true)][string]$Message) @@ -331,6 +436,8 @@ function Assert-ReleaseClientPolicy { [void][DateTime]::Parse([string]$Policy.signature_policy.dual_mode_start_utc) [void][DateTime]::Parse([string]$Policy.signature_policy.canary_enforce_utc) [void][DateTime]::Parse([string]$Policy.signature_policy.grace_end_utc) + + [void](Resolve-HostValidationProfile -Policy $Policy) } $report = [ordered]@{ @@ -386,6 +493,7 @@ try { $policy = Load-EffectivePolicy -ManifestReleaseClient $manifestPolicy -PolicyPath $resolvedPolicyPath Assert-ReleaseClientPolicy -Policy $policy + $hostValidationProfile = Resolve-HostValidationProfile -Policy $policy $statePath = [string]$policy.state_path if ([string]::IsNullOrWhiteSpace($statePath)) { @@ -406,6 +514,9 @@ try { $report.policy_path = $resolvedPolicyPath $report.state_path = $statePath $report.install_report_path = $installReportPath + if ($null -ne $hostValidationProfile) { + $report.details.host_validation_profile = $hostValidationProfile + } if ($Mode -eq 'ValidatePolicy') { $report.status = 'pass' @@ -640,7 +751,26 @@ try { enforcement = $enforcement } - $process = Start-Process -FilePath $installerPath -ArgumentList '/S' -Wait -PassThru + $environmentOverrides = Get-HostValidationEnvironmentOverrides -HostValidationProfile $hostValidationProfile + $environmentSnapshot = @{} + try { + if ($environmentOverrides.Count -gt 0) { + $environmentSnapshot = Set-TemporaryEnvironmentVariables -Variables $environmentOverrides + $report.details.host_validation_environment = [ordered]@{} + foreach ($name in @('LVIE_INSTALLER_EXECUTION_PROFILE', 'LVIE_RUNNERCLI_EXECUTION_LABVIEW_YEAR', 'LVIE_GATE_SINGLE_PPL_BITNESS', 'LVIE_PARITY_WINDOWS_TAG')) { + if ($environmentOverrides.ContainsKey($name)) { + $report.details.host_validation_environment[$name] = [string]$environmentOverrides[$name] + } + } + } + + $process = Start-Process -FilePath $installerPath -ArgumentList '/S' -Wait -PassThru + } finally { + if ($environmentSnapshot.Count -gt 0) { + Restore-TemporaryEnvironmentVariables -Snapshot $environmentSnapshot + } + } + if ([int]$process.ExitCode -ne 0) { Throw-ReleaseClientError -ReasonCode 'installer_exit_nonzero' -Message "Installer exited with code $([int]$process.ExitCode)." } diff --git a/scripts/Test-PolicyContracts.ps1 b/scripts/Test-PolicyContracts.ps1 index f4293ee..b973a7c 100644 --- a/scripts/Test-PolicyContracts.ps1 +++ b/scripts/Test-PolicyContracts.ps1 @@ -143,6 +143,11 @@ if ($installerContractMembers -contains 'release_client') { Add-Check -Scope 'manifest' -Name 'release_client_policy_path' -Passed ([string]$releaseClient.policy_path -eq 'C:\dev\workspace-governance\release-policy.json') -Detail ([string]$releaseClient.policy_path) Add-Check -Scope 'manifest' -Name 'release_client_state_path' -Passed ([string]$releaseClient.state_path -eq 'C:\dev\artifacts\workspace-release-state.json') -Detail ([string]$releaseClient.state_path) Add-Check -Scope 'manifest' -Name 'release_client_latest_report_path' -Passed ([string]$releaseClient.latest_report_path -eq 'C:\dev\artifacts\workspace-release-client-latest.json') -Detail ([string]$releaseClient.latest_report_path) + Add-Check -Scope 'manifest' -Name 'release_client_host_validation_profile_exists' -Passed ($null -ne $releaseClient.host_validation_profile) -Detail 'installer_contract.release_client.host_validation_profile' + Add-Check -Scope 'manifest' -Name 'release_client_host_validation_execution_profile' -Passed ([string]$releaseClient.host_validation_profile.execution_profile -eq 'container-parity') -Detail ([string]$releaseClient.host_validation_profile.execution_profile) + Add-Check -Scope 'manifest' -Name 'release_client_host_validation_runnercli_execution_labview_year' -Passed ([string]$releaseClient.host_validation_profile.runnercli_execution_labview_year -eq '2026') -Detail ([string]$releaseClient.host_validation_profile.runnercli_execution_labview_year) + Add-Check -Scope 'manifest' -Name 'release_client_host_validation_single_ppl_bitness' -Passed ([string]$releaseClient.host_validation_profile.single_ppl_bitness -eq '64') -Detail ([string]$releaseClient.host_validation_profile.single_ppl_bitness) + Add-Check -Scope 'manifest' -Name 'release_client_host_validation_parity_windows_tag' -Passed ([string]$releaseClient.host_validation_profile.parity_windows_tag -eq '2026q1-windows') -Detail ([string]$releaseClient.host_validation_profile.parity_windows_tag) Add-Check -Scope 'manifest' -Name 'release_client_provenance_required' -Passed ([bool]$releaseClient.provenance_required) -Detail ([string]$releaseClient.provenance_required) Add-Check -Scope 'manifest' -Name 'release_client_allowed_repo_upstream' -Passed (@($releaseClient.allowed_repositories) -contains 'LabVIEW-Community-CI-CD/labview-cdev-surface') -Detail ([string]::Join(',', @($releaseClient.allowed_repositories))) Add-Check -Scope 'manifest' -Name 'release_client_allowed_repo_fork' -Passed (@($releaseClient.allowed_repositories) -contains 'svelderrainruiz/labview-cdev-surface') -Detail ([string]::Join(',', @($releaseClient.allowed_repositories))) diff --git a/scripts/Test-ReleaseClientContracts.ps1 b/scripts/Test-ReleaseClientContracts.ps1 index 3b377e5..15bb66f 100644 --- a/scripts/Test-ReleaseClientContracts.ps1 +++ b/scripts/Test-ReleaseClientContracts.ps1 @@ -79,6 +79,11 @@ if ($null -ne $releaseClient) { Add-Check -Name 'state_path' -Passed ([string]$releaseClient.state_path -eq 'C:\dev\artifacts\workspace-release-state.json') -Detail ([string]$releaseClient.state_path) Add-Check -Name 'latest_report_path' -Passed ([string]$releaseClient.latest_report_path -eq 'C:\dev\artifacts\workspace-release-client-latest.json') -Detail ([string]$releaseClient.latest_report_path) Add-Check -Name 'policy_path' -Passed ([string]$releaseClient.policy_path -eq 'C:\dev\workspace-governance\release-policy.json') -Detail ([string]$releaseClient.policy_path) + Add-Check -Name 'host_validation_profile_exists' -Passed ($null -ne $releaseClient.host_validation_profile) -Detail 'installer_contract.release_client.host_validation_profile' + Add-Check -Name 'host_validation_profile_execution_profile' -Passed ([string]$releaseClient.host_validation_profile.execution_profile -eq 'container-parity') -Detail ([string]$releaseClient.host_validation_profile.execution_profile) + Add-Check -Name 'host_validation_profile_runnercli_execution_labview_year' -Passed ([string]$releaseClient.host_validation_profile.runnercli_execution_labview_year -eq '2026') -Detail ([string]$releaseClient.host_validation_profile.runnercli_execution_labview_year) + Add-Check -Name 'host_validation_profile_single_ppl_bitness' -Passed ([string]$releaseClient.host_validation_profile.single_ppl_bitness -eq '64') -Detail ([string]$releaseClient.host_validation_profile.single_ppl_bitness) + Add-Check -Name 'host_validation_profile_parity_windows_tag' -Passed ([string]$releaseClient.host_validation_profile.parity_windows_tag -eq '2026q1-windows') -Detail ([string]$releaseClient.host_validation_profile.parity_windows_tag) Add-Check -Name 'cdev_cli_sync_primary_repo' -Passed ([string]$releaseClient.cdev_cli_sync.primary_repo -eq 'svelderrainruiz/labview-cdev-cli') -Detail ([string]$releaseClient.cdev_cli_sync.primary_repo) Add-Check -Name 'cdev_cli_sync_mirror_repo' -Passed ([string]$releaseClient.cdev_cli_sync.mirror_repo -eq 'LabVIEW-Community-CI-CD/labview-cdev-cli') -Detail ([string]$releaseClient.cdev_cli_sync.mirror_repo) diff --git a/tests/ReleaseClientPolicyContract.Tests.ps1 b/tests/ReleaseClientPolicyContract.Tests.ps1 index 0d05cab..6c5bdb3 100644 --- a/tests/ReleaseClientPolicyContract.Tests.ps1 +++ b/tests/ReleaseClientPolicyContract.Tests.ps1 @@ -42,6 +42,10 @@ Describe 'Release client policy contract' { $releaseClient.policy_path | Should -Be 'C:\dev\workspace-governance\release-policy.json' $releaseClient.state_path | Should -Be 'C:\dev\artifacts\workspace-release-state.json' $releaseClient.latest_report_path | Should -Be 'C:\dev\artifacts\workspace-release-client-latest.json' + $releaseClient.host_validation_profile.execution_profile | Should -Be 'container-parity' + $releaseClient.host_validation_profile.runnercli_execution_labview_year | Should -Be '2026' + $releaseClient.host_validation_profile.single_ppl_bitness | Should -Be '64' + $releaseClient.host_validation_profile.parity_windows_tag | Should -Be '2026q1-windows' $releaseClient.cdev_cli_sync.primary_repo | Should -Be 'svelderrainruiz/labview-cdev-cli' $releaseClient.cdev_cli_sync.mirror_repo | Should -Be 'LabVIEW-Community-CI-CD/labview-cdev-cli' $releaseClient.cdev_cli_sync.strategy | Should -Be 'fork-and-upstream-full-sync' @@ -133,6 +137,11 @@ Describe 'Release client policy contract' { $script:policyScriptContent | Should -Match 'svelderrainruiz/labview-cdev-surface' $script:policyScriptContent | Should -Match 'cdev_cli_sync_primary_repo' $script:policyScriptContent | Should -Match 'cdev_cli_sync_mirror_repo' + $script:policyScriptContent | Should -Match 'host_validation_profile_exists' + $script:policyScriptContent | Should -Match 'host_validation_profile_execution_profile' + $script:policyScriptContent | Should -Match 'host_validation_profile_runnercli_execution_labview_year' + $script:policyScriptContent | Should -Match 'host_validation_profile_single_ppl_bitness' + $script:policyScriptContent | Should -Match 'host_validation_profile_parity_windows_tag' $script:policyScriptContent | Should -Match 'runtime_images_exists' $script:policyScriptContent | Should -Match 'runtime_images_cdev_cli_runtime_canonical_repository' $script:policyScriptContent | Should -Match 'runtime_images_ops_runtime_base_digest' diff --git a/tests/ReleaseClientRuntimeContract.Tests.ps1 b/tests/ReleaseClientRuntimeContract.Tests.ps1 index 92b7be7..bea56ff 100644 --- a/tests/ReleaseClientRuntimeContract.Tests.ps1 +++ b/tests/ReleaseClientRuntimeContract.Tests.ps1 @@ -32,6 +32,17 @@ Describe 'Release client runtime contract' { $script:scriptContent | Should -Match 'workspace-release-client-latest\.json' } + It 'supports policy-driven host-validation profile environment overrides' { + $script:scriptContent | Should -Match 'Resolve-HostValidationProfile' + $script:scriptContent | Should -Match 'Get-HostValidationEnvironmentOverrides' + $script:scriptContent | Should -Match 'Set-TemporaryEnvironmentVariables' + $script:scriptContent | Should -Match 'Restore-TemporaryEnvironmentVariables' + $script:scriptContent | Should -Match 'LVIE_INSTALLER_EXECUTION_PROFILE' + $script:scriptContent | Should -Match 'LVIE_RUNNERCLI_EXECUTION_LABVIEW_YEAR' + $script:scriptContent | Should -Match 'LVIE_GATE_SINGLE_PPL_BITNESS' + $script:scriptContent | Should -Match 'LVIE_PARITY_WINDOWS_TAG' + } + It 'defines deterministic failure reason codes' { foreach ($reason in @('source_blocked', 'asset_missing', 'hash_mismatch', 'signature_missing', 'signature_invalid', 'provenance_invalid', 'installer_exit_nonzero', 'install_report_missing')) { $script:scriptContent | Should -Match $reason diff --git a/workspace-governance-payload/workspace-governance/workspace-governance.json b/workspace-governance-payload/workspace-governance/workspace-governance.json index 486f2af..9ff6630 100644 --- a/workspace-governance-payload/workspace-governance/workspace-governance.json +++ b/workspace-governance-payload/workspace-governance/workspace-governance.json @@ -240,6 +240,12 @@ "state_path": "C:\\dev\\artifacts\\workspace-release-state.json", "latest_report_path": "C:\\dev\\artifacts\\workspace-release-client-latest.json", "policy_path": "C:\\dev\\workspace-governance\\release-policy.json", + "host_validation_profile": { + "execution_profile": "container-parity", + "runnercli_execution_labview_year": "2026", + "single_ppl_bitness": "64", + "parity_windows_tag": "2026q1-windows" + }, "runtime_images": { "cdev_cli_runtime": { "canonical_repository": "ghcr.io/labview-community-ci-cd/labview-cdev-cli-runtime", diff --git a/workspace-governance.json b/workspace-governance.json index 486f2af..9ff6630 100644 --- a/workspace-governance.json +++ b/workspace-governance.json @@ -240,6 +240,12 @@ "state_path": "C:\\dev\\artifacts\\workspace-release-state.json", "latest_report_path": "C:\\dev\\artifacts\\workspace-release-client-latest.json", "policy_path": "C:\\dev\\workspace-governance\\release-policy.json", + "host_validation_profile": { + "execution_profile": "container-parity", + "runnercli_execution_labview_year": "2026", + "single_ppl_bitness": "64", + "parity_windows_tag": "2026q1-windows" + }, "runtime_images": { "cdev_cli_runtime": { "canonical_repository": "ghcr.io/labview-community-ci-cd/labview-cdev-cli-runtime",