fix(ci): upstream Windows SDK validation support (#3327)

* fix(ci): upstream Windows SDK validation support

Port remaining Windows validation tooling from GitLab independently of the combined MXC runtime port. Preserve locked SDK dependencies and guard temporary launcher cleanup.

Co-authored-by: Shailendra Singh <shailendras@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>

* fix(ci): install native TypeScript dependencies before validation

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>

---------

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-authored-by: Shailendra Singh <shailendras@nvidia.com>
This commit is contained in:
Prekshi Vyas
2026-09-15 22:34:36 +00:00
committed by GitHub
co-authored by Shailendra Singh
parent 8751e35e28
commit d95bab5436
14 changed files with 323 additions and 14 deletions
@@ -213,6 +213,10 @@ compatibility under emulation is not part of these tasks. The aggregate
commands above on an ARM64 host.
The repository-wide `mise run pre-commit` task is also supported on Windows.
Run `rust:lockfiles:check`, `sdk:ts:ci`, `go:ci`, and `test:e2e-parity` through
the Windows-aware tasks when validating those surfaces. Do not count the Go
Windows ARM64 race-detector exclusion or POSIX permission-bit skips as security
coverage. SDK test dependencies must remain at their lockfile versions.
Its Rust check, Clippy, and test dependencies enter the same MSVC environment
for the native host target and use an inherited compiler wrapper when it is
available. Linux glibc
+18
View File
@@ -48,6 +48,24 @@ mise run test:rust # cargo test --workspace
Rust validation checks tracked Cargo lockfiles; run `mise run rust:lockfiles:check` to check them directly. If one is stale, refresh it with Cargo using its adjacent manifest, review the diff, and commit the update.
### Native Windows validation
Use `mise run --skip-tools pre-commit` with the existing Rust/MSVC toolchain.
Windows now checks tracked Cargo lockfiles through PowerShell rather than
skipping them. The deterministic gateway parity task uses Git for Windows Bash,
with temporary Python launchers confined to a unique checkout-owned directory.
`mise run --skip-tools sdk:ts:ci` selects the x64 Biome executable on Windows
(including ARM64 hosts running it under emulation), resolves the protobuf
plugin through its Windows `.cmd` launcher, and installs the matching locked
ARM64 Rolldown binding when Node itself is ARM64. The helper preserves lockfile
resolution; it must not upgrade unrelated test dependencies.
`mise run --skip-tools go:ci` retains race detection except on Windows ARM64,
where Go does not support it. Windows token-file tests explicitly skip POSIX
mode-bit assertions; those skips do not establish Windows ACL protection.
Use a checkout with LF text files when running Unix-shell fixture checks.
## Python Unit Tests
Python unit tests use the `*_test.py` suffix convention (not `test_*` prefix)
+6
View File
@@ -108,6 +108,12 @@ async functions caused by cfg-gated Windows stubs. Repository-wide pre-commit
skips only Linux-specific installer, build-environment shell-helper, and
packaging-asset tests; its
cross-platform Python, Markdown, license, and documentation checks still run.
Tracked Cargo lockfiles are checked natively through PowerShell. Deterministic
gateway parity uses Git for Windows Bash with temporary, checkout-scoped Python
launchers. The TypeScript SDK uses Windows protobuf plugin paths and x64 Biome
under emulation on ARM64, while its test binding follows Node's architecture
and the locked Rolldown version. Go tests retain race coverage wherever the
toolchain supports it; POSIX permission-bit checks are not Windows ACL tests.
Test tasks require the Rust target architecture to match the Windows host, so
an ARM64 test result is native coverage rather than x64 emulation coverage.
By default it enables bundled Z3 for reproducible Windows builds. `z3-sys`
+1 -1
View File
@@ -36,7 +36,7 @@ func TestUserConfigDir_XDGUnset(t *testing.T) {
func TestSystemGatewayDir(t *testing.T) {
dir := systemGatewayDir()
assert.Equal(t, "/etc/openshell/gateways", dir)
assert.Equal(t, filepath.FromSlash("/etc/openshell/gateways"), dir)
}
func TestResolveGatewayDir_UserDir(t *testing.T) {
+2 -3
View File
@@ -24,9 +24,8 @@ func TestBrowserCommand_Platform(t *testing.T) {
assert.Equal(t, "xdg-open", name)
assert.Equal(t, []string{"https://example.com/auth"}, args)
case "windows":
assert.Equal(t, "cmd", name)
assert.Contains(t, args, "/c")
assert.Contains(t, args, "start")
assert.Equal(t, "rundll32", name)
assert.Equal(t, []string{"url.dll,FileProtocolHandler", "https://example.com/auth"}, args)
default:
// Unknown platform should still return something (even if it fails).
assert.NotEmpty(t, name)
+12 -5
View File
@@ -7,6 +7,7 @@ import (
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"time"
@@ -41,9 +42,7 @@ func TestWriteToken_ReplacesInsecureExistingFileWithOwnerOnlyFile(t *testing.T)
require.NoError(t, writeToken(dir, &oauth2.Token{AccessToken: "secret"}))
info, err := os.Stat(path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
assertOwnerOnlyFilePermissions(t, path)
}
func TestWriteToken_ExpiresInCalculated(t *testing.T) {
@@ -187,8 +186,16 @@ func TestWriteToken_FilePermissions(t *testing.T) {
err := writeToken(dir, tok)
require.NoError(t, err)
info, err := os.Stat(filepath.Join(dir, "oidc_token.json"))
assertOwnerOnlyFilePermissions(t, filepath.Join(dir, "oidc_token.json"))
}
func assertOwnerOnlyFilePermissions(t *testing.T, path string) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("POSIX file permission bits are not supported on Windows")
}
info, err := os.Stat(path)
require.NoError(t, err)
// File should be owner-only readable (0600).
assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
}
+44 -4
View File
@@ -6,7 +6,17 @@
["go:test"]
description = "Run Go SDK unit tests with coverage"
dir = "sdk/go"
run = "go test -coverprofile=coverage.out -coverpkg=./openshell/... -race ./..."
run = """
#!/usr/bin/env bash
set -euo pipefail
if [ "$(go env GOOS)" = "windows" ] && [ "$(go env GOARCH)" = "arm64" ]; then
echo "Go race detection is unsupported on windows/arm64; running without -race."
go test -coverprofile=coverage.out -coverpkg=./openshell/... ./...
else
go test -coverprofile=coverage.out -coverpkg=./openshell/... -race ./...
fi
"""
hide = true
["go:test:integration"]
@@ -120,11 +130,26 @@ if find proto -maxdepth 1 -name '*.proto' -print -quit | grep -q .; then
exit 1
fi
BUF_TEMPLATE="$SDK_ROOT/buf.gen.yaml"
if command -v cygpath &>/dev/null; then
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
GO_PLUGIN=$(cygpath -m "$(command -v protoc-gen-go)")
GRPC_PLUGIN=$(cygpath -m "$(command -v protoc-gen-go-grpc)")
GO_PLUGIN=${GO_PLUGIN//&/\\&}
GRPC_PLUGIN=${GRPC_PLUGIN//&/\\&}
sed \
-e "s|local: protoc-gen-go$|local: $GO_PLUGIN|" \
-e "s|local: protoc-gen-go-grpc$|local: $GRPC_PLUGIN|" \
"$SDK_ROOT/buf.gen.yaml" > "$WORK_DIR/buf.gen.yaml"
BUF_TEMPLATE="$WORK_DIR/buf.gen.yaml"
fi
# Clean previous output before regeneration
find proto -name '*.pb.go' -delete 2>/dev/null || true
find proto -mindepth 1 -type d -empty -delete 2>/dev/null || true
(cd "$REPO_ROOT" && buf generate --template "$SDK_ROOT/buf.gen.yaml")
(cd "$REPO_ROOT" && buf generate --template "$BUF_TEMPLATE")
echo "Proto generation complete."
echo "Generated packages:"
@@ -162,8 +187,23 @@ if find proto -maxdepth 1 -name '*.proto' -print -quit | grep -q .; then
fi
# Generate to temp directory with adjusted output path
CHECK_TEMPLATE=$(sed 's|out: sdk/go|out: '"$WORK_DIR"'|' buf.gen.yaml)
(cd "$REPO_ROOT" && buf generate --template "$CHECK_TEMPLATE")
BUF_OUTPUT_DIR="$WORK_DIR"
if command -v cygpath &>/dev/null; then
BUF_OUTPUT_DIR=$(cygpath -m "$WORK_DIR")
fi
sed "s|out: sdk/go|out: $BUF_OUTPUT_DIR|" "$SDK_ROOT/buf.gen.yaml" > "$WORK_DIR/buf.gen.yaml"
if command -v cygpath &>/dev/null; then
GO_PLUGIN=$(cygpath -m "$(command -v protoc-gen-go)")
GRPC_PLUGIN=$(cygpath -m "$(command -v protoc-gen-go-grpc)")
GO_PLUGIN=${GO_PLUGIN//&/\\&}
GRPC_PLUGIN=${GRPC_PLUGIN//&/\\&}
sed \
-e "s|local: protoc-gen-go$|local: $GO_PLUGIN|" \
-e "s|local: protoc-gen-go-grpc$|local: $GRPC_PLUGIN|" \
"$WORK_DIR/buf.gen.yaml" > "$WORK_DIR/buf.gen.windows.yaml"
mv "$WORK_DIR/buf.gen.windows.yaml" "$WORK_DIR/buf.gen.yaml"
fi
(cd "$REPO_ROOT" && buf generate --template "$WORK_DIR/buf.gen.yaml")
DIFF_OUTPUT=$(diff -r "$WORK_DIR/proto" "$SDK_ROOT/proto" 2>&1) || true
+1
View File
@@ -4,6 +4,7 @@
["test:e2e-parity"]
description = "Run deterministic schema-v1/schema-v2 parity harness tests"
run = "bash e2e/parity/test.sh"
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/run-git-bash.ps1 e2e/parity/test.sh"
hide = true
["e2e:parity:podman"]
+1 -1
View File
@@ -13,7 +13,7 @@ hide = true
["rust:lockfiles:check"]
description = "Verify all tracked Cargo lockfiles are current"
run = "tasks/scripts/check-cargo-lockfiles.sh"
run_windows = "echo Skipping rust:lockfiles:check: Cargo lockfile validation uses a Unix shell helper."
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/check-cargo-lockfiles.ps1"
hide = true
["rust:lint"]
+44
View File
@@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
$ErrorActionPreference = "Stop"
$repoRoot = (& git rev-parse --show-toplevel).Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
throw "Unable to resolve the repository root"
}
Set-Location -LiteralPath $repoRoot
$lockfiles = @(& git ls-files -- ':(glob)**/Cargo.lock')
if ($LASTEXITCODE -ne 0) {
throw "Unable to enumerate tracked Cargo.lock files"
}
if ($lockfiles.Count -eq 0) {
throw "No tracked Cargo.lock files found"
}
$failed = $false
foreach ($lockfile in $lockfiles) {
$lockfileDirectory = Split-Path -Parent $lockfile
$manifest = if ([string]::IsNullOrEmpty($lockfileDirectory)) {
"Cargo.toml"
} else {
Join-Path $lockfileDirectory "Cargo.toml"
}
if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) {
Write-Error "Tracked lockfile $lockfile has no adjacent Cargo.toml"
$failed = $true
continue
}
Write-Output "Checking $lockfile"
& cargo metadata --locked --format-version 1 --manifest-path $manifest | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Validation failed for $lockfile"
$failed = $true
}
}
if ($failed) {
throw "Resolve the reported errors. If a lockfile needs updating, refresh it with Cargo and commit the result."
}
+90
View File
@@ -0,0 +1,90 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$Script,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$Arguments
)
$ErrorActionPreference = "Stop"
$gitCommand = Get-Command git.exe -ErrorAction Stop
$gitRoot = Split-Path -Parent $gitCommand.Source
$bash = $null
# Git can expose cmd/git.exe, mingw64/bin/git.exe, or clangarm64/bin/git.exe.
for ($level = 0; $level -lt 4 -and $gitRoot; $level++) {
$candidate = Join-Path $gitRoot 'bin\bash.exe'
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
$bash = $candidate
break
}
$gitRoot = Split-Path -Parent $gitRoot
}
if (-not $bash) {
throw "Git for Windows bash.exe was not found beside $($gitCommand.Source)"
}
$repoRoot = (& git rev-parse --show-toplevel).Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
throw 'Unable to resolve the repository root'
}
$repoPrefix = [IO.Path]::GetFullPath($repoRoot).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
$shimDirectory = [IO.Path]::GetFullPath((Join-Path $repoRoot ('.git-bash-shim-' + [guid]::NewGuid().ToString('N'))))
if (-not $shimDirectory.StartsWith($repoPrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw 'Git Bash shim directory escaped the repository'
}
$shimCreated = $false
$previousPath = $env:PATH
$previousTmpdir = $env:TMPDIR
$python = $env:UV_PYTHON
if ([string]::IsNullOrWhiteSpace($python)) {
$worktreePython = Join-Path $repoRoot ".venv\Scripts\python.exe"
if (Test-Path -LiteralPath $worktreePython -PathType Leaf) {
$python = $worktreePython
}
}
try {
New-Item -ItemType Directory -Path $shimDirectory | Out-Null
$shimCreated = $true
if (-not [string]::IsNullOrWhiteSpace($python)) {
$python = [IO.Path]::GetFullPath($python)
if ($python -notmatch '^(?<drive>[A-Za-z]):\\(?<tail>.*)$') {
throw "Python must use an absolute drive path for Git Bash: $python"
}
$bashPython = "/$($Matches.drive.ToLowerInvariant())/$($Matches.tail.Replace('\', '/'))"
if ($bashPython.Contains("'")) {
throw "Python path cannot contain a single quote: $python"
}
$launcher = "#!/usr/bin/env bash`nexec '$bashPython' `"`$@`"`n"
$utf8WithoutBom = [Text.UTF8Encoding]::new($false)
foreach ($launcherName in @("python", "python3")) {
[IO.File]::WriteAllText(
(Join-Path $shimDirectory $launcherName),
$launcher,
$utf8WithoutBom
)
}
}
$bashTemp = Join-Path $shimDirectory "tmp"
New-Item -ItemType Directory -Path $bashTemp | Out-Null
$env:TMPDIR = $bashTemp.Replace("\", "/")
$env:PATH = "$shimDirectory;$gitRoot\usr\bin;$gitRoot\bin;$env:PATH"
& $bash $Script @Arguments
$exitCode = $LASTEXITCODE
} finally {
$env:PATH = $previousPath
$env:TMPDIR = $previousTmpdir
if ($shimCreated -and (Test-Path -LiteralPath $shimDirectory)) {
$resolvedShim = (Resolve-Path -LiteralPath $shimDirectory).ProviderPath
if (-not $resolvedShim.StartsWith($repoPrefix, [StringComparison]::OrdinalIgnoreCase) -or
((Get-Item -LiteralPath $shimDirectory).Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw 'Refusing to remove an unexpected Git Bash shim directory'
}
Remove-Item -LiteralPath $shimDirectory -Recurse -Force
}
}
exit $exitCode
+40
View File
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$nodeArchitecture = (& node -p "process.arch").Trim()
if ($LASTEXITCODE -ne 0) {
throw "Unable to determine the Node.js process architecture (exit code $LASTEXITCODE)."
}
if ($nodeArchitecture -notin @('x64', 'arm64')) {
throw "Unsupported Windows Node.js architecture '$nodeArchitecture'."
}
$previousCpu = $env:npm_config_cpu
try {
$env:npm_config_cpu = $nodeArchitecture
& npm ci
if ($LASTEXITCODE -ne 0) {
throw "Unable to install TypeScript SDK dependencies (exit code $LASTEXITCODE)."
}
}
finally {
$env:npm_config_cpu = $previousCpu
}
$nativePackages = @(
"@biomejs/cli-win32-$nodeArchitecture",
"@bufbuild/buf-win32-$nodeArchitecture",
"@rolldown/binding-win32-$nodeArchitecture-msvc"
)
foreach ($package in $nativePackages) {
$manifest = Join-Path 'node_modules' (Join-Path $package 'package.json')
if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) {
throw "The lockfile-pinned native dependency '$package' was not installed."
}
}
+58
View File
@@ -0,0 +1,58 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$repository = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$sdkRoot = Join-Path $repository 'sdk\typescript'
$sourceTemplate = Join-Path $sdkRoot 'buf.gen.yaml'
$buf = (Resolve-Path (Join-Path $sdkRoot 'node_modules\.bin\buf.cmd')).Path
$plugin = (Resolve-Path (Join-Path $sdkRoot 'node_modules\.bin\protoc-gen-es.cmd')).Path
$temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) "openshell-ts-proto-$([guid]::NewGuid().ToString('N'))"
$windowsTemplate = Join-Path $temporaryRoot 'buf.gen.windows.yaml'
$tempPrefix = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
if (-not [IO.Path]::GetFullPath($temporaryRoot).StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw 'Protobuf temporary directory escaped the temporary root'
}
New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
try {
$pluginPath = $plugin.Replace('\', '/')
$template = [IO.File]::ReadAllText($sourceTemplate).Replace(
'./node_modules/.bin/protoc-gen-es',
$pluginPath
)
[IO.File]::WriteAllText(
$windowsTemplate,
$template,
[Text.UTF8Encoding]::new($false)
)
Push-Location $sdkRoot
try {
& $buf generate --template $windowsTemplate
if ($LASTEXITCODE -ne 0) {
throw "TypeScript protobuf generation failed with exit code $LASTEXITCODE."
}
}
finally {
Pop-Location
}
}
finally {
if (
(Test-Path -LiteralPath $temporaryRoot) -and
(Split-Path -Leaf $temporaryRoot) -like 'openshell-ts-proto-*'
) {
$resolvedTemp = (Resolve-Path -LiteralPath $temporaryRoot).ProviderPath
if (-not $resolvedTemp.StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase) -or
((Get-Item -LiteralPath $temporaryRoot).Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw 'Refusing to remove an unexpected protobuf temporary directory'
}
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
}
}
+2
View File
@@ -9,6 +9,7 @@
description = "Install TypeScript SDK dependencies"
dir = "sdk/typescript"
run = "npm ci"
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File ../../tasks/scripts/typescript-install.ps1"
hide = true
["sdk:ts:proto"]
@@ -16,6 +17,7 @@ description = "Generate TypeScript protobuf stubs for the SDK"
depends = ["sdk:ts:install"]
dir = "sdk/typescript"
run = "npm run gen"
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File ../../tasks/scripts/typescript-proto.ps1"
# Lints the repo-level proto module (buf.yaml at the root) against STANDARD.
# buf ships only in the SDK's devDependencies today, so this depends on the