diff --git a/.github/workflows/windows-scoped-tests.yml b/.github/workflows/windows-scoped-tests.yml new file mode 100644 index 000000000..1675d15e7 --- /dev/null +++ b/.github/workflows/windows-scoped-tests.yml @@ -0,0 +1,206 @@ +name: Windows Scoped Tests + +# The path-scoped Windows free shard (ship-90-90 plan D). windows-smoke.yml is +# still the required Windows check on every pull request, and the full +# curated suite (windows-free-tests.yml) still runs nightly on main. This +# workflow sits between them: when a pull request touches path, file-name or +# process code, one windows-latest job runs the test files the pull request +# changed that are in the curated Windows-safe subset. +# +# "Path, file-name or process code" (the windows-scope filter step): +# - file names: anything under a bin/ directory (shell entry points and +# skill hooks such as freeze/bin/check-freeze.sh, #3758), shell and +# PowerShell scripts, hosts/*/hooks, setup, bun.lock, the Windows +# curation (scripts/test-free-shards.ts, scripts/lib/windows-curation.ts, +# the Windows duration seed, windows-free-tests.yml), and code files whose +# names say path, spawn, exec, process, shell, launch, win32, windows, +# tmp, temp, symlink, state-root, slug, claude-bin, bun-isolation or +# script-invocation; +# - added or removed code lines that spawn processes or spell paths: +# child_process, spawn/exec calls, Bun.spawn, Bun.$, path.win32 and the +# other separator-sensitive path calls, process.platform/execPath, win32, +# tmpdir(), /dev/null, /tmp/, symlink, realpath, chmod, cygpath, +# USERPROFILE, homedir(), fileURLToPath and .exe. #3829's Windows break was +# one such line in a test it added: spawnSync(process.execPath, [..., +# '--config=/dev/null']), which native Windows Bun cannot open. +# test/windows-path-scope-workflow.test.ts proves the filter catches #3758 +# and #3829 and passes over a docs-only and a pure-logic change. +# +# The windows-scoped-tests job is the check to read: it passes when nothing +# is in scope (no Windows job ran) and otherwise requires the Windows job, so +# a skipped run reports success. It is not one of the required checks, but +# gstack-ship-queue ready and land refuse a head with any failed check, so a +# red Windows job blocks the pull request it runs on: in a pull request that +# touches path, file-name or process code, a changed curated test file has to +# pass on Windows. A change to path or process code with no changed curated +# test selects nothing; the nightly suite covers it. +# gstack-pregate reads a job keyed on a dispatch input as manual, never a PR +# obligation: this check runs on the pull request itself. A workflow_dispatch +# run with the files input reruns chosen Windows-safe files on a branch. + +on: + pull_request: + branches: [main] + workflow_dispatch: + inputs: + files: + description: Space-separated Windows-safe free test files to run (from bun run test:windows --list) + type: string + required: true + +concurrency: + group: windows-scoped-tests-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +# Test-only lane — no token writes. +permissions: + contents: read + +jobs: + windows-scope: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + files: ${{ steps.select.outputs.files }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + if: github.event_name == 'pull_request' + with: + fetch-depth: 2 + persist-credentials: false + - id: filter + name: Does the diff touch path, file-name or process code? + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + git diff --name-only --no-renames HEAD^1 HEAD > "$RUNNER_TEMP/changed-files" + code_re='\.(ts|tsx|js|mjs|cjs|sh|bash|ps1|cmd|bat)$|(^|/)bin/[^/.]+$|(^|/)setup$' + path_re='(^|/)bin/|\.(sh|bash|ps1|cmd|bat)$|(^|/)hosts/[^/]+/hooks/|(^|/)setup$|^bun\.lock$|^scripts/test-free-shards\.ts$|^scripts/lib/windows-curation\.ts$|^scripts/free-test-durations-windows\.json$|^\.github/workflows/windows-free-tests\.yml$' + name_re='(path|spawn|exec|process|shell|launch|win32|windows|tmp|temp|symlink|state-root|slug|claude-bin|bun-isolation|script-invocation)[^/]*$' + content_re='child_process|\b(spawn|spawnSync|execSync|execFile|execFileSync)[[:space:]]*\(|Bun\.(spawn|spawnSync|\$)|\bpath\.(win32|posix|sep|delimiter|relative|isAbsolute|normalize)\b|process\.(platform|execPath)|\bwin32\b|tmpdir\(|/dev/null|/tmp/|symlink|realpath|chmod|cygpath|USERPROFILE|homedir\(|fileURLToPath|\.exe\b' + { + grep -E "$path_re" "$RUNNER_TEMP/changed-files" | sed 's/^/path: /' || true + grep -E "$code_re" "$RUNNER_TEMP/changed-files" | grep -E "$name_re" | sed 's/^/name: /' || true + { grep -E "$code_re" "$RUNNER_TEMP/changed-files" || true; } | while IFS= read -r file; do + git diff -U0 --no-renames HEAD^1 HEAD -- "$file" | grep -E '^[+-]' | grep -vE '^(\+\+\+|---) ' | grep -m1 -E "$content_re" | awk -v f="$file" '{ print "content: " f ": " $0 }' || true + done + } > "$RUNNER_TEMP/windows-scope" + if [ -s "$RUNNER_TEMP/windows-scope" ]; then + echo 'windows=true' >> "$GITHUB_OUTPUT" + { echo 'This pull request touches path, file-name or process code:'; echo '~~~'; head -n 40 "$RUNNER_TEMP/windows-scope"; echo '~~~'; } >> "$GITHUB_STEP_SUMMARY" + else + echo 'windows=false' >> "$GITHUB_OUTPUT" + echo 'This pull request touches no path, file-name or process code, so no Windows job runs. windows-smoke still runs on every pull request.' >> "$GITHUB_STEP_SUMMARY" + fi + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + if: steps.filter.outputs.windows == 'true' + with: + bun-version: 1.4.2 + - id: select + name: Pick the changed test files in the curated Windows-safe subset + if: steps.filter.outputs.windows == 'true' + run: | + set -euo pipefail + GITHUB_STEP_SUMMARY=/dev/null bun run scripts/test-free-shards.ts --windows-only --ci-plan "$RUNNER_TEMP/curated.json" --shards 1 > /dev/null + jq -r '.shards[].files[]' "$RUNNER_TEMP/curated.json" | LC_ALL=C sort -u > "$RUNNER_TEMP/curated" + LC_ALL=C sort -u "$RUNNER_TEMP/changed-files" | LC_ALL=C comm -12 - "$RUNNER_TEMP/curated" > "$RUNNER_TEMP/selected" + if [ ! -s "$RUNNER_TEMP/selected" ]; then + echo 'No changed test file is in the curated Windows-safe subset, so no Windows job runs.' >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + { echo 'files<> "$GITHUB_OUTPUT" + { echo "The Windows job runs $(wc -l < "$RUNNER_TEMP/selected" | tr -d ' ') changed Windows-safe test file(s):"; sed 's/^/- /' "$RUNNER_TEMP/selected"; } >> "$GITHUB_STEP_SUMMARY" + + windows-scoped-shard: + needs: windows-scope + if: ${{ (github.event_name == 'workflow_dispatch' && inputs.files != '') || needs.windows-scope.outputs.files != '' }} + runs-on: windows-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.4.2 + + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24.18.0 + + # The same inputs as a windows-free-tests.yml shard, with no bun install + # cache (a cold install beats restoring it on windows-latest). + - name: Configure git identity (required by tests that init temp repos) + run: | + git config --global user.email "windows-ci@gstack.test" + git config --global user.name "Windows CI" + git config --global init.defaultBranch main + shell: bash + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build server-node.mjs (required by Windows browse path) + run: bash browse/scripts/build-node-server.sh + shell: bash + + - name: Generate host SKILL.md outputs (.agents, .factory) + run: bun run gen:skill-docs --host all + shell: bash + + - name: Install Chromium for the Node worker smoke + run: bunx playwright install chromium + + - name: Run the scoped Windows-safe test files + env: + FILES: ${{ inputs.files || needs.windows-scope.outputs.files }} + GSTACK_FREE_RETRY_FLAKY: '1' + GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl + TEMP: ${{ runner.temp }} + TMP: ${{ runner.temp }} + run: | + set -euo pipefail + printf '%s\n' "$FILES" | tr -d '\r' | tr ' ' '\n' | sed '/^$/d' > "$RUNNER_TEMP/windows-scoped-files.txt" + bun run scripts/test-free-shards.ts --files-from "$RUNNER_TEMP/windows-scoped-files.txt" + shell: bash + + - name: Upload full test logs + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: windows-scoped-test-logs + path: | + .context/free-test-logs/gstack-free-test-*.log + ${{ runner.temp }}/gstack-free-test-*.log + include-hidden-files: true + if-no-files-found: ignore + + - name: Upload flake ledger + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: flake-ledger-windows-scoped + path: ${{ runner.temp }}/flake-ledger.jsonl + if-no-files-found: ignore + retention-days: 90 + + windows-scoped-tests: + needs: [windows-scope, windows-scoped-shard] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Pass when nothing is in scope, else require the Windows job + env: + SCOPE_RESULT: ${{ needs.windows-scope.result }} + SHARD_RESULT: ${{ needs.windows-scoped-shard.result }} + FILES: ${{ inputs.files || needs.windows-scope.outputs.files }} + run: | + set -eu + test "$SCOPE_RESULT" = success + if [ -z "$FILES" ]; then + echo 'Nothing in scope (no changed Windows-safe test file in a pull request that touches path, file-name or process code); passing without a Windows run.' >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + test "$SHARD_RESULT" = success diff --git a/.github/workflows/windows-setup-e2e.yml b/.github/workflows/windows-setup-e2e.yml index 2ca74e7b4..37cee929a 100644 --- a/.github/workflows/windows-setup-e2e.yml +++ b/.github/workflows/windows-setup-e2e.yml @@ -32,6 +32,13 @@ name: Windows Setup E2E # Separate from windows-free-tests.yml because that one runs a curated # unit-test subset; this one exercises the install path itself. # +# Runs by path (ship-90-90 plan D): the paths below are the install files +# this workflow runs or ./setup sources or runs. The windows-scope job then +# drops a pull request whose only install change is package.json's version +# line, which is what the merge-train stamp commit puts on every PR head. +# windows-scope's list must match the paths list (pinned by +# test/windows-path-scope-workflow.test.ts). +# # Runner: GitHub-hosted free windows-latest, two parallel jobs. ~10-25 min. on: @@ -39,6 +46,7 @@ on: branches: [main] paths: - 'package.json' + - 'bun.lock' - 'scripts/build.sh' - 'scripts/build-cso.sh' - 'scripts/build-cso-windows.ps1' @@ -64,6 +72,18 @@ on: - 'bin/gstack-claude-root.sh' - 'bin/gstack-browser-ensure' - 'bin/gstack-path-spelling.sh' + - 'bin/gstack-config' + - 'bin/gstack-patch-names' + - 'bin/gstack-state-root.sh' + - 'bin/gstack-skill-name.sh' + - 'bin/gstack-install-registry.sh' + - 'bin/gstack-render-claude.sh' + - 'bin/gstack-host-renders.sh' + - 'bin/gstack-migrate-claude-code' + - 'bin/gstack-settings-hook' + - 'bin/gstack-relink' + - 'bin/gstack-harness' + - 'scripts/preflight-codex-overlap.ts' - 'lib/path-spelling.ts' - 'hosts/claude/hooks/**' - 'gstack-upgrade/**' @@ -81,7 +101,94 @@ permissions: contents: read jobs: + windows-scope: + # The paths filter above starts this workflow; this job decides whether + # the Windows jobs run. A workflow_dispatch run always runs them. + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + install: ${{ steps.scope.outputs.install }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + if: github.event_name == 'pull_request' + with: + fetch-depth: 2 + persist-credentials: false + - id: scope + name: Run unless the only install change is package.json's version line + run: | + set -euo pipefail + if [ "$GITHUB_EVENT_NAME" != pull_request ]; then + echo 'install=true' >> "$GITHUB_OUTPUT" + exit 0 + fi + pkg_version_only=true + if git diff -U0 --no-renames HEAD^1 HEAD -- package.json | grep -E '^[+-]' | grep -vE '^(\+\+\+|---) ' \ + | grep -vE '^[+-] "version": "[^"]*",?$' > /dev/null; then + pkg_version_only=false + fi + git diff --name-only --no-renames HEAD^1 HEAD | while IFS= read -r file; do + if [ "$file" = package.json ] && [ "$pkg_version_only" = true ]; then continue; fi + while IFS= read -r glob; do + # shellcheck disable=SC2254 + case "$file" in ${glob//\*\*/*}) echo "$file"; break ;; esac + done <<'PATHS' + package.json + bun.lock + scripts/build.sh + scripts/build-cso.sh + scripts/build-cso-windows.ps1 + scripts/write-version-files.sh + setup + setup.ps1 + test/setup-ps1.test.ts + docs/troubleshooting.md + browse/src/cli.ts + browse/src/find-browse.ts + browse/src/server.ts + browse/src/meta-commands.ts + browse/scripts/build-node-server.sh + design/src/cli.ts + make-pdf/src/cli.ts + bin/gstack-global-discover.ts + bin/gstack-paths + bin/gstack-doctor + bin/gstack-launch-probe.sh + bin/gstack-hook-check + bin/gstack-session-update + bin/gstack-ff-update + bin/gstack-claude-root.sh + bin/gstack-browser-ensure + bin/gstack-path-spelling.sh + bin/gstack-config + bin/gstack-patch-names + bin/gstack-state-root.sh + bin/gstack-skill-name.sh + bin/gstack-install-registry.sh + bin/gstack-render-claude.sh + bin/gstack-host-renders.sh + bin/gstack-migrate-claude-code + bin/gstack-settings-hook + bin/gstack-relink + bin/gstack-harness + scripts/preflight-codex-overlap.ts + lib/path-spelling.ts + hosts/claude/hooks/** + gstack-upgrade/** + .github/workflows/windows-setup-e2e.yml + PATHS + done > "$RUNNER_TEMP/install-files" + if [ -s "$RUNNER_TEMP/install-files" ]; then + echo 'install=true' >> "$GITHUB_OUTPUT" + { echo 'Install files changed, so the Windows jobs run:'; sed 's/^/- /' "$RUNNER_TEMP/install-files"; } >> "$GITHUB_STEP_SUMMARY" + else + echo 'install=false' >> "$GITHUB_OUTPUT" + echo "The only install change is package.json's version line (the merge-train stamp), so the Windows jobs do not run." >> "$GITHUB_STEP_SUMMARY" + fi + windows-setup: + needs: windows-scope + if: needs.windows-scope.outputs.install == 'true' runs-on: windows-latest timeout-minutes: 35 @@ -377,6 +484,8 @@ jobs: # docs/troubleshooting.md, against the runner's real ~/.claude install # path ($HOME in PowerShell). GSTACK_E2E_POWERSHELL and # GSTACK_E2E_UPDATER_PATH_PREFIX exist only to run these steps off Windows. + needs: windows-scope + if: needs.windows-scope.outputs.install == 'true' runs-on: windows-latest timeout-minutes: 50 env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9332270ac..e5b016d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,79 @@ # Changelog +## [1.94.12.0] - 2026-11-03 + +### Fixed + +- **Six markup tricks no longer pass a remote-loading or scripted SVG off as a chart (GST-940).** `</style>` or `<!--` inside a `` let CSS that loads a remote image read as a plain drawing, and a non-ASCII namespace prefix such as `` hid a script. Any of these could get the SVG a light review. The style walker now reads the `` let CSS that loads a remote image read as a plain drawing, and a non-ASCII namespace prefix such as `` hid a script. Any of these could get the SVG a light review. The style walker now reads the `