mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
fix(sync): run gstack-brain-sync through bash, not cmd.exe, on Windows
The brain-sync stage failed on EVERY Windows run with "is not recognized as an internal or external command", so /sync-gbrain always reported ERR brain-sync among otherwise green stages. #1731 gave these spawns shell: NEEDS_SHELL_ON_WINDOWS. That is correct for the gbrain.cmd shim and does nothing here: shell:true routes through cmd.exe, which resolves .cmd/.bat via PATHEXT but has no concept of a shebang, so an extension-less bash script is rejected outright. A .cmd shim needs a shell; a shebang script needs an interpreter. The two cases look identical and are not. The failure was quiet rather than loud. artifacts_sync_mode defaults to pushing curated artifacts to git, so a Windows user's learnings piled up uncommitted in ~/.gstack indefinitely while the sync report showed one red line out of four. New bashScriptInvocation() resolves Git for Windows' bash explicitly and passes the script as argv[0]. It prefers Git bash over a bare `bash` on PATH because WindowsApps ships a bash.exe that is the WSL launcher, which would read C:\... as a Linux path; GSTACK_BASH overrides for unusual installs; forward slashes because bash treats backslashes as escapes; and it returns null when no bash exists so the stage says so plainly instead of surfacing an unactionable spawn error. The #1731 tripwire asserted the shape that does not work, so it now asserts the opposite (never a raw spawnSync(brainSyncPath, ...)) and six unit tests cover the resolver. Verified on Windows: the stage now reports "OK brain-sync curated artifacts pushed (4.2s)" and the artifacts repo committed + pushed on its own. Affected-test set unchanged at 14 pre-existing failures before and after, with 6 new passing tests.
This commit is contained in:
@@ -136,6 +136,66 @@ export function buildGbrainEnv(opts: BuildGbrainEnvOptions = {}): NodeJS.Process
|
||||
*/
|
||||
export const NEEDS_SHELL_ON_WINDOWS = process.platform === "win32";
|
||||
|
||||
/** Where Git for Windows puts bash, most-specific first. */
|
||||
const WINDOWS_BASH_CANDIDATES = [
|
||||
"C:\\Program Files\\Git\\bin\\bash.exe",
|
||||
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
||||
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
||||
];
|
||||
|
||||
export interface ScriptInvocation {
|
||||
cmd: string;
|
||||
argv: string[];
|
||||
/** Always false: we resolve the interpreter ourselves rather than via cmd.exe. */
|
||||
shell: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* How to invoke a **bash shebang script** (`gstack-brain-sync`) on this platform.
|
||||
*
|
||||
* POSIX execs it directly — the shebang does the work. Windows cannot, and
|
||||
* `shell: true` does NOT rescue it: that routes through cmd.exe, which resolves
|
||||
* `.cmd`/`.bat` via PATHEXT but has no concept of a shebang, so an
|
||||
* extension-less bash script comes back as *"is not recognized as an internal
|
||||
* or external command"*. This is why #1731's `shell: NEEDS_SHELL_ON_WINDOWS`
|
||||
* fix genuinely cured the `gbrain.cmd` shim while leaving the brain-sync stage
|
||||
* failing on **every** run on Windows. The two cases look identical and are not:
|
||||
* a `.cmd` shim needs a shell, a shebang script needs an interpreter.
|
||||
*
|
||||
* The consequence was quiet rather than loud. `artifacts_sync_mode` defaults to
|
||||
* pushing curated artifacts to git, so a Windows user's learnings accumulated in
|
||||
* `~/.gstack` and were never committed, while `/sync-gbrain` printed one red
|
||||
* line among four green ones.
|
||||
*
|
||||
* Git for Windows' bash is preferred over a bare `bash` on PATH because
|
||||
* WindowsApps ships a `bash.exe` that is the WSL launcher; if it wins PATH
|
||||
* order it interprets `C:\...` as a Linux path and the script never sees the
|
||||
* repo. `GSTACK_BASH` overrides everything for unusual installs.
|
||||
*
|
||||
* Returns `null` when no bash can be found, so the caller can say so plainly
|
||||
* instead of surfacing a spawn error nobody can act on.
|
||||
*/
|
||||
export function bashScriptInvocation(
|
||||
scriptPath: string,
|
||||
args: string[],
|
||||
opts: { platform?: string; exists?: (p: string) => boolean; env?: NodeJS.ProcessEnv } = {},
|
||||
): ScriptInvocation | null {
|
||||
const platform = opts.platform ?? process.platform;
|
||||
if (platform !== "win32") return { cmd: scriptPath, argv: args, shell: false };
|
||||
|
||||
const exists = opts.exists ?? existsSync;
|
||||
const env = opts.env ?? process.env;
|
||||
|
||||
const override = env.GSTACK_BASH?.trim();
|
||||
const candidates = [...(override ? [override] : []), ...WINDOWS_BASH_CANDIDATES];
|
||||
const bash = candidates.find((p) => exists(p));
|
||||
if (!bash) return null;
|
||||
|
||||
// Forward slashes: bash treats backslashes as escapes, so a Windows path
|
||||
// passed verbatim loses its separators.
|
||||
return { cmd: bash, argv: [scriptPath.replace(/\\/g, "/"), ...args], shell: false };
|
||||
}
|
||||
|
||||
export interface SpawnGbrainOptions {
|
||||
/** Timeout in milliseconds. Defaults to 30s. */
|
||||
timeout?: number;
|
||||
|
||||
Reference in New Issue
Block a user