From 3f4a651fc1ace0c7e89f5eeff737f4ada58078fb Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 14 Sep 2026 14:12:42 -0700 Subject: [PATCH 1/2] 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 Signed-off-by: Prekshi Vyas --- .../build-openshell-mxc-windows/SKILL.md | 4 + TESTING.md | 18 ++++ architecture/windows-msvc-build.md | 6 ++ sdk/go/openshell/v1/gateway/paths_test.go | 2 +- sdk/go/openshell/v1/oidc/browser_test.go | 5 +- sdk/go/openshell/v1/oidc/token_test.go | 17 ++-- tasks/go.toml | 48 +++++++++- tasks/parity.toml | 1 + tasks/rust.toml | 2 +- tasks/scripts/check-cargo-lockfiles.ps1 | 44 +++++++++ tasks/scripts/run-git-bash.ps1 | 90 +++++++++++++++++++ tasks/scripts/typescript-proto.ps1 | 58 ++++++++++++ tasks/scripts/typescript-test.ps1 | 36 ++++++++ tasks/typescript.toml | 10 ++- 14 files changed, 326 insertions(+), 15 deletions(-) create mode 100644 tasks/scripts/check-cargo-lockfiles.ps1 create mode 100644 tasks/scripts/run-git-bash.ps1 create mode 100644 tasks/scripts/typescript-proto.ps1 create mode 100644 tasks/scripts/typescript-test.ps1 diff --git a/.agents/skills/build-openshell-mxc-windows/SKILL.md b/.agents/skills/build-openshell-mxc-windows/SKILL.md index cd5689d7bb..9b6229d96b 100644 --- a/.agents/skills/build-openshell-mxc-windows/SKILL.md +++ b/.agents/skills/build-openshell-mxc-windows/SKILL.md @@ -214,6 +214,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 diff --git a/TESTING.md b/TESTING.md index aaad71aa41..699f44a728 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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) diff --git a/architecture/windows-msvc-build.md b/architecture/windows-msvc-build.md index 956b1b3e9c..d221ca99cc 100644 --- a/architecture/windows-msvc-build.md +++ b/architecture/windows-msvc-build.md @@ -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 the `z3-sys` prebuilt-release feature and pins Z3 4.16.0. diff --git a/sdk/go/openshell/v1/gateway/paths_test.go b/sdk/go/openshell/v1/gateway/paths_test.go index 547bd3fc3c..fff9a01d73 100644 --- a/sdk/go/openshell/v1/gateway/paths_test.go +++ b/sdk/go/openshell/v1/gateway/paths_test.go @@ -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) { diff --git a/sdk/go/openshell/v1/oidc/browser_test.go b/sdk/go/openshell/v1/oidc/browser_test.go index 714767c93d..f2596ea40c 100644 --- a/sdk/go/openshell/v1/oidc/browser_test.go +++ b/sdk/go/openshell/v1/oidc/browser_test.go @@ -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) diff --git a/sdk/go/openshell/v1/oidc/token_test.go b/sdk/go/openshell/v1/oidc/token_test.go index 4840c882a4..cff24260af 100644 --- a/sdk/go/openshell/v1/oidc/token_test.go +++ b/sdk/go/openshell/v1/oidc/token_test.go @@ -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()) } diff --git a/tasks/go.toml b/tasks/go.toml index 81091f2912..4ad689c52c 100644 --- a/tasks/go.toml +++ b/tasks/go.toml @@ -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 diff --git a/tasks/parity.toml b/tasks/parity.toml index ea8f422054..2bc139a53a 100644 --- a/tasks/parity.toml +++ b/tasks/parity.toml @@ -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"] diff --git a/tasks/rust.toml b/tasks/rust.toml index 548e302f12..2a0396922c 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -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"] diff --git a/tasks/scripts/check-cargo-lockfiles.ps1 b/tasks/scripts/check-cargo-lockfiles.ps1 new file mode 100644 index 0000000000..9fd482fc53 --- /dev/null +++ b/tasks/scripts/check-cargo-lockfiles.ps1 @@ -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." +} diff --git a/tasks/scripts/run-git-bash.ps1 b/tasks/scripts/run-git-bash.ps1 new file mode 100644 index 0000000000..a705085f04 --- /dev/null +++ b/tasks/scripts/run-git-bash.ps1 @@ -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 '^(?[A-Za-z]):\\(?.*)$') { + 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 diff --git a/tasks/scripts/typescript-proto.ps1 b/tasks/scripts/typescript-proto.ps1 new file mode 100644 index 0000000000..6ccc056eee --- /dev/null +++ b/tasks/scripts/typescript-proto.ps1 @@ -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 + } +} diff --git a/tasks/scripts/typescript-test.ps1 b/tasks/scripts/typescript-test.ps1 new file mode 100644 index 0000000000..d2e581e89c --- /dev/null +++ b/tasks/scripts/typescript-test.ps1 @@ -0,0 +1,36 @@ +# 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 -eq 'arm64') { + $bindingVersion = (& node -p "require('./package-lock.json').packages['node_modules/rolldown'].version").Trim() + if ($LASTEXITCODE -ne 0 -or $bindingVersion -notmatch '^\d+\.\d+\.\d+([+-][0-9A-Za-z.-]+)?$') { + throw 'Unable to determine the locked Rolldown version.' + } + $previousCpu = $env:npm_config_cpu + try { + $env:npm_config_cpu = 'arm64' + # Keep lockfile resolution enabled: disabling it silently upgrades + # unrelated test dependencies instead of testing the pinned SDK tree. + & npm install --no-save "@rolldown/binding-win32-arm64-msvc@$bindingVersion" + if ($LASTEXITCODE -ne 0) { + throw "Unable to install the ARM64 Rolldown test binding (exit code $LASTEXITCODE)." + } + } + finally { + $env:npm_config_cpu = $previousCpu + } +} + +& npm test +exit $LASTEXITCODE diff --git a/tasks/typescript.toml b/tasks/typescript.toml index 06f6e8c4f1..5478457b8c 100644 --- a/tasks/typescript.toml +++ b/tasks/typescript.toml @@ -9,6 +9,7 @@ description = "Install TypeScript SDK dependencies" dir = "sdk/typescript" run = "npm ci" +run_windows = "set npm_config_cpu=x64&& npm ci" 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 @@ -38,12 +40,14 @@ description = "Lint + format-check the TypeScript SDK (Biome, read-only)" depends = ["sdk:ts:install"] dir = "sdk/typescript" run = "npm run lint" +run_windows = ".\\node_modules\\@biomejs\\cli-win32-x64\\biome.exe ci ." ["sdk:ts:format"] description = "Format the TypeScript SDK and apply safe fixes (Biome, writes)" depends = ["sdk:ts:install"] dir = "sdk/typescript" run = "npm run format" +run_windows = ".\\node_modules\\@biomejs\\cli-win32-x64\\biome.exe check --write ." ["sdk:ts:build"] description = "Build the TypeScript SDK (emit dist/)" @@ -53,9 +57,13 @@ run = "npm run build" ["sdk:ts:test"] description = "Run TypeScript SDK unit tests (Vitest, in-memory transport)" -depends = ["sdk:ts:proto"] +# On Windows ARM64, typescript-test.ps1 installs the native Rolldown binding +# into node_modules. Keep type checking ahead of that mutation when the +# aggregate `ci` task schedules `check` and `test` concurrently. +depends = ["sdk:ts:typecheck"] dir = "sdk/typescript" run = "npm test" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File ../../tasks/scripts/typescript-test.ps1" ["sdk:ts:ci"] description = "TypeScript SDK checks (proto lint, Biome lint, codegen, typecheck, test, build)" From 42ccd430c5f899a42d8536778e16b5f43527d339 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 15 Sep 2026 10:44:10 -0700 Subject: [PATCH 2/2] fix(ci): install native TypeScript dependencies before validation Signed-off-by: Prekshi Vyas --- tasks/scripts/typescript-install.ps1 | 40 ++++++++++++++++++++++++++++ tasks/scripts/typescript-test.ps1 | 36 ------------------------- tasks/typescript.toml | 10 ++----- 3 files changed, 42 insertions(+), 44 deletions(-) create mode 100644 tasks/scripts/typescript-install.ps1 delete mode 100644 tasks/scripts/typescript-test.ps1 diff --git a/tasks/scripts/typescript-install.ps1 b/tasks/scripts/typescript-install.ps1 new file mode 100644 index 0000000000..b54aaeda77 --- /dev/null +++ b/tasks/scripts/typescript-install.ps1 @@ -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." + } +} diff --git a/tasks/scripts/typescript-test.ps1 b/tasks/scripts/typescript-test.ps1 deleted file mode 100644 index d2e581e89c..0000000000 --- a/tasks/scripts/typescript-test.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -# 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 -eq 'arm64') { - $bindingVersion = (& node -p "require('./package-lock.json').packages['node_modules/rolldown'].version").Trim() - if ($LASTEXITCODE -ne 0 -or $bindingVersion -notmatch '^\d+\.\d+\.\d+([+-][0-9A-Za-z.-]+)?$') { - throw 'Unable to determine the locked Rolldown version.' - } - $previousCpu = $env:npm_config_cpu - try { - $env:npm_config_cpu = 'arm64' - # Keep lockfile resolution enabled: disabling it silently upgrades - # unrelated test dependencies instead of testing the pinned SDK tree. - & npm install --no-save "@rolldown/binding-win32-arm64-msvc@$bindingVersion" - if ($LASTEXITCODE -ne 0) { - throw "Unable to install the ARM64 Rolldown test binding (exit code $LASTEXITCODE)." - } - } - finally { - $env:npm_config_cpu = $previousCpu - } -} - -& npm test -exit $LASTEXITCODE diff --git a/tasks/typescript.toml b/tasks/typescript.toml index 5478457b8c..57277e05ba 100644 --- a/tasks/typescript.toml +++ b/tasks/typescript.toml @@ -9,7 +9,7 @@ description = "Install TypeScript SDK dependencies" dir = "sdk/typescript" run = "npm ci" -run_windows = "set npm_config_cpu=x64&& npm ci" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File ../../tasks/scripts/typescript-install.ps1" hide = true ["sdk:ts:proto"] @@ -40,14 +40,12 @@ description = "Lint + format-check the TypeScript SDK (Biome, read-only)" depends = ["sdk:ts:install"] dir = "sdk/typescript" run = "npm run lint" -run_windows = ".\\node_modules\\@biomejs\\cli-win32-x64\\biome.exe ci ." ["sdk:ts:format"] description = "Format the TypeScript SDK and apply safe fixes (Biome, writes)" depends = ["sdk:ts:install"] dir = "sdk/typescript" run = "npm run format" -run_windows = ".\\node_modules\\@biomejs\\cli-win32-x64\\biome.exe check --write ." ["sdk:ts:build"] description = "Build the TypeScript SDK (emit dist/)" @@ -57,13 +55,9 @@ run = "npm run build" ["sdk:ts:test"] description = "Run TypeScript SDK unit tests (Vitest, in-memory transport)" -# On Windows ARM64, typescript-test.ps1 installs the native Rolldown binding -# into node_modules. Keep type checking ahead of that mutation when the -# aggregate `ci` task schedules `check` and `test` concurrently. -depends = ["sdk:ts:typecheck"] +depends = ["sdk:ts:proto"] dir = "sdk/typescript" run = "npm test" -run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File ../../tasks/scripts/typescript-test.ps1" ["sdk:ts:ci"] description = "TypeScript SDK checks (proto lint, Biome lint, codegen, typecheck, test, build)"