name: Windows Setup E2E # End-to-end fresh-install gate for Windows. Builds a clean windows-latest # checkout and asserts the build completes, binaries resolve via # find-browse, and the gstack-paths state root resolves cleanly. Catches Bun # shell-parser regressions in package.json's build chain (#1538, #1537, # #1530, #1457, #1561) before they reach users. Then it runs `./setup` into # an isolated HOME and drives the installed runtime root's browse from an # unrelated directory: a piped two-command `chain` flow must print the page # text (the runtime root has no node_modules, so this is the checkout-bundle # path from #3026, and the piped-stdin path from #3039). # # Separate from windows-free-tests.yml because that one runs a curated # unit-test subset; this one exercises the install path itself. # # Runner: GitHub-hosted free windows-latest. ~6-10 min total. on: pull_request: branches: [main] paths: - 'package.json' - 'scripts/build.sh' - 'scripts/write-version-files.sh' - 'setup' - 'browse/src/cli.ts' - 'browse/src/find-browse.ts' - 'browse/src/server.ts' - 'browse/src/meta-commands.ts' - 'browse/scripts/build-node-server.sh' - 'bin/gstack-paths' - '.github/workflows/windows-setup-e2e.yml' workflow_dispatch: concurrency: # PR-number keyed: head_ref carries no fork prefix, so same-name branches # from two forks would share one group and cancel each other's runs. group: windows-setup-e2e-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true # Install-path exercise only — no token writes. permissions: contents: read jobs: windows-setup: runs-on: windows-latest timeout-minutes: 25 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.4.2 # Same lockfile-keyed install cache as windows-free-tests.yml (install # was 45s of a 64s job, all network). - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ~/.bun/install/cache key: windows-bun-${{ hashFiles('bun.lock') }} # A lockfile bump starts from the previous cache instead of cold # (restore alone costs ~43s; without this a bump pays it for nothing). restore-keys: | windows-bun- - name: Configure git identity run: | git config --global user.email "windows-setup-e2e@gstack.test" git config --global user.name "Windows Setup E2E" git config --global init.defaultBranch main shell: bash - name: Install dependencies run: bun install --frozen-lockfile shell: bash - name: Run bun run build (the previously-broken path) # This is the regression gate. Bun's Windows shell parser rejected # multiple constructs the old inline build chain used; the wave # moved the build to scripts/build.sh. If this step fails on # Windows, the build chain regressed. run: bun run build shell: bash env: GSTACK_SKIP_PLAYWRIGHT: '1' - name: Verify binaries exist (with .exe extension on Windows) run: | set -e test -f browse/dist/browse.exe || test -f browse/dist/browse || (echo "MISSING: browse" && exit 1) test -f browse/dist/find-browse.exe || test -f browse/dist/find-browse || (echo "MISSING: find-browse" && exit 1) test -f design/dist/design.exe || test -f design/dist/design || (echo "MISSING: design" && exit 1) test -f bin/gstack-global-discover.exe || test -f bin/gstack-global-discover || (echo "MISSING: gstack-global-discover" && exit 1) echo "All binaries present" shell: bash - name: Verify find-browse resolves to the .exe variant run: | set -e OUT=$(bun browse/src/find-browse.ts 2>&1) || true echo "find-browse output: $OUT" # On Windows, find-browse should successfully resolve to a binary, # whether or not it has the .exe extension on disk. Empty output # or "not found" means the .exe extension resolver regressed. echo "$OUT" | grep -qE '(browse\.exe|browse)$' || (echo "find-browse failed to resolve binary on Windows" && exit 1) shell: bash - name: Verify gstack-paths state root resolves run: | set -e eval "$(bash bin/gstack-paths)" test -n "$GSTACK_STATE_ROOT" || (echo "GSTACK_STATE_ROOT empty" && exit 1) test -n "$PLAN_ROOT" || (echo "PLAN_ROOT empty" && exit 1) test -n "$TMP_ROOT" || (echo "TMP_ROOT empty" && exit 1) echo "GSTACK_STATE_ROOT=$GSTACK_STATE_ROOT" echo "PLAN_ROOT=$PLAN_ROOT" echo "TMP_ROOT=$TMP_ROOT" shell: bash - name: Install with ./setup into an isolated HOME # A real install: the codex host's runtime root is a copy of the # runtime files with no node_modules beside it. setup installs # Playwright's Chromium (best-effort) for the journey below. run: | set -euo pipefail E2E_HOME="$RUNNER_TEMP/gstack-e2e-home" mkdir -p "$E2E_HOME" HOME="$E2E_HOME" USERPROFILE="$(cygpath -w "$E2E_HOME")" ./setup --host codex ROOT="$E2E_HOME/.codex/skills/gstack" test -f "$ROOT/.source-path" || (echo "MISSING: $ROOT/.source-path" && exit 1) test ! -e "$ROOT/node_modules" || (echo "UNEXPECTED: $ROOT/node_modules (the journey must exercise the copy without it)" && exit 1) echo "runtime root: $ROOT -> source $(cat "$ROOT/.source-path")" shell: bash - name: Installed browse runs a piped chain flow from an unrelated directory run: | set -euo pipefail E2E_HOME="$RUNNER_TEMP/gstack-e2e-home" USERPROFILE="$(cygpath -w "$E2E_HOME")" export HOME="$E2E_HOME" USERPROFILE BROWSE="$E2E_HOME/.codex/skills/gstack/browse/dist/browse.exe" [ -f "$BROWSE" ] || BROWSE="$E2E_HOME/.codex/skills/gstack/browse/dist/browse" test -f "$BROWSE" || (echo "MISSING: installed browse" && exit 1) bun -e 'Bun.serve({ hostname: "127.0.0.1", port: 47123, fetch: () => new Response("