From 1a1b5d28df57553c47d49aeb0d72c05844ee1f1f Mon Sep 17 00:00:00 2001 From: Sergio Velderrain <156447188+svelderrainruiz@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:05:17 -0800 Subject: [PATCH 1/3] docs(agents): add windows gate contract for cli --- AGENTS.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 9b60478..25ea4cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,22 @@ This repository is the control-plane CLI for deterministic `C:\dev` workspace or - `scripts/lib/Invoke-NiLinuxDeployCheck.ps1` validates NI Linux image deploy path using Docker Desktop Linux context. - Default Linux deploy image: `nationalinstruments/labview:latest-linux`. +## Windows Contract +- Windows gate orchestration must target Docker Desktop Windows container mode on self-hosted runners. +- Required gate runner labels: + - `self-hosted` + - `windows` + - `self-hosted-windows-lv` + - `windows-containers` + - `user-session` + - `cdev-surface-windows-gate` +- Default Windows gate image is `nationalinstruments/labview:2026q1-windows` unless an explicit override is set by policy. +- Gate container command surface must use Windows PowerShell (`powershell.exe`) and may use host-mounted PowerShell 7, but must not require in-image `pwsh` availability. +- `g-cli` is a host-side dependency for Windows LabVIEW execution and is not required to exist inside the container image. +- Gate runs that exercise PPL validation must enforce one bitness per image run (`LVIE_GATE_SINGLE_PPL_BITNESS=32|64`), never serialized dual-bitness in a single run. +- When gate-required LabVIEW year is `2026`, x86 parity must be enforced before 32-bit PPL validation; if missing, bootstrap `ni-labview-2026-core-x86-en` via NI Package Manager. +- `VIPM_COMMUNITY_EDITION=true` must be set for Windows image lanes that activate VIPM CLI in community mode. + ## CI Contract - Required checks for default branch target: - `CI Pipeline` From 0d3a3ed2d1e22ee76932b0fc196cdbe2b5ca26a9 Mon Sep 17 00:00:00 2001 From: Sergio Velderrain <156447188+svelderrainruiz@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:03:36 -0800 Subject: [PATCH 2/3] fix(cli): make windows command path powershell-first --- AGENTS.md | 2 ++ README.md | 18 ++++++++++-------- scripts/Invoke-CdevCli.ps1 | 31 +++++++++++++++++++++---------- scripts/lib/Common.ps1 | 33 ++++++++++++++++++++++++++++++--- tests/CdevCliContract.Tests.ps1 | 30 +++++++++++++++++++++++++++--- 5 files changed, 90 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 25ea4cc..dd7163b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,8 @@ This repository is the control-plane CLI for deterministic `C:\dev` workspace or ## CLI Orchestration Contract - CLI entrypoint: `scripts/Invoke-CdevCli.ps1`. +- Windows invocation contract: `powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 ...`. +- Linux invocation contract: `pwsh -NoProfile -File ./scripts/Invoke-CdevCli.ps1 ...`. - CLI is the preferred operator interface for: - repo topology inspection (`repos list`) - governance checks (`repos doctor`) diff --git a/README.md b/README.md index efd0c13..8439bcf 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ Control-plane CLI for deterministic `C:\dev` workspace operations. ## Entrypoint ```powershell -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 [options] +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 [options] ``` +On Linux, invoke the same entrypoint with `pwsh -NoProfile -File`. + ## Commands - `help [topic]` @@ -26,23 +28,23 @@ pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 [options] ## Quick Start ```powershell -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 repos list -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 repos doctor --workspace-root C:\dev -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 installer exercise --mode fast --iterations 1 -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 ci integration-gate --repo svelderrainruiz/labview-cdev-surface --branch main +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 repos list +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 repos doctor --workspace-root C:\dev +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 installer exercise --mode fast --iterations 1 +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 ci integration-gate --repo svelderrainruiz/labview-cdev-surface --branch main ``` ## Linux Flow (Docker Desktop Linux) ```powershell -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 linux install --workspace-root C:\dev-linux -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 linux deploy-ni --workspace-root C:\dev-linux --docker-context desktop-linux --image nationalinstruments/labview:latest-linux +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 linux install --workspace-root C:\dev-linux +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 linux deploy-ni --workspace-root C:\dev-linux --docker-context desktop-linux --image nationalinstruments/labview:latest-linux ``` ## Release Packaging ```powershell -pwsh -NoProfile -File .\scripts\Invoke-CdevCli.ps1 release package --output-root .\artifacts\release\cli +powershell -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\Invoke-CdevCli.ps1 release package --output-root .\artifacts\release\cli ``` Release artifacts: diff --git a/scripts/Invoke-CdevCli.ps1 b/scripts/Invoke-CdevCli.ps1 index 017075f..2544273 100644 --- a/scripts/Invoke-CdevCli.ps1 +++ b/scripts/Invoke-CdevCli.ps1 @@ -1,4 +1,3 @@ -#Requires -Version 7.0 [CmdletBinding()] param( [Parameter(Position = 0, ValueFromRemainingArguments = $true)] @@ -8,12 +7,24 @@ param( [string]$SurfaceRoot, [Parameter()] - [string]$ReportPath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'artifacts\cli\cdev-cli-last-run.json') + [string]$ReportPath = '' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +if ([string]::IsNullOrWhiteSpace([string]$ReportPath)) { + $scriptDirectory = if (-not [string]::IsNullOrWhiteSpace([string]$PSScriptRoot)) { + $PSScriptRoot + } elseif (-not [string]::IsNullOrWhiteSpace([string]$PSCommandPath)) { + Split-Path -Parent $PSCommandPath + } else { + (Get-Location).Path + } + + $ReportPath = Join-Path (Split-Path -Parent $scriptDirectory) 'artifacts\cli\cdev-cli-last-run.json' +} + $libRoot = Join-Path $PSScriptRoot 'lib' foreach ($libFile in @( 'Common.ps1', @@ -37,7 +48,7 @@ function Show-CdevHelp { 'cdev control-plane CLI', '', 'Usage:', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 [options]', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 [options]', '', 'Groups and commands:', ' help [topic]', @@ -54,12 +65,12 @@ function Show-CdevHelp { ' release package', '', 'Examples:', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 repos list', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 repos doctor --workspace-root C:\dev', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 installer exercise --mode fast --iterations 1', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 postactions collect --report-path C:\dev\artifacts\workspace-install-latest.json', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 linux install --workspace-root C:\dev-linux', - ' pwsh -NoProfile -File scripts/Invoke-CdevCli.ps1 linux deploy-ni --workspace-root C:\dev-linux --docker-context desktop-linux' + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 repos list', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 repos doctor --workspace-root C:\dev', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 installer exercise --mode fast --iterations 1', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 postactions collect --report-path C:\dev\artifacts\workspace-install-latest.json', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 linux install --workspace-root C:\dev-linux', + ' powershell -NoProfile -ExecutionPolicy RemoteSigned -File scripts/Invoke-CdevCli.ps1 linux deploy-ni --workspace-root C:\dev-linux --docker-context desktop-linux' ) if ([string]::IsNullOrWhiteSpace($Topic)) { @@ -81,7 +92,7 @@ function Show-CdevHelp { } } -Assert-CdevCommand -Name 'pwsh' +Resolve-CdevPowerShellHost | Out-Null $cliRepoRoot = Get-CdevRepoRoot -ScriptPath $PSCommandPath $argsMap = Convert-CdevArgsToMap -InputArgs $CommandArgs $script:resolvedSurfaceRoot = $null diff --git a/scripts/lib/Common.ps1 b/scripts/lib/Common.ps1 index 828bba5..c87d487 100644 --- a/scripts/lib/Common.ps1 +++ b/scripts/lib/Common.ps1 @@ -1,5 +1,26 @@ Set-StrictMode -Version Latest +function Resolve-CdevPowerShellHost { + [CmdletBinding()] + param() + + $isWindowsHost = [string]::Equals($env:OS, 'Windows_NT', [System.StringComparison]::OrdinalIgnoreCase) + $candidates = if ($isWindowsHost) { + @('powershell.exe', 'powershell', 'pwsh.exe', 'pwsh') + } else { + @('pwsh', 'pwsh.exe', 'powershell', 'powershell.exe') + } + + foreach ($candidate in $candidates) { + $command = Get-Command -Name $candidate -CommandType Application -ErrorAction SilentlyContinue + if ($null -ne $command -and -not [string]::IsNullOrWhiteSpace($command.Source)) { + return $command.Source + } + } + + throw "Unable to resolve a PowerShell host executable (tried: $($candidates -join ', '))." +} + function Get-CdevRepoRoot { param([Parameter(Mandatory = $true)][string]$ScriptPath) return (Resolve-Path -Path (Join-Path (Split-Path -Parent $ScriptPath) '..')).Path @@ -78,16 +99,22 @@ function Invoke-CdevPwshScript { throw "Script not found: $ScriptPath" } - $argList = @('-NoProfile', '-File', $ScriptPath) + $powerShellHost = Resolve-CdevPowerShellHost + $argList = @('-NoProfile') + if ([string]::Equals($env:OS, 'Windows_NT', [System.StringComparison]::OrdinalIgnoreCase)) { + $argList += @('-ExecutionPolicy', 'RemoteSigned') + } + $argList += @('-File', $ScriptPath) if ($null -ne $Arguments -and $Arguments.Count -gt 0) { $argList += $Arguments } - & pwsh @argList - $exitCode = $LASTEXITCODE + & $powerShellHost @argList + $exitCode = if ($LASTEXITCODE -ne $null) { [int]$LASTEXITCODE } else { 0 } return [pscustomobject]@{ script = $ScriptPath + host = $powerShellHost arguments = @($Arguments) exit_code = $exitCode status = if ($exitCode -eq 0) { 'succeeded' } else { 'failed' } diff --git a/tests/CdevCliContract.Tests.ps1 b/tests/CdevCliContract.Tests.ps1 index cd24671..ed5125e 100644 --- a/tests/CdevCliContract.Tests.ps1 +++ b/tests/CdevCliContract.Tests.ps1 @@ -1,10 +1,32 @@ -#Requires -Version 7.0 #Requires -Modules Pester $ErrorActionPreference = 'Stop' Describe 'cdev CLI command contract' { BeforeAll { + $isWindowsHost = [string]::Equals($env:OS, 'Windows_NT', [System.StringComparison]::OrdinalIgnoreCase) + $candidateHosts = if ($isWindowsHost) { + @('powershell.exe', 'powershell', 'pwsh.exe', 'pwsh') + } else { + @('pwsh', 'pwsh.exe', 'powershell', 'powershell.exe') + } + $script:powerShellHost = $null + foreach ($candidate in $candidateHosts) { + $cmd = Get-Command -Name $candidate -CommandType Application -ErrorAction SilentlyContinue + if ($null -ne $cmd -and -not [string]::IsNullOrWhiteSpace($cmd.Source)) { + $script:powerShellHost = $cmd.Source + break + } + } + if ([string]::IsNullOrWhiteSpace([string]$script:powerShellHost)) { + throw "Unable to resolve a PowerShell host executable (tried: $($candidateHosts -join ', '))." + } + + $script:powerShellInvocationArgs = @('-NoProfile') + if ($isWindowsHost) { + $script:powerShellInvocationArgs += @('-ExecutionPolicy', 'RemoteSigned') + } + $script:repoRoot = (Resolve-Path -Path (Join-Path $PSScriptRoot '..')).Path $script:entrypoint = Join-Path $script:repoRoot 'scripts/Invoke-CdevCli.ps1' $script:contractPath = Join-Path $script:repoRoot 'cli-contract.json' @@ -56,7 +78,8 @@ Describe 'cdev CLI command contract' { try { Remove-Item Env:CDEV_SURFACE_ROOT -ErrorAction SilentlyContinue - & pwsh -NoProfile -File $script:entrypoint -ReportPath $reportPath help + $invokeArgs = @($script:powerShellInvocationArgs + @('-File', $script:entrypoint, '-ReportPath', $reportPath, 'help')) + & $script:powerShellHost @invokeArgs $LASTEXITCODE | Should -Be 0 (Test-Path -LiteralPath $reportPath -PathType Leaf) | Should -BeTrue } finally { @@ -76,7 +99,8 @@ Describe 'cdev CLI command contract' { $resolvedOutputRoot = [System.IO.Path]::GetFullPath($outputRoot) try { - & pwsh -NoProfile -File $script:entrypoint -ReportPath $reportPath release package --output-root $outputRoot + $invokeArgs = @($script:powerShellInvocationArgs + @('-File', $script:entrypoint, '-ReportPath', $reportPath, 'release', 'package', '--output-root', $outputRoot)) + & $script:powerShellHost @invokeArgs $LASTEXITCODE | Should -Be 0 $report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json -ErrorAction Stop From 61808bdb1a516232f93228c3c2b1a4e9e84a9623 Mon Sep 17 00:00:00 2001 From: Sergio Velderrain <156447188+svelderrainruiz@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:06:45 -0800 Subject: [PATCH 3/3] fix(cli): select deterministic powershell host path --- scripts/lib/Common.ps1 | 2 +- tests/CdevCliContract.Tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/lib/Common.ps1 b/scripts/lib/Common.ps1 index c87d487..16670c0 100644 --- a/scripts/lib/Common.ps1 +++ b/scripts/lib/Common.ps1 @@ -12,7 +12,7 @@ function Resolve-CdevPowerShellHost { } foreach ($candidate in $candidates) { - $command = Get-Command -Name $candidate -CommandType Application -ErrorAction SilentlyContinue + $command = Get-Command -Name $candidate -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $command -and -not [string]::IsNullOrWhiteSpace($command.Source)) { return $command.Source } diff --git a/tests/CdevCliContract.Tests.ps1 b/tests/CdevCliContract.Tests.ps1 index ed5125e..cf3c662 100644 --- a/tests/CdevCliContract.Tests.ps1 +++ b/tests/CdevCliContract.Tests.ps1 @@ -12,7 +12,7 @@ Describe 'cdev CLI command contract' { } $script:powerShellHost = $null foreach ($candidate in $candidateHosts) { - $cmd = Get-Command -Name $candidate -CommandType Application -ErrorAction SilentlyContinue + $cmd = Get-Command -Name $candidate -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $cmd -and -not [string]::IsNullOrWhiteSpace($cmd.Source)) { $script:powerShellHost = $cmd.Source break