mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
ci: windows-smoke becomes the required Windows check; the six-shard Windows suite runs nightly on main (GSTA-203 PR 1, R50)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
83ae96e086
commit
68466832ac
17 files changed
+273
-56
No files matched your search
@@ -161,7 +161,7 @@ jobs:
|
||||
- Files that failed while recording (durations kept; this PR's own free checks decide): $failed_line
|
||||
- Seed growth ratchet (60 s ceiling, shrink-only allowlist): $ratchet_line
|
||||
|
||||
Data only. This run dispatched Free Tests (the required free-tests check), Windows Free Tests and Quality gate on this branch; those are this PR's checks. The pull_request runs wait as "action required" because a token-opened PR never starts them without approval. Stamp and merge per docs/ship-policy.md; the stamp push runs the full PR checks.
|
||||
Data only. This run dispatched Free Tests (the required free-tests check), Windows Smoke and Quality gate on this branch; those are this PR's checks. The pull_request runs wait as "action required" because a token-opened PR never starts them without approval. Stamp and merge per docs/ship-policy.md; the stamp push runs the full PR checks.
|
||||
EOF
|
||||
if [ -n "$number" ]; then
|
||||
gh pr edit "$number" --body-file pr-body.md
|
||||
@@ -169,5 +169,5 @@ jobs:
|
||||
gh pr create --base main --head "$branch" --title "test(seed): refresh the free-suite duration seeds" --body-file pr-body.md
|
||||
fi
|
||||
gh workflow run free-tests.yml --repo "$GITHUB_REPOSITORY" --ref "$branch"
|
||||
gh workflow run windows-free-tests.yml --repo "$GITHUB_REPOSITORY" --ref "$branch"
|
||||
gh workflow run windows-smoke.yml --repo "$GITHUB_REPOSITORY" --ref "$branch"
|
||||
gh workflow run quality-gate.yml --repo "$GITHUB_REPOSITORY" --ref "$branch"
|
||||
@@ -1,9 +1,15 @@
|
||||
name: Windows Free Tests
|
||||
|
||||
# The Windows-safe subset of the free suite on GitHub's free `windows-latest`
|
||||
# runners. 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
|
||||
# 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.
|
||||
@@ -21,13 +27,9 @@ name: Windows Free Tests
|
||||
# diagnostics) are dispatch-only in native-qualification.yml.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
# Also on main pushes, as free-tests.yml does: trains dispatch only the
|
||||
# Linux free workflow, so without this no Windows run ever sees a train's
|
||||
# combined tree or main after a merge.
|
||||
push:
|
||||
branches: [main]
|
||||
# 09:17 UTC, about 2 AM Pacific, on the default branch (main).
|
||||
schedule:
|
||||
- cron: '17 9 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
record_durations:
|
||||
@@ -79,6 +81,9 @@ jobs:
|
||||
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.
|
||||
if: ${{ !inputs.record_durations }}
|
||||
needs: windows-plan
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 15
|
||||
@@ -104,14 +109,8 @@ jobs:
|
||||
name: windows-plan
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
# bun's install cache lives under ~/.bun/install/cache on every platform.
|
||||
- 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-keys: |
|
||||
windows-bun-
|
||||
# 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: |
|
||||
|
||||
@@ -92,16 +92,8 @@ jobs:
|
||||
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-
|
||||
# No bun install cache, as in windows-free-tests.yml: a cold install is
|
||||
# faster on windows-latest than restoring the ~450 MB cache entry.
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
name: Windows Smoke
|
||||
|
||||
# The required Windows check under minimal support (docs/support-tiers.md):
|
||||
# with a default Git for Windows install, ./setup installs and the core skills
|
||||
# start. One windows-latest job, under 5 minutes:
|
||||
# 1. ./setup --host claude into an isolated HOME under Git Bash, with
|
||||
# gstack's own browser (Playwright Chromium).
|
||||
# 2. The core skills start: gstack-skill-start for /ship, /review,
|
||||
# /qa-only, /investigate and /browse answers SKILL_START_PROTO: 1.
|
||||
# 3. /browse on gstack's own browser: the installed browse runs a piped
|
||||
# chain flow against a local page from an unrelated directory.
|
||||
# 4. The fastest Windows-safe free tests for those paths (list below).
|
||||
# The full curated Windows suite is windows-free-tests.yml: nightly on main
|
||||
# and on dispatch, never a PR gate.
|
||||
#
|
||||
# No bun install cache: a cold install took 16-17 s on windows-latest, a
|
||||
# restore of the ~450 MB cache 33-42 s (GSTA-203 PR 1 A/B, 3 runs per arm).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: windows-smoke-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
windows-smoke:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.4.2
|
||||
|
||||
- name: Configure git identity (required by tests that init temp repos)
|
||||
run: |
|
||||
git config --global user.email "windows-smoke@gstack.test"
|
||||
git config --global user.name "Windows Smoke"
|
||||
git config --global init.defaultBranch main
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
shell: bash
|
||||
|
||||
- name: ./setup installs into an isolated HOME
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SMOKE_HOME="$RUNNER_TEMP/smoke-home"
|
||||
mkdir -p "$SMOKE_HOME"
|
||||
HOME="$SMOKE_HOME" USERPROFILE="$(cygpath -w "$SMOKE_HOME")" ./setup --host claude
|
||||
test -x "$SMOKE_HOME/.claude/skills/gstack/bin/gstack-skill-start" || (echo "MISSING: installed gstack-skill-start" && exit 1)
|
||||
shell: bash
|
||||
|
||||
- name: The core skills start
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SMOKE_HOME="$RUNNER_TEMP/smoke-home"
|
||||
USERPROFILE="$(cygpath -w "$SMOKE_HOME")"
|
||||
export HOME="$SMOKE_HOME" USERPROFILE
|
||||
for skill in ship review qa-only investigate browse; do
|
||||
set +e
|
||||
OUT=$(timeout 120 "$SMOKE_HOME/.claude/skills/gstack/bin/gstack-skill-start" --skill "$skill" --model claude 2>&1 < /dev/null)
|
||||
CODE=$?
|
||||
set -e
|
||||
echo "::group::$skill"; echo "$OUT"; echo "::endgroup::"
|
||||
test "$CODE" -eq 0 || (echo "/$skill: gstack-skill-start exited $CODE" && exit 1)
|
||||
grep -q '^SKILL_START_PROTO: 1' <<< "$OUT" || (echo "/$skill: no SKILL_START_PROTO: 1 line" && exit 1)
|
||||
done
|
||||
shell: bash
|
||||
|
||||
- name: /browse on gstack's own browser runs a piped chain flow from an unrelated directory
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SMOKE_HOME="$RUNNER_TEMP/smoke-home"
|
||||
USERPROFILE="$(cygpath -w "$SMOKE_HOME")"
|
||||
export HOME="$SMOKE_HOME" USERPROFILE
|
||||
BROWSE="$SMOKE_HOME/.claude/skills/gstack/browse/dist/browse.exe"
|
||||
[ -f "$BROWSE" ] || BROWSE="$SMOKE_HOME/.claude/skills/gstack/browse/dist/browse"
|
||||
test -f "$BROWSE" || (echo "MISSING: installed browse" && exit 1)
|
||||
bun -e 'Bun.serve({ hostname: "127.0.0.1", port: 47124, fetch: () => new Response("<title>gstack smoke</title><h1>gstack-windows-smoke-ok</h1>", { headers: { "content-type": "text/html" } }) }); setInterval(() => {}, 1 << 30);' &
|
||||
PAGE_PID=$!
|
||||
trap 'kill "$PAGE_PID" 2>/dev/null || true; "$BROWSE" stop >/dev/null 2>&1 || true' EXIT
|
||||
mkdir -p "$RUNNER_TEMP/unrelated-dir" && cd "$RUNNER_TEMP/unrelated-dir"
|
||||
set +e
|
||||
OUT=$(printf '%s\n' '[["goto","http://127.0.0.1:47124/"],["text"]]' | "$BROWSE" chain 2>&1)
|
||||
CODE=$?
|
||||
set -e
|
||||
echo "$OUT"
|
||||
if [ "$CODE" -ne 0 ]; then
|
||||
find "$RUNNER_TEMP/unrelated-dir" "$SMOKE_HOME" -maxdepth 4 \( -name 'browse-daemon.log' -o -name 'browse-startup-error.log' \) -print -exec tail -n 60 {} \; 2>/dev/null || true
|
||||
echo "browse chain exited $CODE" && exit 1
|
||||
fi
|
||||
grep -q 'gstack-windows-smoke-ok' <<< "$OUT" || (echo "chain output lacks the page text: the flow did not run" && exit 1)
|
||||
shell: bash
|
||||
|
||||
- name: Fastest Windows-safe free tests for install and the core skills
|
||||
# Windows-measured seconds from scripts/free-test-durations-windows.json.
|
||||
# setup-windows-fallback (0.1): setup's _link_or_copy copies, never links, on Windows
|
||||
# setup-staged-copy (0.6): a copy install is staged and swapped in only when complete
|
||||
# setup-windows-rerun-refresh (1.7): re-running ./setup refreshes copied skills (#2444)
|
||||
# setup-skill-copies (0.1): copied SKILL.md hashes and the backup of an edited copy
|
||||
# setup-bun-cmd-and-pipe-bugs (0.4): setup's bun.cmd and pipe handling
|
||||
# gstack-launch-probe (0.1): setup's Smart App Control launch probe (#2595)
|
||||
# state-root-parity (2.1): every skill start resolves one state root in TS and bash
|
||||
# bun-isolation-launch (1.1): Bun entries ignore .env and bunfig under Git Bash (#3415)
|
||||
# gstack-lease-git-bash (3.7): the skill-start lease under Git Bash (#3330)
|
||||
# gstack-wtree-windows-paths (1.3): /review and /ship record a start on C:/ paths (#3101)
|
||||
# ship-hook-windows-paths (2.3): /ship's pre-push guard in a Windows worktree (#2805)
|
||||
# qa-only-browser-probe (2.4): /qa-only's browser probe fixture
|
||||
# find-browse (0.1): /browse resolves its binary (.exe)
|
||||
# cli-chain-stdin (0.3): /browse chain reads its flow from stdin on Windows (#3039)
|
||||
# windows-spawn-hide (0.1): /browse's spawns hide their console window (#1835)
|
||||
env:
|
||||
GSTACK_FREE_RETRY_FLAKY: '1'
|
||||
GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl
|
||||
TEMP: ${{ runner.temp }}
|
||||
TMP: ${{ runner.temp }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat > "$RUNNER_TEMP/windows-smoke-files.txt" <<'LIST'
|
||||
test/setup-windows-fallback.test.ts
|
||||
test/setup-staged-copy.test.ts
|
||||
test/setup-windows-rerun-refresh.test.ts
|
||||
test/setup-skill-copies.test.ts
|
||||
test/setup-bun-cmd-and-pipe-bugs.test.ts
|
||||
test/gstack-launch-probe.test.ts
|
||||
test/state-root-parity.test.ts
|
||||
test/bun-isolation-launch.test.ts
|
||||
test/gstack-lease-git-bash.test.ts
|
||||
test/gstack-wtree-windows-paths.test.ts
|
||||
test/ship-hook-windows-paths.test.ts
|
||||
test/qa-only-browser-probe.test.ts
|
||||
browse/test/find-browse.test.ts
|
||||
browse/test/cli-chain-stdin.test.ts
|
||||
browse/test/windows-spawn-hide.test.ts
|
||||
LIST
|
||||
bun run scripts/test-free-shards.ts --files-from "$RUNNER_TEMP/windows-smoke-files.txt"
|
||||
shell: bash
|
||||
|
||||
- name: Upload full test logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: windows-smoke-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-smoke
|
||||
path: ${{ runner.temp }}/flake-ledger.jsonl
|
||||
if-no-files-found: ignore
|
||||
retention-days: 90
|
||||
@@ -16,8 +16,7 @@
|
||||
"supabase/**": []
|
||||
},
|
||||
"selection": {
|
||||
"free-tests.yml/free-suite": "bun run scripts/test-free-shards.ts --list",
|
||||
"windows-free-tests.yml/windows-free-shard": "bun run scripts/test-free-shards.ts --windows-only --list"
|
||||
"free-tests.yml/free-suite": "bun run scripts/test-free-shards.ts --list"
|
||||
},
|
||||
"warn": [],
|
||||
"strays": { "allow": ["modules/loadouts.json", "modules/loadouts.schema.json"] }
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"pr_count": { "default_pr_count": 1, "split_requires_owner": true },
|
||||
"maintainers": ["garrytan"],
|
||||
"train": { "gate_workflow": "evals.yml", "free_workflow": "free-tests.yml", "full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record", "max_size": 5, "fill_hours": 1, "screen_selection": "gstack-paid", "trial_reservation_usd": 1.5 },
|
||||
"changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-free-tests", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] },
|
||||
"changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-smoke", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] },
|
||||
"min_gstack_version": "1.91.340.0"
|
||||
}
|
||||
@@ -282,9 +282,11 @@ Agents poll `eval:bg:*` logs for the `### gstack-detach EXIT=<code> ###` sentine
|
||||
## Platform support
|
||||
|
||||
- **macOS** + **Linux**: full test suite supported.
|
||||
- **Windows**: the curated Windows-safe subset runs in the `windows-free-tests`
|
||||
CI workflow across six `windows-latest` jobs, packed by Windows-measured
|
||||
durations. Setup script (`./setup`) requires Git Bash or
|
||||
- **Windows**: minimal support; [docs/support-tiers.md](docs/support-tiers.md)
|
||||
says what is promised. Every PR must pass the `windows-smoke` check (`./setup`,
|
||||
the core skills start, the fastest Windows-safe tests); the curated
|
||||
Windows-safe subset runs nightly on main in `windows-free-tests` and never
|
||||
blocks a PR. Setup script (`./setup`) requires Git Bash or
|
||||
MSYS today; native PowerShell support is a future expansion. The `bin/gstack-paths`
|
||||
helper resolves state roots through `CLAUDE_PLUGIN_DATA` / `GSTACK_HOME` so plugin
|
||||
installs work on every platform.
|
||||
|
||||
@@ -1543,7 +1543,7 @@ refresh to a maintainer.
|
||||
(`ubicloud-standard-4`); both are Ubicloud standard runners. When either recording adds
|
||||
or drops a test file, it force-updates the `bot/free-duration-seeds` branch,
|
||||
opens or refreshes one seed PR whose body states the ratchet result, and
|
||||
dispatches Free Tests, Windows Free Tests and Quality gate on it. Those
|
||||
dispatches Free Tests, Windows Smoke and Quality gate on it. Those
|
||||
dispatched runs are the seed PR's checks, and Free Tests supplies the required
|
||||
`free-tests` check. GitHub holds the pull_request runs of a token-opened PR as
|
||||
"action required" until someone approves them. A file that fails while recording turns its
|
||||
@@ -1557,6 +1557,13 @@ refresh to a maintainer.
|
||||
|
||||
### Windows free lane
|
||||
|
||||
Windows has minimal support ([support tiers](support-tiers.md)). The required
|
||||
PR check is `windows-smoke.yml`: one `windows-latest` job runs `./setup`, starts
|
||||
the core skills, drives `/browse` on gstack's own browser, and runs the short
|
||||
test list in its last step, where each file's reason sits beside it.
|
||||
`test/windows-native-workflows.test.ts` keeps that list Windows-safe and fast.
|
||||
|
||||
The curated suite below runs nightly on main and on dispatch, never on a PR.
|
||||
`windows-free-tests.yml` plans on Linux (`--windows-only --ci-plan --shards 6`),
|
||||
runs one strict serial shard per `windows-latest` job
|
||||
(`bun run test:windows --ci-run`), and verifies every shard receipt on Linux
|
||||
|
||||
+2
-2
@@ -92,7 +92,7 @@ hard-code:
|
||||
"changelog": "CHANGELOG.md",
|
||||
"history": "squash-ok",
|
||||
"stamp_sensitive_gates": ["tests"],
|
||||
"changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-free-tests", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] },
|
||||
"changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-smoke", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] },
|
||||
"min_gstack_version": "1.91.340.0"
|
||||
}
|
||||
```
|
||||
@@ -210,7 +210,7 @@ READY PR as conflicting each time main stamps; the train merges the CHANGELOG
|
||||
by section, so the lane never merges main for it. The conflict does mean GitHub
|
||||
has no merge ref for the PR, so no `pull_request` CI runs on the head and
|
||||
`ready` prints `ci: none at <head12>` with a `ci-hint:` line. The lane then
|
||||
runs `ready … --dispatch-ci free-tests.yml,windows-free-tests.yml [--timeout
|
||||
runs `ready … --dispatch-ci free-tests.yml,windows-smoke.yml [--timeout
|
||||
<s>]` (under a train policy): it dispatches each named workflow on the branch
|
||||
(`gh workflow run <wf> --ref <branch>`, skipped when a run at this head already
|
||||
concluded), waits for the runs (`CI_DISPATCH: <wf> --ref <branch> run <id> <url>
|
||||
|
||||
@@ -174,7 +174,7 @@ export async function readyMain(ctx: QueueContext, args: string[], base: { pr: s
|
||||
if (args.includes('--broad') && !screen) return usage('--broad selects the screen policy; it needs --screen');
|
||||
const dispatchCi = flag(args, '--dispatch-ci');
|
||||
const workflows = dispatchCi === undefined ? [] : dispatchCi.split(',').map(s => s.trim()).filter(Boolean);
|
||||
if (dispatchCi !== undefined && (!workflows.length || workflows.some(w => !/^[\w.-]+\.ya?ml$/.test(w)))) return usage('--dispatch-ci names workflow files under .github/workflows, comma-separated (free-tests.yml,windows-free-tests.yml)');
|
||||
if (dispatchCi !== undefined && (!workflows.length || workflows.some(w => !/^[\w.-]+\.ya?ml$/.test(w)))) return usage('--dispatch-ci names workflow files under .github/workflows, comma-separated (free-tests.yml,windows-smoke.yml)');
|
||||
const timeoutSec = positive(flag(args, '--timeout'), 2700);
|
||||
if (!Number.isFinite(timeoutSec)) return usage('--timeout needs a positive number of seconds');
|
||||
const fixer = flag(args, '--fixer');
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ function conflictHint(mergeable: unknown, ci: { green: boolean; none: boolean },
|
||||
if (mergeable !== 'CONFLICTING') return null;
|
||||
const ref = branch && /^[\w.][\w./-]*$/.test(branch) ? `--ref ${branch}` : '--ref <branch>';
|
||||
if (ci.none && fork) return `ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${head.slice(0, 12)}) and its head is on a fork, so the free workflows cannot be dispatched here; the coordinator decides (advisory)`;
|
||||
if (ci.none) return `ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${head.slice(0, 12)}); dispatch the free workflows on the branch (gh workflow run free-tests.yml ${ref} && gh workflow run windows-free-tests.yml ${ref}), clear the pregate lanes with their run URLs (gstack-pregate clear --remote <lane> --run-url <run>), then rerun ready (advisory)`;
|
||||
if (ci.none) return `ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${head.slice(0, 12)}); dispatch the free workflows on the branch (gh workflow run free-tests.yml ${ref} && gh workflow run windows-smoke.yml ${ref}), clear the pregate lanes with their run URLs (gstack-pregate clear --remote <lane> --run-url <run>), then rerun ready (advisory)`;
|
||||
if (ci.green) return `ci-hint: PR conflicts with the base (no merge ref); the runs at ${head.slice(0, 12)} were not pull_request runs (dispatched on the branch, or push-triggered), and they count (advisory)`;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -292,13 +292,13 @@ export function collectHealth(inputs: HealthInputs): HealthReport {
|
||||
detail: window.files.slice(0, 15).map(file => `${file.file}: ${file.flakyRuns}/${window.runs} runs (${pct(file.rate)})${file.failing ? ' FAIL > 5%' : ''}`) };
|
||||
}));
|
||||
|
||||
metrics.push(metric('free-wall', 'Free-suite and Windows wall', 'p50 of run_started_at to updated_at for successful main runs of free-tests.yml and successful pull_request runs of windows-free-tests.yml (its workflow_dispatch duration recordings excluded)', () => {
|
||||
metrics.push(metric('free-wall', 'Free-suite and Windows wall', 'p50 of run_started_at to updated_at for successful main runs of free-tests.yml and successful pull_request runs of windows-smoke.yml, the required Windows check (its workflow_dispatch runs excluded)', () => {
|
||||
const walls = (workflow: string, filter: { branch?: string; event?: string }) =>
|
||||
listWorkflowRuns(client, repo, workflow, { since, ...filter }).filter(run => run.conclusion === 'success').map(run => span(run.startedAt, run.updatedAt)!);
|
||||
const free = walls('free-tests.yml', { branch: 'main' });
|
||||
const windows = walls('windows-free-tests.yml', { event: 'pull_request' });
|
||||
const windows = walls('windows-smoke.yml', { event: 'pull_request' });
|
||||
const windowsValue = windows.length ? `${minutes(percentile(windows, 0.5))} (${windows.length} runs)` : 'no successful PR runs in window';
|
||||
return { value: `free-tests ${minutes(percentile(free, 0.5))} (${free.length} runs); windows-free-tests (successful PR runs) ${windowsValue}` };
|
||||
return { value: `free-tests ${minutes(percentile(free, 0.5))} (${free.length} runs); windows-smoke (successful PR runs) ${windowsValue}` };
|
||||
}));
|
||||
|
||||
metrics.push(metric('free-serial', 'Free-suite serial time', 'sum of scripts/free-test-durations.json (the committed seed)', () => {
|
||||
|
||||
@@ -294,6 +294,7 @@ export const FREE_ONLY_PR_FILES = [
|
||||
'.github/workflows/windows-free-tests.yml',
|
||||
'.github/workflows/windows-pregate-e2e.yml',
|
||||
'.github/workflows/windows-setup-e2e.yml',
|
||||
'.github/workflows/windows-smoke.yml',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -82,7 +82,7 @@ test('the propose step touches only the two seeds, never the allowlist, on one b
|
||||
expect(script).not.toContain('free-test-seed-allowlist.json');
|
||||
expect(script).toContain('branch=bot/free-duration-seeds');
|
||||
expect(script).toContain('bun test test/free-seed-ratchet.test.ts');
|
||||
for (const workflow of ['free-tests.yml', 'windows-free-tests.yml', 'quality-gate.yml']) {
|
||||
for (const workflow of ['free-tests.yml', 'windows-smoke.yml', 'quality-gate.yml']) {
|
||||
expect(script).toContain(`gh workflow run ${workflow} --repo "$GITHUB_REPOSITORY" --ref "$branch"`);
|
||||
expect(load(workflow).on).toHaveProperty('workflow_dispatch');
|
||||
}
|
||||
|
||||
@@ -561,10 +561,10 @@ describe('ready on a PR that conflicts with the base (CHANGELOG-only conflicts,
|
||||
const { r, at } = readyWith('CONFLICTING', { check_runs: [] });
|
||||
expect(r.status).toBe(EXIT.refused);
|
||||
expect(line(r.out, 'ci: ')).toBe(`ci: none at ${at} (passed=0 failed=0 pending=0 skipped=0)`);
|
||||
expect(line(r.out, 'ci-hint: ')).toBe(`ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${at}); dispatch the free workflows on the branch (gh workflow run free-tests.yml --ref pr-a && gh workflow run windows-free-tests.yml --ref pr-a), clear the pregate lanes with their run URLs (gstack-pregate clear --remote <lane> --run-url <run>), then rerun ready (advisory)`);
|
||||
expect(line(r.out, 'ci-hint: ')).toBe(`ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${at}); dispatch the free workflows on the branch (gh workflow run free-tests.yml --ref pr-a && gh workflow run windows-smoke.yml --ref pr-a), clear the pregate lanes with their run URLs (gstack-pregate clear --remote <lane> --run-url <run>), then rerun ready (advisory)`);
|
||||
for (const name of ['x;$(curl evil)', '--field']) {
|
||||
const odd = readyWith('CONFLICTING', { check_runs: [] }, { headRefName: name });
|
||||
expect(line(odd.r.out, 'ci-hint: ')).toContain('gh workflow run free-tests.yml --ref <branch> && gh workflow run windows-free-tests.yml --ref <branch>');
|
||||
expect(line(odd.r.out, 'ci-hint: ')).toContain('gh workflow run free-tests.yml --ref <branch> && gh workflow run windows-smoke.yml --ref <branch>');
|
||||
}
|
||||
const fork = readyWith('CONFLICTING', { check_runs: [] }, { isCrossRepository: true });
|
||||
expect(line(fork.r.out, 'ci-hint: ')).toBe(`ci-hint: PR conflicts with the base (no merge ref, so pull_request CI did not run on ${fork.at}) and its head is on a fork, so the free workflows cannot be dispatched here; the coordinator decides (advisory)`);
|
||||
|
||||
@@ -189,7 +189,7 @@ describe('collectHealth over recorded fixture data', () => {
|
||||
[/workflows\/free-tests\.yml\/runs/, () => ({ workflow_runs: [run(60), run(61, { conclusion: 'failure' })] })],
|
||||
[/runs\/60\/artifacts/, () => ({ artifacts: [{ id: 61, name: 'flake-ledger-2', size_in_bytes: 10 }] })],
|
||||
[/runs\/61\/artifacts/, () => ({ artifacts: [] })],
|
||||
[/workflows\/windows-free-tests\.yml\/runs/, () => ({ workflow_runs: windowsRuns })],
|
||||
[/workflows\/windows-smoke\.yml\/runs/, () => ({ workflow_runs: windowsRuns })],
|
||||
], {
|
||||
11: { 'manifest.json': JSON.stringify({ prCoverage: { mode: 'full-fallback', e2e: ['gate-a', 'gate-b'], judges: ['judge-a'],
|
||||
shadow: { e2e: ['gate-a', 'gate-b'], wouldDrop: [], wouldAdd: [], provenance: [], predictedSavingUsd: 0, unpricedCases: 0 } } }) },
|
||||
@@ -218,7 +218,7 @@ describe('collectHealth over recorded fixture data', () => {
|
||||
expect(value('census-redispatch').value).toBe('0 of 1 census(es) re-dispatched; longest paid slice job 20.0 min');
|
||||
expect(value('quarantine-evidence').value).toContain('insufficient evidence (not the same as healthy)');
|
||||
expect(value('free-flaky-pass-rate').value).toStartWith('50.0% (1/2 main runs');
|
||||
expect(value('free-wall').value).toBe('free-tests 0.0 min (1 runs); windows-free-tests (successful PR runs) 0.0 min (1 runs)');
|
||||
expect(value('free-wall').value).toBe('free-tests 0.0 min (1 runs); windows-smoke (successful PR runs) 0.0 min (1 runs)');
|
||||
expect(value('free-serial').value).toBe('3 s over 3 files');
|
||||
for (const entry of report.metrics.filter(entry => entry.id !== 'net-complexity')) {
|
||||
expect(entry.rule.length).toBeGreaterThan(10);
|
||||
@@ -287,10 +287,10 @@ describe('collectHealth over recorded fixture data', () => {
|
||||
const pr = (id: number, minutes: number, extra: Record<string, unknown> = {}) => run(id, { event: 'pull_request', head_branch: `pr-${id}`,
|
||||
run_started_at: day(1), updated_at: new Date(Date.parse(day(1)) + minutes * 60_000).toISOString(), ...extra });
|
||||
expect(wall([pr(70, 6), pr(71, 7), pr(74, 8), pr(72, 30, { conclusion: 'failure' }), run(73, { event: 'workflow_dispatch', run_started_at: day(3), updated_at: day(1) })]).value)
|
||||
.toBe('free-tests 0.0 min (1 runs); windows-free-tests (successful PR runs) 7.0 min (3 runs)');
|
||||
.toBe('free-tests 0.0 min (1 runs); windows-smoke (successful PR runs) 7.0 min (3 runs)');
|
||||
expect(wall([run(73, { event: 'workflow_dispatch', run_started_at: day(3), updated_at: day(1) })]).value)
|
||||
.toBe('free-tests 0.0 min (1 runs); windows-free-tests (successful PR runs) no successful PR runs in window');
|
||||
expect(wall([]).rule).toContain('workflow_dispatch duration recordings excluded');
|
||||
.toBe('free-tests 0.0 min (1 runs); windows-smoke (successful PR runs) no successful PR runs in window');
|
||||
expect(wall([]).rule).toContain('workflow_dispatch runs excluded');
|
||||
});
|
||||
|
||||
test('a rate-limited API degrades every remote metric to a named unavailable reason, never 0, and keeps the issue open', () => {
|
||||
|
||||
@@ -14,6 +14,8 @@ const root = path.resolve(import.meta.dir, '..');
|
||||
const load = (file: string) => Bun.YAML.parse(readFileSync(path.join(root, '.github/workflows', file), 'utf8')) as any;
|
||||
const windows = load('windows-free-tests.yml');
|
||||
const native = load('native-qualification.yml');
|
||||
const smoke = load('windows-smoke.yml');
|
||||
const smokeText = readFileSync(path.join(root, '.github/workflows/windows-smoke.yml'), 'utf8');
|
||||
|
||||
/**
|
||||
* Ratchet: the Windows-safe subset may not silently shrink. If you deleted or
|
||||
@@ -22,9 +24,9 @@ const native = load('native-qualification.yml');
|
||||
*/
|
||||
const WINDOWS_CURATED_FLOOR = 795;
|
||||
|
||||
test('the Windows lane plans, runs one strict shard per job and verifies every result', () => {
|
||||
expect(Object.keys(windows.on).sort()).toEqual(['pull_request', 'push', 'workflow_call', 'workflow_dispatch']);
|
||||
expect(windows.on.push).toEqual({ branches: ['main'] });
|
||||
test('the Windows lane runs nightly and by hand, plans, runs one strict shard per job and verifies every result', () => {
|
||||
expect(Object.keys(windows.on).sort()).toEqual(['schedule', 'workflow_call', 'workflow_dispatch']);
|
||||
expect(windows.on.schedule).toEqual([{ cron: '17 9 * * *' }]);
|
||||
expect(Object.keys(windows.on.workflow_dispatch.inputs)).toEqual(['record_durations']);
|
||||
expect(Object.keys(windows.on.workflow_call.inputs)).toEqual(['record_durations']);
|
||||
const plan = windows.jobs['windows-plan'];
|
||||
@@ -35,6 +37,7 @@ test('the Windows lane plans, runs one strict shard per job and verifies every r
|
||||
const shard = windows.jobs['windows-free-shard'];
|
||||
expect(shard['runs-on']).toBe('windows-latest');
|
||||
expect(shard.needs).toBe('windows-plan');
|
||||
expect(shard.if).toBe('${{ !inputs.record_durations }}');
|
||||
expect(shard.strategy).toEqual({ 'fail-fast': false, matrix: '${{ fromJSON(needs.windows-plan.outputs.matrix) }}' });
|
||||
const run = shard.steps.find((step: any) => step.name === 'Run Windows-safe shard');
|
||||
expect(run.run).toStartWith('bun run test:windows --ci-run "$RUNNER_TEMP/windows-plan.json" --shard ${{ matrix.shard }} --result ');
|
||||
@@ -77,6 +80,50 @@ test('a record_durations dispatch times each Windows-safe file alone and uploads
|
||||
expect(job.steps.at(-1).if).toBe("always() && steps.seed.outcome == 'success'");
|
||||
});
|
||||
|
||||
test('windows-smoke is the PR gate: every pull request and main push, one windows-latest job', () => {
|
||||
expect(Object.keys(smoke.on).sort()).toEqual(['pull_request', 'push', 'workflow_dispatch']);
|
||||
expect(smoke.on.pull_request).toEqual({ branches: ['main'] });
|
||||
expect(smoke.on.push).toEqual({ branches: ['main'] });
|
||||
expect(smoke.permissions).toEqual({ contents: 'read' });
|
||||
expect(Object.keys(smoke.jobs)).toEqual(['windows-smoke']);
|
||||
const job = smoke.jobs['windows-smoke'];
|
||||
expect(job['runs-on']).toBe('windows-latest');
|
||||
expect(job.if).toBeUndefined();
|
||||
const step = (name: string) => job.steps.find((s: any) => s.name === name);
|
||||
expect(step('./setup installs into an isolated HOME').run).toContain('./setup --host claude');
|
||||
const start = step('The core skills start').run;
|
||||
expect(start).toContain('for skill in ship review qa-only investigate browse; do');
|
||||
expect(start).toContain("grep -q '^SKILL_START_PROTO: 1'");
|
||||
expect(step("/browse on gstack's own browser runs a piped chain flow from an unrelated directory").run).toContain('| "$BROWSE" chain');
|
||||
const tests = step('Fastest Windows-safe free tests for install and the core skills');
|
||||
expect(tests.run).toContain('bun run scripts/test-free-shards.ts --files-from "$RUNNER_TEMP/windows-smoke-files.txt"');
|
||||
expect(tests.env.GSTACK_FREE_RETRY_FLAKY).toBe('1');
|
||||
});
|
||||
|
||||
test('every windows-smoke test is Windows-safe, seeded fast, and says why it is there', () => {
|
||||
const run: string = smoke.jobs['windows-smoke'].steps.find((s: any) => s.name === 'Fastest Windows-safe free tests for install and the core skills').run;
|
||||
const files = run.slice(run.indexOf("<<'LIST'\n") + 9, run.indexOf('\nLIST')).split('\n');
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
expect(new Set(files).size).toBe(files.length);
|
||||
const { safe } = curateWindowsSafe(collectFreeTestFiles(root), root);
|
||||
expect(files.filter(file => !safe.includes(file))).toEqual([]);
|
||||
const seed = JSON.parse(readFileSync(path.join(root, 'scripts/free-test-durations-windows.json'), 'utf8')).durations as Record<string, number>;
|
||||
expect(files.filter(file => !(seed[file] <= 5_000))).toEqual([]);
|
||||
expect(files.reduce((sum, file) => sum + seed[file]!, 0)).toBeLessThan(60_000);
|
||||
for (const file of files) expect(smokeText).toMatch(new RegExp('# ' + path.basename(file, '.test.ts').replace(/[.-]/g, '\\$&') + ' \\([0-9.]+\\): \\S'));
|
||||
});
|
||||
|
||||
test('no Windows job restores or saves the bun install cache', () => {
|
||||
// GSTA-203 PR 1: a cold install (16-17 s) beat restoring the ~450 MB
|
||||
// windows-bun entry (33-42 s), and PR merge-ref saves filled the 10 GB quota.
|
||||
for (const file of ['windows-free-tests.yml', 'windows-smoke.yml', 'windows-setup-e2e.yml']) {
|
||||
for (const [name, job] of Object.entries<any>(load(file).jobs)) {
|
||||
const caches = (job.steps ?? []).filter((step: any) => step.uses?.startsWith('actions/cache'));
|
||||
expect({ file, job: name, caches }).toEqual({ file, job: name, caches: [] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Windows shards use the pinned Node runtime and retain complete logs on every run', () => {
|
||||
const steps = windows.jobs['windows-free-shard'].steps;
|
||||
const node = steps.find((step: any) => step.uses?.startsWith('actions/setup-node@'));
|
||||
|
||||
Reference in new issue
Block a user