From 361dfebcbe90109c1181837817d2bdacdf759dc6 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 22 Aug 2026 01:59:16 +0000 Subject: [PATCH] fix(bin): route remaining Windows-reachable mkdirSync sites through mkdirpSync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweep follow-up to #2641's lib/fs-utils.ts helper: bun on Windows throws EEXIST from a recursive mkdir on an existing dir, so every unguarded recursive mkdirSync on a Windows-reachable path is a latent crash. Converted: bin/gstack-decision-log (unguarded, runs on every decision log — the second call on any machine hits the pre-existing projects dir), bin/gstack-evidence logsDir + ledger dir sites, and bin/gstack-redact-prepush's skip-log site (already try-wrapped, so its failure mode was a silent skip-log loss rather than a crash — the fix makes the log survive). The ~15 remaining gbrain/mac-lane sites are deliberately left alone. Regression: fs-utils.test.ts drives gstack-decision-log twice, the second run under the bun-Windows EEXIST preload fixture — the pre-sweep code exits 1 with EEXIST there; verified red against v1.68.3.0. --- bin/gstack-decision-log | 6 ++++-- bin/gstack-evidence | 7 ++++--- bin/gstack-redact-prepush | 6 +++++- test/fs-utils.test.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/bin/gstack-decision-log b/bin/gstack-decision-log index 6ee838955..bfe27acc6 100755 --- a/bin/gstack-decision-log +++ b/bin/gstack-decision-log @@ -14,8 +14,8 @@ * validateDecide; a rejected decision exits 1 with a message, nothing persisted. */ -import { mkdirSync } from "fs"; import { dirname } from "path"; +import { mkdirpSync } from "../lib/fs-utils"; import { spawnSync } from "child_process"; import { decisionPaths, @@ -33,7 +33,9 @@ const HERE = import.meta.dir; const args = process.argv.slice(2); const slug = resolveSlug(`${HERE}/gstack-slug`); const paths = decisionPaths(slug); -mkdirSync(dirname(paths.log), { recursive: true }); +// mkdirpSync, not bare mkdirSync: bun on Windows throws EEXIST from a +// recursive mkdir on an existing dir (#2635), and this runs on every log call. +mkdirpSync(dirname(paths.log)); function enqueue(): void { // Fire-and-forget cross-machine sync (no-op when artifacts_sync is off). diff --git a/bin/gstack-evidence b/bin/gstack-evidence index ca25df373..8343fe9cf 100755 --- a/bin/gstack-evidence +++ b/bin/gstack-evidence @@ -34,7 +34,8 @@ * in the ledger; it cannot prove that an expected lane ever ran. */ -import { mkdirSync, openSync, writeSync, closeSync, readdirSync, statSync, unlinkSync, chmodSync, readFileSync } from "fs"; +import { openSync, writeSync, closeSync, readdirSync, statSync, unlinkSync, chmodSync, readFileSync } from "fs"; +import { mkdirpSync } from "../lib/fs-utils"; import { join, dirname } from "path"; import { spawnSync } from "child_process"; import { appendJsonl, readJsonl } from "../lib/jsonl-store"; @@ -137,7 +138,7 @@ function pruneOldLogs(logsDir: string): void { /** Exclusive-open a collision-safe log file. Returns undefined on failure. */ function openLog(logsDir: string, label: string, cmdSha: string): { fd: number; path: string } | undefined { try { - mkdirSync(logsDir, { recursive: true }); + mkdirpSync(logsDir); pruneOldLogs(logsDir); const ts = new Date().toISOString().replace(/[:.]/g, "-"); const base = `${ts}-${label}-${process.pid}-${cmdSha.slice(0, 8)}`; @@ -266,7 +267,7 @@ async function cmdRun(argv: string[]): Promise { let paths: ReturnType | undefined; try { paths = ledgerPath(); - mkdirSync(paths.dir, { recursive: true }); + mkdirpSync(paths.dir); } catch (e: any) { warn(`ledger setup failed (${e?.message ?? e}) — result will not be recorded`); } diff --git a/bin/gstack-redact-prepush b/bin/gstack-redact-prepush index 1dac0e94e..3e678f3a3 100755 --- a/bin/gstack-redact-prepush +++ b/bin/gstack-redact-prepush @@ -30,6 +30,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { scan, type Finding } from "../lib/redact-engine"; +import { mkdirpSync } from "../lib/fs-utils"; const ZERO = /^0+$/; // The canonical empty-tree object; diffing against it yields all content as added. @@ -349,7 +350,10 @@ function logSkip(reason: string): void { try { const home = process.env.GSTACK_HOME || path.join(os.homedir(), ".gstack"); const dir = path.join(home, "security"); - fs.mkdirSync(dir, { recursive: true }); + // mkdirpSync, not bare mkdirSync: bun-on-Windows EEXIST (#2635). This site + // is try-wrapped by the caller, so the old failure was a silent skip-log + // loss rather than a crash — the fix makes the log survive, not un-crash. + mkdirpSync(dir); fs.appendFileSync( path.join(dir, "prepush-skip.jsonl"), JSON.stringify({ ts: new Date().toISOString(), reason }) + "\n", diff --git a/test/fs-utils.test.ts b/test/fs-utils.test.ts index 163e29463..6959b8a57 100644 --- a/test/fs-utils.test.ts +++ b/test/fs-utils.test.ts @@ -61,6 +61,33 @@ describe("mkdirpSync", () => { }); }); +describe("swept mkdirp sites under bun-on-Windows EEXIST semantics (#2635)", () => { + const DECISION_LOG = path.resolve(import.meta.dir, "..", "bin", "gstack-decision-log"); + + test("decision-log still writes when its projects dir already exists", () => { + // Proves the sweep WIRING, not just the helper: the first call creates + // ~/.gstack/projects//, the second hits the emulated Windows EEXIST + // on that pre-existing dir — bare mkdirSync crashed here before the sweep. + const base = tmpdir(); + try { + const work = path.join(base, "work"); + fs.mkdirSync(work, { recursive: true }); + const payload = '{"decision":"eexist probe","rationale":"r","scope":"repo","source":"user"}'; + const env = { ...process.env, HOME: base }; + const first = spawnSync("bun", [DECISION_LOG, payload], { cwd: work, encoding: "utf8", env }); + expect(first.status).toBe(0); + const second = spawnSync( + "bun", ["--preload", EEXIST_PRELOAD, DECISION_LOG, payload], + { cwd: work, encoding: "utf8", env }, + ); + expect(second.status).toBe(0); + expect(second.stderr ?? "").not.toContain("EEXIST"); + } finally { + fs.rmSync(base, { recursive: true, force: true }); + } + }); +}); + describe("install-prepush-hook under bun-on-Windows EEXIST semantics (#2635)", () => { test("install succeeds when .git/hooks already exists, existing hook preserved", () => { const base = tmpdir();