From f0748d61578b84effb36fe0b609af84f623dafd7 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 17 Oct 2026 00:02:56 +0000 Subject: [PATCH] ci(pregate): gstack-pregate joins the Windows pregate job; pregate tests return to the Windows lane (#3197) - windows-pregate-e2e runs gstack-pregate run (preflight: regen, secrets through the gstack-redact bun bin, literals, guards) in both journeys, and asserts a missing GSTACK_GIT_BASH refuses with one WTREE_UNAVAILABLE --json envelope. - test/helpers/pregate-fixture.ts and restamp-fixture.ts runBin launch bins through lib/script-invocation.ts (bun bins as Bun argv, bash bins through Git Bash on Windows), which removes the "shebang via CreateProcess" reason for the pregate tests; they move from KNOWN_WINDOWS_INCOMPATIBLE to KNOWN_WINDOWS_SAFE. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- .github/workflows/windows-pregate-e2e.yml | 18 +++++++++++++++--- scripts/lib/windows-curation.ts | 22 ++++++++++++---------- test/helpers/pregate-fixture.ts | 3 ++- test/helpers/restamp-fixture.ts | 3 ++- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/.github/workflows/windows-pregate-e2e.yml b/.github/workflows/windows-pregate-e2e.yml index 3c225d19b..2debe137c 100644 --- a/.github/workflows/windows-pregate-e2e.yml +++ b/.github/workflows/windows-pregate-e2e.yml @@ -11,12 +11,13 @@ name: Windows Pregate E2E # command through `bash -c`) and write (the no-scratch path); # - gstack-evidence records a content fingerprint, its command through bash -c; # - gstack-gate-diff records a candidate with a tree id, not `+nowtree`; +# - gstack-pregate run fingerprints the tree and runs its preflight checks +# (secrets through the gstack-redact bun bin, regen and guards); # - a hook invocation is timed cold and warm (the cached resolution); -# - a missing GSTACK_GIT_BASH refuses with BASH_UNAVAILABLE, not `()`; +# - a missing GSTACK_GIT_BASH refuses with BASH_UNAVAILABLE, not `()`, and +# gstack-pregate --json refuses with one WTREE_UNAVAILABLE envelope; # - the same journey under a pinned, checksum-verified PortableGit found # only through `git --exec-path`. -# `gstack-pregate run` joins this job in the follow-up that migrates the -# pregate consumers (item 10b). # # Runner: GitHub-hosted free windows-latest. ~4-6 min. @@ -39,6 +40,7 @@ on: - 'bin/gstack-evidence' - 'bin/gstack-gate-diff' - 'bin/gstack-redact' + - 'bin/gstack-pregate' - 'hosts/claude/hooks/**' - '.github/workflows/windows-pregate-e2e.yml' workflow_dispatch: @@ -155,6 +157,10 @@ jobs: echo "$OUT" | grep -q 'cannot fingerprint the working tree (gstack-wtree could not start: gstack: no Git Bash found' || (echo "the refusal does not say why" && exit 1) echo "$OUT" | grep -q 'GSTACK_GIT_BASH C:/no-such-git/bin/bash.exe: does not exist' || (echo "the refusal does not list the override" && exit 1) echo "$OUT" | grep -q '(BASH_UNAVAILABLE)' || (echo "the refusal has no result code" && exit 1) + OUT=$(GSTACK_HOME="$T/home" GSTACK_GIT_BASH='C:\no-such-git\bin\bash.exe' bun "$GITHUB_WORKSPACE/bin/gstack-pregate" run --base main --stage preflight --json 2>/dev/null) && CODE=0 || CODE=$? + echo "$OUT" + test "$CODE" -eq 3 || (echo "gstack-pregate exited $CODE, expected 3 (refused)" && exit 1) + echo "$OUT" | bun -e 'const j = JSON.parse(await Bun.stdin.text()); const ok = j.ok === false && j.status === "refused" && j.code === "WTREE_UNAVAILABLE" && j.detail.includes("GSTACK_GIT_BASH C:/no-such-git/bin/bash.exe: does not exist") && j.fix.includes("GSTACK_GIT_BASH"); process.exit(ok ? 0 : 1);' || (echo "gstack-pregate --json did not refuse with one WTREE_UNAVAILABLE envelope" && exit 1) shell: bash - name: Download the pinned PortableGit and verify its checksum @@ -221,6 +227,12 @@ jobs: test "$CODE" -eq 0 || (echo "gstack-gate-diff exited $CODE" && exit 1) ! echo "$OUT" | grep -q 'fingerprint unavailable' || (echo "gate-diff could not fingerprint the tree" && exit 1) echo "$OUT" | grep -qE 'candidate=[0-9a-f]{12}\+[0-9a-f]{12} ' || (echo "gate-diff candidate has no tree id" && exit 1) + + OUT=$(bun "$G/bin/gstack-pregate" run --base main --stage preflight --only regen,secrets,literals,guards --allow-repo-commands 2>&1) || { echo "$OUT"; echo "gstack-pregate run failed"; exit 1; } + echo "$OUT" | tail -n 10 + echo "$OUT" | grep -qE '^preflight secrets pass .* added lines clean' || (echo "pregate secrets did not scan through gstack-redact" && exit 1) + echo "$OUT" | grep -q 'GSTACK_RESULT: skill=pregate status=complete run=' || (echo "pregate did not complete" && exit 1) + grep -qE '"tree": "[0-9a-f]{40}"' .gstack/tmp/pregate.json || (echo "pregate.json has no tree fingerprint" && exit 1) } echo "::group::runner Git for Windows ($(git --version))" (journey) diff --git a/scripts/lib/windows-curation.ts b/scripts/lib/windows-curation.ts index f1267efa6..ec4746fd1 100644 --- a/scripts/lib/windows-curation.ts +++ b/scripts/lib/windows-curation.ts @@ -51,17 +51,10 @@ const WINDOWS_FRAGILE_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [ // when possible; this list is for environment-/runtime-specific tests where // the failure mode is structural rather than detectable via source-file scan. export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }> = [ - // C-pregate: these drive bin/gstack-regen, bin/gstack-pregate, bin/gstack-issue-links and - // bin/gstack-ship-receipt through test/helpers/pregate-fixture.ts runBin (spawnSync on the - // shebang file, which CreateProcess cannot exec) and run `node gen.js` / `node --test` fixture - // commands through the pre-gate's POSIX `bash -o pipefail` runner. The pure parsers and the - // import graph are covered by test/pregate-selection.test.ts, which the bin/ pattern excludes too. - { file: 'test/gstack-regen.test.ts', reason: 'spawns bin/gstack-regen through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and POSIX fixture regen commands' }, - { file: 'test/pregate-guards.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and runs node guard and lane fixture commands through the POSIX runner' }, - { file: 'test/pregate-remote.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and runs POSIX remote-gate fixture commands through the lane runner' }, - { file: 'test/pregate-regate.test.ts', reason: 'spawns bin/gstack-pregate regate through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess)' }, + // C-pregate: these drive bin/gstack-issue-links and bin/gstack-ship-receipt through + // test/helpers/pregate-fixture.ts runBin. The pregate tests moved to KNOWN_WINDOWS_SAFE once + // runBin launched through lib/script-invocation.ts (fix wave PR 5, item 10b). { file: 'test/stamp-templates.test.ts', reason: 'spawns bin/gstack-tree-receipt through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess)' }, - { file: 'test/pregate-invalidation.test.ts', reason: 'spawns bin/gstack-pregate and bin/gstack-ship-receipt through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and the pipefail lane runner' }, { file: 'test/ship-issue-intent.test.ts', reason: 'spawns bin/gstack-issue-links and bin/gstack-ship-receipt through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) with a `gh` shell shim on PATH' }, // PR E2: lane ownership and evidence verification run bin/gstack-lane-check and bin/gstack-evidence // through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess); the lib paths are @@ -288,6 +281,15 @@ export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [ { file: 'test/gstack-config-gbrain-refresh.test.ts', reason: 'E6: runs bin/gstack-config through explicit bash; its gbrain/python3 shims are found by bash PATH lookup, never launched by CreateProcess' }, { file: 'test/hook-scripts.test.ts', reason: '#3067 PowerShell and NotebookEdit hook coverage: every hook runs through spawnSync("bash", [script]) with the payload on stdin, scratch state lives under os.tmpdir(); its /tmp/ literals are payload strings, never opened; symlink, newline-path and POSIX-PATH cases are skipIf win32' }, { file: 'test/gstack-doctor.test.ts', reason: 'runs bin/gstack-doctor and setup --status through explicit bash argv in an os.tmpdir fixture; its bun/codex/claude/probe stubs are found by bash PATH lookup, and expected paths use bash spellings' }, + // Fix wave PR 5 (item 10b, #3197): test/helpers/pregate-fixture.ts and restamp-fixture.ts runBin + // launch bins through lib/script-invocation.ts (bun bins as Bun argv, bash bins through Git Bash), + // and the pregate runner runs fixture commands through the resolved Git Bash. + { file: 'test/gstack-regen.test.ts', reason: 'runs bin/gstack-regen through the fixture runBin (lib/script-invocation.ts) and its regen commands through the resolved Git Bash' }, + { file: 'test/pregate-guards.test.ts', reason: 'runs bin/gstack-pregate through the fixture runBin (lib/script-invocation.ts); guard and lane fixture commands run through the resolved Git Bash' }, + { file: 'test/pregate-remote.test.ts', reason: 'runs bin/gstack-pregate through the fixture runBin (lib/script-invocation.ts); remote-gate fixture commands run through the resolved Git Bash' }, + { file: 'test/pregate-regate.test.ts', reason: 'runs bin/gstack-pregate regate through the restamp fixture runBin (lib/script-invocation.ts)' }, + { file: 'test/pregate-invalidation.test.ts', reason: 'runs bin/gstack-pregate and bin/gstack-ship-receipt through the fixture runBin (lib/script-invocation.ts) and the lane runner through the resolved Git Bash' }, + { file: 'test/pregate-selection.test.ts', reason: 'its bin/ strings are fixture paths; bin/gstack-pregate runs through the fixture runBin (lib/script-invocation.ts) and lanes through the resolved Git Bash' }, { file: 'test/qa-evidence.test.ts', reason: 'invokes the production helper through Bun argv and exercises native Windows job cleanup, private file captures and backpressured receipt output', diff --git a/test/helpers/pregate-fixture.ts b/test/helpers/pregate-fixture.ts index 80ca78bc9..d3ecbdc52 100644 --- a/test/helpers/pregate-fixture.ts +++ b/test/helpers/pregate-fixture.ts @@ -13,6 +13,7 @@ import { spawnSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; +import { runScriptSync } from '../../lib/script-invocation'; import { gitArgvIn } from './scratch-repo'; export const ROOT = path.resolve(import.meta.dir, '..', '..'); @@ -153,7 +154,7 @@ export function commit(repo: FixtureRepo, files: Record, } export function runBin(repo: FixtureRepo, name: string, args: string[], extraEnv: Record = {}, cwd = repo.work) { - const r = spawnSync(BIN(name), args, { cwd, encoding: 'utf8', timeout: 180_000, maxBuffer: 64 * 1024 * 1024, env: { ...process.env, ...repo.env, ...extraEnv } }); + const r = runScriptSync(BIN(name), args, { cwd, encoding: 'utf8', timeout: 180_000, maxBuffer: 64 * 1024 * 1024, env: { ...process.env, ...repo.env, ...extraEnv } }); return { status: r.status, out: r.stdout ?? '', err: r.stderr ?? '' }; } diff --git a/test/helpers/restamp-fixture.ts b/test/helpers/restamp-fixture.ts index a1bf8c1e2..2b6366c99 100644 --- a/test/helpers/restamp-fixture.ts +++ b/test/helpers/restamp-fixture.ts @@ -13,6 +13,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { gitArgvIn } from './scratch-repo'; import { mergeChangelogs } from '../../lib/changelog-check'; +import { runScriptSync } from '../../lib/script-invocation'; export const ROOT = path.resolve(import.meta.dir, '..', '..'); export const BIN = (name: string) => path.join(ROOT, 'bin', name); @@ -84,7 +85,7 @@ export function makePr(repo: FixtureRepo, branch: string, opts: { expects?: stri } export function runBin(repo: FixtureRepo, name: string, args: string[], extraEnv: Record = {}, cwd = repo.work) { - const r = spawnSync(BIN(name), args, { cwd, encoding: 'utf8', timeout: 120_000, env: { ...process.env, ...repo.env, ...extraEnv } }); + const r = runScriptSync(BIN(name), args, { cwd, encoding: 'utf8', timeout: 120_000, env: { ...process.env, ...repo.env, ...extraEnv } }); return { status: r.status, out: r.stdout ?? '', err: r.stderr ?? '' }; }