mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 07:47:27 +01:00
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> # Conflicts: # .github/workflows/free-tests.yml # docs/TESTING_INTERNALS.md # scripts/harness-version.json # scripts/lib/shard-jobs.ts # scripts/test-free-shards.ts # test/shard-jobs.test.ts # test/windows-native-workflows.test.ts
371 lines
16 KiB
YAML
371 lines
16 KiB
YAML
name: Windows Free Tests
|
|
|
|
# Nightly, not a PR gate. Windows has minimal support (docs/support-tiers.md):
|
|
# the required Windows check is windows-smoke.yml (./setup plus the core
|
|
# skills). This workflow runs the curated Windows-safe subset of the free suite
|
|
# on main every night and on workflow_dispatch, and never blocks a pull
|
|
# request. A red nightly run gets a fix only when it shows an install, startup
|
|
# or core-workflow break; other Windows-only failures close as won't fix.
|
|
#
|
|
# scripts/test-free-shards.ts --windows-only is the single source of truth for
|
|
# which files run: POSIX-bound tests (bin/ shebang spawns, raw /tmp paths,
|
|
# /bin/sh, mode bitmasks, the Bun-driven browse server) are excluded by
|
|
# pattern there, with explicit force-include and known-incompatible lists.
|
|
# Every run prints the curated count and top exclusion reasons in the job
|
|
# summary so a shrinking subset is visible.
|
|
#
|
|
# Same plan/run/verify model as free-tests.yml: one duration-balanced plan of
|
|
# the curated files, one shard per Windows job, and an aggregate that requires
|
|
# every strict result and exact coverage. The plan and the verify step are
|
|
# static (file contents and the git revision), so they run on Linux. Packing
|
|
# reads Windows-measured durations (scripts/free-test-durations-windows.json):
|
|
# Windows-only files such as the native cookie job cost minutes there and
|
|
# nothing on Linux. Refresh it with a `record_durations` dispatch and commit
|
|
# the artifact.
|
|
#
|
|
# Native browser qualification campaigns (Windows cookie extraction, launch
|
|
# diagnostics) are dispatch-only in native-qualification.yml.
|
|
#
|
|
# One full suite per tree (GSTA-156, docs/pregate.md#ci-reuse), as in
|
|
# free-tests.yml: the aggregate uploads `tested-tree` only after strict
|
|
# verification passes, and a push to main whose merged pull request already
|
|
# ran this workflow green on the same tree reuses that run. Pull request,
|
|
# dispatch and duration-recording runs never consume.
|
|
|
|
on:
|
|
# 09:17 UTC, about 2 AM Pacific, on the default branch (main).
|
|
schedule:
|
|
- cron: '17 9 * * *'
|
|
workflow_dispatch:
|
|
inputs:
|
|
record_durations:
|
|
description: Record per-file Windows durations (artifact) instead of running the lane
|
|
type: boolean
|
|
default: false
|
|
windows_jobs:
|
|
description: 'Sub-shard experiments only: sub-shards per Windows shard (--shard-jobs), 1-4 (empty keeps WINDOWS_FREE_JOBS). Never cite such a run as a PR result.'
|
|
type: string
|
|
default: ''
|
|
# free-durations-record.yml calls the recording path weekly.
|
|
workflow_call:
|
|
inputs:
|
|
record_durations:
|
|
type: boolean
|
|
default: false
|
|
|
|
# k in `--ci-plan --shard-jobs k`: each Windows shard runs min(k, its parallel
|
|
# files) sandboxed sub-shards side by side (scripts/lib/shard-jobs.ts).
|
|
env:
|
|
WINDOWS_FREE_JOBS: '1'
|
|
|
|
concurrency:
|
|
group: windows-free-${{ github.event.pull_request.number || github.run_id }}
|
|
cancel-in-progress: true
|
|
|
|
# Test-only lane — no token writes.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# Main pushes only, and the one job with actions: read; see free-tests.yml.
|
|
# A skipped or failed reuse job runs every job below as before.
|
|
reuse:
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
actions: read
|
|
outputs:
|
|
reused: ${{ steps.find.outputs.reused }}
|
|
run_id: ${{ steps.find.outputs.run_id }}
|
|
run_url: ${{ steps.find.outputs.run_url }}
|
|
tree: ${{ steps.find.outputs.tree }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- id: find
|
|
name: Find a green pull request or dispatch run of this exact tree
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: bun run scripts/ci-tree-reuse.ts find --workflow windows-free-tests.yml --profile ci-reuse/windows-free-tests
|
|
|
|
windows-plan:
|
|
needs: reuse
|
|
if: ${{ !cancelled() && !inputs.record_durations && (needs.reuse.result != 'success' || needs.reuse.outputs.reused != 'true') }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
outputs:
|
|
matrix: ${{ steps.plan.outputs.matrix }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
with:
|
|
bun-version: 1.4.2
|
|
- id: plan
|
|
name: Curate and balance the Windows-safe free suite
|
|
# Six shards: ~85 s of Linux-measured work each, ~2.5 min on a
|
|
# 4-vCPU Windows runner, so the lane lands near 4 min with setup.
|
|
env:
|
|
GSTACK_FREE_TEST_DURATIONS: scripts/free-test-durations-windows.json
|
|
JOBS_OVERRIDE: ${{ inputs.windows_jobs }}
|
|
run: |
|
|
set -euo pipefail
|
|
jobs="${JOBS_OVERRIDE:-$WINDOWS_FREE_JOBS}"
|
|
case "$jobs" in
|
|
''|*[!0-9]*) echo "::error::windows_jobs '$jobs' is not a whole number"; exit 1 ;;
|
|
esac
|
|
if [ "$jobs" -lt 1 ] || [ "$jobs" -gt 4 ]; then echo "::error::windows_jobs $jobs is outside 1-4"; exit 1; fi
|
|
if [ "$jobs" != "$WINDOWS_FREE_JOBS" ]; then
|
|
echo "- Sub-shard experiment: $jobs sub-shards per Windows shard (default $WINDOWS_FREE_JOBS); not a PR Windows Free Tests result" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
matrix=$(bun run scripts/test-free-shards.ts --windows-only --ci-plan "$RUNNER_TEMP/windows-plan.json" --shards 6 --shard-jobs "$jobs")
|
|
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
|
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: windows-plan
|
|
path: ${{ runner.temp }}/windows-plan.json
|
|
if-no-files-found: error
|
|
|
|
windows-free-shard:
|
|
# gstack-pregate reads a job keyed on a dispatch input as manual, never a
|
|
# PR obligation; this lane runs only nightly or by hand.
|
|
needs: [windows-plan, reuse]
|
|
if: ${{ !cancelled() && !inputs.record_durations && needs.windows-plan.result == 'success' && (needs.reuse.result != 'success' || needs.reuse.outputs.reused != 'true') }}
|
|
runs-on: windows-latest
|
|
timeout-minutes: 15
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJSON(needs.windows-plan.outputs.matrix) }}
|
|
|
|
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
|
|
|
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
with:
|
|
name: windows-plan
|
|
path: ${{ runner.temp }}
|
|
|
|
# No bun install cache: restoring the ~450 MB entry took 33-42 s per
|
|
# shard, a cold install 16-17 s (GSTA-203 PR 1 A/B, 3 runs per arm).
|
|
|
|
- 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)
|
|
# browse/src/cli.ts throws at module load on Windows if server-node.mjs
|
|
# is missing — Bun can't drive Playwright's Chromium on Windows
|
|
# (oven-sh/bun#4253), so any test that transitively loads cli.ts needs
|
|
# the Node-compatible server bundle. The full build is unnecessary.
|
|
run: bash browse/scripts/build-node-server.sh
|
|
shell: bash
|
|
|
|
- name: Generate host SKILL.md outputs (.agents, .factory)
|
|
# Golden-file tests read gitignored generated host outputs.
|
|
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 Windows-safe shard
|
|
env:
|
|
# Same policy as the required Linux lane: an attributed failure gets
|
|
# one serial retry (cap 5 files, truncation veto) and a clean retry
|
|
# is a loud flaky pass, appended to the ledger uploaded below and
|
|
# listed in the aggregate job summary.
|
|
GSTACK_FREE_RETRY_FLAKY: '1'
|
|
GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl
|
|
TEMP: ${{ runner.temp }}
|
|
TMP: ${{ runner.temp }}
|
|
run: bun run test:windows --ci-run "$RUNNER_TEMP/windows-plan.json" --shard ${{ matrix.shard }} --result "$RUNNER_TEMP/windows-results/shard-${{ matrix.shard }}.json"
|
|
shell: bash
|
|
|
|
- name: Upload strict shard result
|
|
if: always()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: windows-result-${{ matrix.shard }}
|
|
path: ${{ runner.temp }}/windows-results/*.json
|
|
if-no-files-found: error
|
|
|
|
# A red lane must carry the WHY: the runner's quiet console names files,
|
|
# not causes, so complete shard logs are kept on every run.
|
|
- name: Upload full shard logs
|
|
if: always()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: windows-free-test-shard-logs-${{ matrix.shard }}
|
|
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-${{ matrix.shard }}
|
|
path: ${{ runner.temp }}/flake-ledger.jsonl
|
|
if-no-files-found: ignore
|
|
retention-days: 90
|
|
|
|
windows-free-tests:
|
|
if: ${{ always() && !inputs.record_durations }}
|
|
needs: [reuse, windows-plan, windows-free-shard]
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Take the reuse path only for a verified main-push reuse
|
|
id: route
|
|
env:
|
|
REUSE_RESULT: ${{ needs.reuse.result }}
|
|
REUSED: ${{ needs.reuse.outputs.reused }}
|
|
REUSE_RUN_ID: ${{ needs.reuse.outputs.run_id }}
|
|
REUSE_RUN_URL: ${{ needs.reuse.outputs.run_url }}
|
|
REUSE_TREE: ${{ needs.reuse.outputs.tree }}
|
|
run: |
|
|
set -eu
|
|
if [ "$REUSE_RESULT" != success ] || [ "$REUSED" != true ]; then
|
|
echo 'reused=false' >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
test "$GITHUB_EVENT_NAME" = push
|
|
test "$GITHUB_REF" = refs/heads/main
|
|
case "$REUSE_RUN_ID" in ''|*[!0-9]*) echo "::error::the reuse job named no run id"; exit 1 ;; esac
|
|
case "$REUSE_TREE" in ''|*[!0-9a-f]*) echo "::error::the reuse job named no tree"; exit 1 ;; esac
|
|
test "$REUSE_RUN_URL" = "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$REUSE_RUN_ID"
|
|
echo "Reused $REUSE_RUN_URL, which passed on tree $REUSE_TREE; this push ran no suite of its own." >> "$GITHUB_STEP_SUMMARY"
|
|
echo 'reused=true' >> "$GITHUB_OUTPUT"
|
|
- name: Require the plan and every Windows shard
|
|
if: steps.route.outputs.reused != 'true'
|
|
env:
|
|
PLAN_RESULT: ${{ needs.windows-plan.result }}
|
|
SHARD_RESULT: ${{ needs.windows-free-shard.result }}
|
|
run: |
|
|
set -eu
|
|
test "$PLAN_RESULT" = success
|
|
test "$SHARD_RESULT" = success
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
if: steps.route.outputs.reused != 'true'
|
|
with:
|
|
persist-credentials: false
|
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
if: steps.route.outputs.reused != 'true'
|
|
with:
|
|
bun-version: 1.4.2
|
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
if: steps.route.outputs.reused != 'true'
|
|
with:
|
|
name: windows-plan
|
|
path: ${{ runner.temp }}
|
|
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
if: steps.route.outputs.reused != 'true'
|
|
with:
|
|
pattern: windows-result-*
|
|
merge-multiple: true
|
|
path: ${{ runner.temp }}/windows-results
|
|
- name: Require exact coverage and complete strict results
|
|
if: steps.route.outputs.reused != 'true'
|
|
run: bun run scripts/test-free-shards.ts --windows-only --ci-verify "$RUNNER_TEMP/windows-plan.json" --results "$RUNNER_TEMP/windows-results"
|
|
# Only after strict verification passed; the reuse path never reaches here.
|
|
- name: Record the tested tree
|
|
if: steps.route.outputs.reused != 'true'
|
|
env:
|
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: bun run scripts/ci-tree-reuse.ts record --plan "$RUNNER_TEMP/windows-plan.json" --results "$RUNNER_TEMP/windows-results" --os Windows --out "$RUNNER_TEMP/tested-tree/tree.json"
|
|
- name: Upload the tested tree
|
|
if: steps.route.outputs.reused != 'true'
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: tested-tree
|
|
path: ${{ runner.temp }}/tested-tree/tree.json
|
|
if-no-files-found: error
|
|
|
|
windows-record-durations:
|
|
if: ${{ inputs.record_durations }}
|
|
runs-on: windows-latest
|
|
timeout-minutes: 30
|
|
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
|
|
- name: Prepare the same inputs as a Windows shard
|
|
shell: bash
|
|
run: |
|
|
git config --global user.email "windows-ci@gstack.test"
|
|
git config --global user.name "Windows CI"
|
|
git config --global init.defaultBranch main
|
|
bun install --frozen-lockfile
|
|
bash browse/scripts/build-node-server.sh
|
|
bun run gen:skill-docs --host all
|
|
bunx playwright install chromium
|
|
# Records into this checkout's default seed path: GSTACK_FREE_TEST_DURATIONS
|
|
# would leak into every timed test, and seed-reading tests would then read
|
|
# the half-written recording instead of their fixtures.
|
|
- name: Time every Windows-safe file alone
|
|
id: record
|
|
shell: bash
|
|
env:
|
|
TEMP: ${{ runner.temp }}
|
|
TMP: ${{ runner.temp }}
|
|
run: |
|
|
set -o pipefail
|
|
bun run test:windows --record-durations --wall-timeout 1200 2>&1 | tee "$RUNNER_TEMP/record.log"
|
|
- name: Name the recording as the Windows seed
|
|
id: seed
|
|
if: always() && steps.record.outcome != 'skipped'
|
|
shell: bash
|
|
run: |
|
|
grep -q 'wrote [0-9]* durations to' "$RUNNER_TEMP/record.log" || { echo "::error::The recording was interrupted; no Windows seed to upload."; exit 1; }
|
|
cp scripts/free-test-durations.json "$RUNNER_TEMP/free-test-durations-windows.json"
|
|
sed -n '/failed while recording:/,$p' "$RUNNER_TEMP/record.log" | sed 's/\r$//' | { grep -E '^ ✗ [^ ]+$' || true; } | sed 's/^ ✗ //' > "$RUNNER_TEMP/record-failures.txt"
|
|
- name: Upload full recording logs
|
|
if: always()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
with:
|
|
name: windows-record-durations-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
|
|
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
if: always() && steps.seed.outcome == 'success'
|
|
with:
|
|
name: free-test-durations-windows
|
|
path: |
|
|
${{ runner.temp }}/free-test-durations-windows.json
|
|
${{ runner.temp }}/record-failures.txt
|
|
if-no-files-found: error
|