From 1b88b195c0197c949491ecd6244f1fccdbc0c68d Mon Sep 17 00:00:00 2001 From: ppcvote Date: Fri, 7 Aug 2026 02:58:38 +0800 Subject: [PATCH] fix(tests): resolve Git Bash explicitly so WSL cannot claim `bash` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #206. The release-automation suite spawns an unqualified `bash` in 20 places. Windows resolves that against PATH, and on a machine with WSL enabled C:\Windows\System32\bash.exe is a real executable that frequently precedes Git for Windows. The extracted workflow fragments then run inside WSL, where the Windows node, gh and jq the suite depends on are absent and its Windows path assumptions do not hold, so tests fail for reasons unrelated to the code under test. `resolveBash()` walks PATH and skips the two launcher locations before taking the first bash.exe it finds: %SystemRoot%\System32 and Sysnative (the WSL launcher) and any WindowsApps directory (the Store execution alias, also WSL). Non-Windows platforms keep the bare `bash` they use today. Measured on Windows 10 with Git for Windows 2.52.0, WSL2 enabled and bun 1.3.11, by putting System32 ahead of Git on PATH to match the report: before after System32 first (as reported) 116 pass 69 fail 180 pass 5 fail Git Bash first (default order) 180 pass 5 fail 180 pass 5 fail 64 tests recovered, and the WSL-first environment now produces exactly the result the Git-Bash-first environment does. The 5 remaining failures are `jq: command not found` on this host, present identically before and after and in both PATH orders, so they are an unrelated environment gap rather than something this change leaves behind. PATH is split on `delimiter` from node:path rather than a literal ";" — bun on Windows reports a Windows-style PATH even when launched from Git Bash, so the platform separator is the honest thing to split on. --- .../tests-ts/release-automation.test.ts | 83 ++++++++++++++----- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/sdk/typescript/tests-ts/release-automation.test.ts b/sdk/typescript/tests-ts/release-automation.test.ts index cf5afe6d..63c56d5b 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -1,6 +1,7 @@ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { + existsSync, mkdirSync, mkdtempSync, readFileSync, @@ -8,10 +9,50 @@ import { writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { delimiter, join } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, test } from "bun:test"; +/** + * Path to a POSIX bash for running the extracted workflow fragments. + * + * On Windows, an unqualified "bash" can resolve to C:\Windows\System32\bash.exe, + * the WSL launcher, whenever System32 precedes Git for Windows on PATH. The + * fragments then execute inside WSL, where the Windows node, gh and jq this + * suite relies on are not present and its Windows path assumptions do not hold, + * so a large cluster of release tests fails for reasons unrelated to the code + * under test. Skipping the two launcher locations picks Git Bash instead. + */ +const BASH = resolveBash(); + +function resolveBash(): string { + if (process.platform !== "win32") { + return "bash"; + } + const systemRoot = process.env.SystemRoot ?? "C:\\Windows"; + const launchers = new Set( + [join(systemRoot, "System32"), join(systemRoot, "Sysnative")].map((entry) => + entry.toLowerCase(), + ), + ); + for (const entry of (process.env.PATH ?? "").split(delimiter)) { + const directory = entry.trim().replace(/^"|"$/g, "").replace(/[\\/]+$/, ""); + if (directory.length === 0) { + continue; + } + const normalized = directory.toLowerCase(); + // WindowsApps holds the Store execution alias, which is also WSL. + if (launchers.has(normalized) || normalized.endsWith("\\windowsapps")) { + continue; + } + const candidate = join(directory, "bash.exe"); + if (existsSync(candidate)) { + return candidate; + } + } + return "bash"; +} + type ReleaseMetadata = Record; type ReleaseAutomation = { @@ -1454,7 +1495,7 @@ describe("GitHub release workflow safeguards", () => { "git() { return 0; }", "npm() { printf '%s\\n' '[\"0.1.1\",\"999999999999999999999999.0.0\"]'; }", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mocks}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mocks}\n${script}`], { cwd: fileURLToPath(new URL("../../../", import.meta.url)), encoding: "utf8", env: { @@ -1544,7 +1585,7 @@ describe("GitHub release workflow safeguards", () => { try { const outputPath = join(workspace, "outputs"); - const result = spawnSync("bash", ["-c", `${mocks}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mocks}\n${script}`], { cwd: fileURLToPath(new URL("../../../", import.meta.url)), encoding: "utf8", env: { @@ -1640,7 +1681,7 @@ describe("GitHub release workflow safeguards", () => { " return 1", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mocks}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mocks}\n${script}`], { cwd: fileURLToPath(new URL("../../../", import.meta.url)), encoding: "utf8", env: { @@ -1691,7 +1732,7 @@ describe("GitHub release workflow safeguards", () => { " printf 'created tag at %s\\n' \"$RELEASE_SHA\"", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -1759,7 +1800,7 @@ describe("GitHub release workflow safeguards", () => { " printf 'created exact release tag\\n'", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -1835,7 +1876,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -1934,7 +1975,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -1978,7 +2019,7 @@ describe("GitHub release workflow safeguards", () => { "git() { return 0; }", "sfw() { printf '%s\\n' '[\"0.1.1\",\"999999999999999999999999.0.0\"]'; }", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mocks}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mocks}\n${script}`], { cwd: fileURLToPath(new URL("../../../", import.meta.url)), encoding: "utf8", env: { @@ -2013,7 +2054,7 @@ describe("GitHub release workflow safeguards", () => { "git() { return 0; }", `sfw() { printf '%s\\n' '["0.1.0","${checkedOutVersion}"]'; }`, ].join("\n"); - const result = spawnSync("bash", ["-c", `${mocks}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mocks}\n${script}`], { cwd: fileURLToPath(new URL("../../../", import.meta.url)), encoding: "utf8", env: { @@ -2100,7 +2141,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -2165,7 +2206,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mocks}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mocks}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -2209,7 +2250,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mocks}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mocks}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -2350,7 +2391,7 @@ describe("GitHub release workflow safeguards", () => { " fi", "}", ].join("\n"); - const result = spawnSync("bash", [], { + const result = spawnSync(BASH, [], { input: `${mocks}\n${script}`, encoding: "utf8", env: { @@ -2516,7 +2557,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mocks}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mocks}\n${script}`], { cwd: workspace, encoding: "utf8", env: { @@ -2828,7 +2869,7 @@ describe("GitHub release workflow safeguards", () => { ' command node "$@"', "}", ].join("\n"); - const result = spawnSync("bash", [], { + const result = spawnSync(BASH, [], { input: `${mocks}\n${script}`, encoding: "utf8", env: { @@ -2979,7 +3020,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -3052,7 +3093,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -3113,7 +3154,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -3190,7 +3231,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env, @@ -3243,7 +3284,7 @@ describe("GitHub release workflow safeguards", () => { " esac", "}", ].join("\n"); - const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + const result = spawnSync(BASH, ["-c", `${mock}\n${script}`], { encoding: "utf8", env: { ...process.env,