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:
ShahriarLak
2026-08-16 08:48:01 -07:00
committed by Garry Tan
parent 69c1b3d88a
commit ea780fed61
3 changed files with 164 additions and 19 deletions
+26 -13
View File
@@ -41,7 +41,7 @@ import { ensureSourceRegistered, sourcePageCount, parseSourcesList, cycleComplet
import { detectAutopilot, decideSourceRemove, decideCodeSync } from "../lib/gbrain-guards";
import { writeReceipt } from "../lib/egress-receipt";
import { localEngineStatus, type LocalEngineStatus } from "../lib/gbrain-local-status";
import { buildGbrainEnv, spawnGbrain, execGbrainJson, NEEDS_SHELL_ON_WINDOWS } from "../lib/gbrain-exec";
import { buildGbrainEnv, spawnGbrain, execGbrainJson, NEEDS_SHELL_ON_WINDOWS, bashScriptInvocation } from "../lib/gbrain-exec";
import { repoPolicyTier as sharedRepoPolicyTier } from "../lib/gbrain-repo-policy-client";
import { checkOwnedStagingDir } from "../lib/staging-guard";
@@ -1245,18 +1245,31 @@ function runBrainSyncPush(args: CliArgs): StageResult {
return { name: "brain-sync", ran: false, ok: true, duration_ms: 0, summary: "skipped (gstack-brain-sync not installed)" };
}
// #1731: gstack-brain-sync is a bash shebang script; Windows can't spawn it
// without a shell, which surfaced as "brain-sync exited undefined".
spawnSync(brainSyncPath, ["--discover-new"], {
stdio: args.quiet ? ["ignore", "ignore", "ignore"] : ["ignore", "inherit", "inherit"],
timeout: 60 * 1000,
shell: NEEDS_SHELL_ON_WINDOWS,
});
const result = spawnSync(brainSyncPath, ["--once"], {
stdio: args.quiet ? ["ignore", "ignore", "ignore"] : ["ignore", "inherit", "inherit"],
timeout: 60 * 1000,
shell: NEEDS_SHELL_ON_WINDOWS,
});
// gstack-brain-sync is a bash shebang script, so it needs an INTERPRETER, not
// a shell. #1731 gave it `shell: NEEDS_SHELL_ON_WINDOWS`, which is right for
// the gbrain.cmd shim and useless here: cmd.exe resolves .cmd/.bat via PATHEXT
// and rejects an extension-less shebang script outright ("is not recognized as
// an internal or external command"), so this stage failed on EVERY Windows run
// while looking like a single red line in an otherwise green report. See
// bashScriptInvocation.
const discover = bashScriptInvocation(brainSyncPath, ["--discover-new"]);
const once = bashScriptInvocation(brainSyncPath, ["--once"]);
if (!discover || !once) {
return {
name: "brain-sync",
ran: false,
ok: true,
duration_ms: Date.now() - t0,
summary: "skipped (no bash found; set GSTACK_BASH to your Git bash.exe)",
};
}
const stdio: "ignore"[] | ("ignore" | "inherit")[] = args.quiet
? ["ignore", "ignore", "ignore"]
: ["ignore", "inherit", "inherit"];
spawnSync(discover.cmd, discover.argv, { stdio, timeout: 60 * 1000, shell: discover.shell });
const result = spawnSync(once.cmd, once.argv, { stdio, timeout: 60 * 1000, shell: once.shell });
return {
name: "brain-sync",