mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 00:03:15 +01:00
fix: pre-landing review fixes (dispatch-ci skips a workflow the branch predates; harness decision; changelog fragment)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
68466832ac
commit
195bf0b3fc
5 files changed
+60
-9
No files matched your search
@@ -0,0 +1,13 @@
|
||||
**PRs no longer wait on the six-shard Windows suite. A short `windows-smoke` check is the required Windows check, and the full suite runs nightly on main.** Windows has minimal support (docs/support-tiers.md). Since 2026-11-01 06:00 UTC, Windows Free Tests finished last on 71 of 73 PR heads, with a wall time of 892 s at p50 and 1,886 s at p90 over 71 green PR runs, and each run used about 59 Windows runner-minutes. The new smoke check finished in 198 s on this PR (run 41325664750). With smoke in its place, the slowest required check on a PR drops from 892 s to 562 s at p50 and from 1,877 s to 1,193 s at p90.
|
||||
|
||||
### Changed
|
||||
- **New `windows-smoke.yml`** (check `windows-smoke`, on pull requests, pushes to main and by hand). It installs gstack with `./setup --host claude` into a separate home, starts /ship, /review, /qa-only, /investigate and /browse with `gstack-skill-start`, runs the installed browse chain against gstack's own browser, and runs 15 Windows-safe test files. Each file's reason is written next to it in the workflow. No cache: a cold `bun install` took 16 to 17 s, while restoring the cache took 33 to 42 s in a three-round A/B.
|
||||
- **`windows-free-tests.yml` runs nightly** (09:17 UTC) on main, and by hand. It no longer runs on pull requests or pushes and never blocks a merge. A red nightly gets a fix only when it shows a broken install, startup or core workflow. Other Windows-only failures close as won't fix with a link to docs/support-tiers.md.
|
||||
- **Bun cache removed** from `windows-free-tests.yml` and `windows-setup-e2e.yml`. The 18 cache entries on pull-request refs (8.05 GB) were deleted. Main's entries were left alone.
|
||||
- **`gstack-ship-queue ready --dispatch-ci`** skips a named workflow that main has but the PR branch predates, and prints a note instead of failing. GitHub reads the workflow file from the branch, so dispatching `windows-smoke.yml` on an older branch could only fail.
|
||||
|
||||
### Gate integrity
|
||||
- `.gstack/ship-policy.json` `required_checks`: `windows-free-tests` is replaced by `windows-smoke`. The ci-hint, `--dispatch-ci` docs, train CLI help and `free-durations-record.yml` now name `windows-smoke.yml`.
|
||||
- `.gstack/pregate.json`: the `windows-free-tests.yml/windows-free-shard` remote lane is removed, since that suite no longer runs on PRs. Linux and macOS lanes are unchanged.
|
||||
- `scripts/test-health-report.ts` measures Windows from `windows-smoke.yml` PR runs.
|
||||
- No test file was deleted, skipped or re-tiered. `scripts/harness-version.json` records `scripts/test-pr-profile.ts` as a non-behavioral change: it only lists `windows-smoke.yml` among files no paid case reads.
|
||||
+9
-7
@@ -213,13 +213,15 @@ has no merge ref for the PR, so no `pull_request` CI runs on the head and
|
||||
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>
|
||||
<conclusion>`), clears every requires-remote pregate lane whose workflow is one
|
||||
of them with the run URL (`PREGATE_CLEARED: <lane> <url>`; a lane of another
|
||||
workflow stays uncleared and is named in the refusal), and then reads the head
|
||||
as usual, which passes because the dispatched check runs attach to the commit;
|
||||
the `ci-hint:` line then says those non-pull_request runs count. A red run
|
||||
clears no lane and refuses (`QUEUE_NOT_READY`); a run still going at `--timeout`
|
||||
concluded, and skipped with a note when the base has the workflow file but the
|
||||
branch predates it, since GitHub reads the file from the branch), waits for the
|
||||
runs (`CI_DISPATCH: <wf> --ref <branch> run <id> <url> <conclusion>`), clears
|
||||
every requires-remote pregate lane whose workflow is one of them with the run
|
||||
URL (`PREGATE_CLEARED: <lane> <url>`; a lane of another workflow stays
|
||||
uncleared and is named in the refusal), and then reads the head as usual,
|
||||
which passes because the dispatched check runs attach to the commit; the
|
||||
`ci-hint:` line then says those non-pull_request runs count. A red run clears
|
||||
no lane and refuses (`QUEUE_NOT_READY`); a run still going at `--timeout`
|
||||
(default 2700 s) is `QUEUE_TRAIN_PENDING`, and rerunning the command keeps
|
||||
waiting; a fork head is never dispatched here (`QUEUE_TRAIN_UNTRUSTED`). By hand the same four steps are `gh workflow
|
||||
run <wf> --ref <branch>`, wait, `gstack-pregate clear --remote <lane> --run-url
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
* run at this head already concluded), a wait for each run, and one
|
||||
* `PREGATE_CLEARED:` per requires-remote lane whose workflow is one of them
|
||||
* and whose run concluded success. A red run clears no lane and refuses. A
|
||||
* fork head is never dispatched here (QUEUE_TRAIN_UNTRUSTED).
|
||||
* fork head is never dispatched here (QUEUE_TRAIN_UNTRUSTED). A workflow the
|
||||
* base has but the head predates (windows-smoke.yml on a branch cut before it
|
||||
* landed) is skipped with a note: GitHub reads the workflow file from the
|
||||
* branch, so the dispatch could only fail.
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -17,6 +20,7 @@ import type { PregateFile } from './headless-artifacts';
|
||||
import { PREGATE_OUT_REL, clearObligation, readPregateFile, writePregateFile } from './pregate/index';
|
||||
import type { QueueContext } from './ship-queue';
|
||||
import type { QueueFailure } from './ship-queue-gate';
|
||||
import { git } from './ship-queue-gate';
|
||||
import { prInfo, repoSlug, runView } from './ship-queue-train-github';
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -47,6 +51,11 @@ export async function dispatchCi(ctx: QueueContext, o: DispatchCiOptions): Promi
|
||||
const prior = atHead.find(r => r.status === 'completed');
|
||||
if (prior) { lines.push(`CI_DISPATCH: ${wf} not dispatched (run ${prior.databaseId} already concluded ${prior.conclusion ?? 'unknown'} at this head)`); concluded.set(wf, prior); continue; }
|
||||
const live = atHead[0];
|
||||
const wfPath = `.github/workflows/${wf}`;
|
||||
if (!live && git(ctx.repoRoot, ['cat-file', '-e', `${head}^{commit}`]).ok && git(ctx.repoRoot, ['cat-file', '-e', `${ctx.remote ?? 'origin'}/${ctx.base}:${wfPath}`]).ok && !git(ctx.repoRoot, ['cat-file', '-e', `${head}:${wfPath}`]).ok) {
|
||||
lines.push(`CI_DISPATCH: ${wf} not dispatched (${wfPath} is on ${ctx.base} but not in ${head.slice(0, 12)}; the branch predates it, so its pull_request run comes with the stamp commit)`);
|
||||
continue;
|
||||
}
|
||||
if (!live) {
|
||||
const d = ctx.gh(['workflow', 'run', wf, '--ref', branch]);
|
||||
if (!d.ok) return { lines, failure: { code: 'QUEUE_NOT_READY', message: `gh workflow run ${wf} --ref ${branch} failed (${(d.err ?? d.out).trim().replace(/\s+/g, ' ').slice(0, 120) || 'no output'})` } };
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"scripts/lib/published-text.ts": {"blob":"3f2e4d616b5dd0204035710c66b64d49055c5714","since":6},
|
||||
"scripts/lib/shard-engine.ts": {"blob":"dc714d7d897aed185c0ec3d2c92ad22d954039e5","since":6,"nonBehavioral":"createShardSandbox drops the Capy launch markers (GSTACK_SESSION_KIND, GSTACK_CAPY, GSTACK_EPHEMERAL, GSTACK_INSTALL_VERDICT) from shard environments; CI never sets them, so no case's input or verdict changes there, and on a Capy machine fixtures now see the session kind they set themselves instead of the installer's"},
|
||||
"scripts/test-paid-shards.ts": {"blob":"5439d0a8f6e1569fa7c924af20d67c30c81d5d30","since":6,"nonBehavioral":"credential rejections are classed infra instead of assertion: no case's pass or fail changes; only failed trials whose provider rejected the run's key move class, so no rerun spends on a rejected key"},
|
||||
"scripts/test-pr-profile.ts": {"blob":"911e393d0c36c050c3a68db5a81ef21701bfd5ae","since":6,"nonBehavioral":"selection only: fragment exemption narrowed to .md files directly in the fragments dir; changed-file lists include a rename's old path; no case prompt, fixture, judge or verdict logic changes (GST-400)"},
|
||||
"scripts/test-pr-profile.ts": {"blob":"25e2d88dd2e1caae5213fb396b977891bc67549a","since":6,"nonBehavioral":"scripts/test-pr-profile.ts only lists .github/workflows/windows-smoke.yml among files no paid case reads; no case's selection, prompt or verdict can change"},
|
||||
"scripts/test-strict-output.ts": {"blob":"01185d20dc9761b04b9b2f751ec98664fe0eaf93","since":6},
|
||||
"test/helpers/agent-sdk-runner.ts": {"blob":"210bba92d241a90df578b2c7fc943ac5755d0025","since":6},
|
||||
"test/helpers/auq-substance-panel.ts": {"blob":"4881260ef793668e4963a062db6d06a5e1f7a40f","since":6},
|
||||
|
||||
@@ -449,6 +449,33 @@ describe('ready --dispatch-ci', () => {
|
||||
expect([fork.status, fork.err]).toEqual([EXIT.refused, expect.stringMatching(/#1's head is on a fork.*not dispatched.*\(QUEUE_TRAIN_UNTRUSTED\)/)]);
|
||||
expect(queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--no-screen', '--dispatch-ci', '../evil.yml']).status).toBe(EXIT.usage);
|
||||
}, 120_000);
|
||||
|
||||
// Value: protects=a conflicting PR cut before main gained windows-smoke.yml still gets its free-tests dispatch when the hint names both workflows (GitHub reads the workflow file from the branch, so dispatching the missing one could only fail);
|
||||
// fails_when=the missing workflow is dispatched, a missing workflow refuses the whole command, or a workflow the branch has is skipped; why_new=windows-smoke.yml replaced windows-free-tests.yml in the hint while open branches predate it; seam=git objects in the fixture checkout; no_claim=GitHub's exact error for a workflow missing at the ref
|
||||
test('skips a workflow the base has but the PR head predates, with a note, and dispatches the rest', () => {
|
||||
const { fx, head, pregate } = readyFixture();
|
||||
const { repo } = fx;
|
||||
repo.git('checkout', '-q', 'origin/main');
|
||||
fs.mkdirSync(path.join(repo.work, '.github', 'workflows'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, '.github', 'workflows', 'windows-smoke.yml'), 'name: Windows Smoke\n');
|
||||
repo.git('add', '-A');
|
||||
repo.git('commit', '-q', '-m', 'add windows-smoke.yml');
|
||||
repo.git('push', '-q', 'origin', 'HEAD:main');
|
||||
repo.git('fetch', '-q', 'origin', 'main');
|
||||
repo.git('checkout', '-q', 'pr-1');
|
||||
fx.write(d => {
|
||||
(d.prs['1'] as any).mergeable = 'CONFLICTING';
|
||||
d.ci[head] = [];
|
||||
d.rules['free-tests.yml'] = { ci: 'free-tests', headSha: head };
|
||||
d.rules['windows-smoke.yml'] = { ci: 'windows-smoke', headSha: head };
|
||||
});
|
||||
const r = queue(fx, ['ready', '--pr', '1', '--pregate', pregate, '--no-screen', '--dispatch-ci', 'free-tests.yml,windows-smoke.yml', '--timeout', '5']);
|
||||
expect(r.out.split('\n').filter(l => l.startsWith('CI_DISPATCH:'))).toEqual([
|
||||
'CI_DISPATCH: free-tests.yml --ref pr-1 run 100 https://example.invalid/runs/100 success',
|
||||
`CI_DISPATCH: windows-smoke.yml not dispatched (.github/workflows/windows-smoke.yml is on main but not in ${head.slice(0, 12)}; the branch predates it, so its pull_request run comes with the stamp commit)`]);
|
||||
expect(dispatches(fx).map(l => l.workflow)).toEqual(['free-tests.yml']);
|
||||
expect(line(r.out, 'ci:')).toBe(`ci: green at ${head.slice(0, 12)} (passed=1 failed=0 pending=0 skipped=0)`);
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
describe('train build --dry-run --stack-tests', () => {
|
||||
|
||||
Reference in new issue
Block a user