From 51efe02933341784139b3e2d6a8b11c06794648f Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 2 Nov 2026 02:20:52 +0000 Subject: [PATCH 1/7] test(pregate): clear --run-url accepts a run of another workflow on a lane with no profile (red, GSTA-203 PR 6) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- test/pregate-clear-runs.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/pregate-clear-runs.test.ts b/test/pregate-clear-runs.test.ts index b91de7911..1d254672f 100644 --- a/test/pregate-clear-runs.test.ts +++ b/test/pregate-clear-runs.test.ts @@ -179,6 +179,29 @@ describe.skipIf(process.platform === 'win32')('clear --run: Actions runs as remo expect([viaRun.status, viaRun.err]).toEqual([EXIT.fail, expect.stringContaining('origin is not a github.com repository')]); }, 120_000); + test('a lane with no profile refuses a --run-url run of another workflow, or one that cannot be read, and still clears from a run of its own workflow (GSTA-203)', async () => { + const repo = fixture(); + const head = repo.git('rev-parse', 'HEAD'); + const base = await pregateAt(repo); + const PLATFORM = 'a.yml/job'; + const file: PregateFile = { ...base, requires_remote: [...base.requires_remote!, { lane: PLATFORM, platform: 'windows', workflow: 'a.yml', job: 'job', tests: ['test/a.test.ts'], receipt: null }] }; + const ranWorkflow = (workflow: string): RunCheck => { + const c = check(repo, []); + const run = (id: number): ApiRun => { + if (id !== 300) throw new Error('HTTP 404'); + return { id, run_attempt: 1, status: 'completed', conclusion: 'success', event: 'pull_request', head_sha: head, path: `.github/workflows/${workflow}`, repository: { full_name: 'fx/fx' }, head_repository: { full_name: 'fx/fx' } }; + }; + return { ...c, deps: { ...c.deps, run } }; + }; + const url = 'https://github.com/fx/fx/actions/runs/300'; + const msg = (d: ReturnType) => ('refused' in d ? `${d.refused}: ${d.message}` : `cleared ${d.receipt}`); + expect(msg(clearDecision(file, PLATFORM, { runUrl: url, evidence: ranWorkflow('b.yml') }, repo.work))).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=workflow_mismatch — run 300 attempt 1 ran .github/workflows/b.yml, not .github/workflows/a.yml; ${PLATFORM} stays uncleared`); + expect(clearDecision(file, PLATFORM, { runUrl: url, evidence: ranWorkflow('a.yml') }, repo.work)).toEqual({ receipt: url, attested: false }); + expect(msg(clearDecision(file, PLATFORM, { runUrl: 'https://github.com/fx/fx/actions/runs/301', evidence: ranWorkflow('a.yml') }, repo.work))).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=api_error — reading run 301 failed: HTTP 404; ${PLATFORM} stays uncleared`); + expect(msg(clearDecision(file, PLATFORM, { runUrl: 'https://ci.example.com/runs/300', evidence: ranWorkflow('a.yml') }, repo.work))).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=run_ref_invalid — "https://ci.example.com/runs/300" is not a run id or a run URL on fx/fx; ${PLATFORM} stays uncleared`); + expect(clearDecision(file, PLATFORM, { runUrl: url }, repo.work)).toEqual({ receipt: url, attested: false }); + }, 120_000); + test('a lane the base profiles never falls back to an unverified record when the GitHub origin cannot be read', async () => { const repo = fixture({ run_profiles: PROFILES, remote_gate_runs: GATE_RUNS }); const file = await pregateAt(repo); From 55be83a3c52194543d3ae1becf9d183af1cb0511 Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 2 Nov 2026 02:23:11 +0000 Subject: [PATCH 2/7] fix(pregate): clear --run-url refuses a run of another workflow on a lane with no profile (GSTA-203 PR 6) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- TODOS.md | 2 +- changelog.d/gsta203-pr6-clear-workflow-check.md | 1 + docs/pregate.md | 15 +++++++++++++-- lib/pregate/index.ts | 17 +++++++++++++++-- test/pregate-clear-runs.test.ts | 2 +- 5 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 changelog.d/gsta203-pr6-clear-workflow-check.md diff --git a/TODOS.md b/TODOS.md index eb40e432b..215b3b0aa 100644 --- a/TODOS.md +++ b/TODOS.md @@ -18,7 +18,7 @@ - **Bound the queue's recompute time.** `gstack-ship-queue` recomputes each receipt's needed depth at READY and at train build; add a test that a five-PR train recomputes within a fixed budget, so a large diff cannot stall the train. **Effort:** S. **Priority:** P1. ### P2/P3: Windows smoke follow-ups (filed 2026-11-01, from the GSTA-203 PR 1 /ship review) -- **Branches cut before windows-smoke.yml still owe the old Windows lane (P2)** — pregate reads lanes from the branch's own workflows, so such a branch keeps an uncleared `windows-free-tests.yml/windows-free-shard` obligation, and `ready --dispatch-ci` skips `windows-smoke.yml` there. The lane clears it by passing `windows-free-tests.yml` to `--dispatch-ci` (the old cost) or with a main-merge grant. Have the skip note name that route, or dispatch the old workflow in smoke's place. The skip also treats a branch that deletes or renames `windows-smoke.yml` as one that predates it; check the merge base too. **Effort:** S. **Priority:** P2. +- **Branches cut before windows-smoke.yml still owe the old Windows lane (P2)** — pregate reads lanes from the branch's own workflows, so such a branch keeps an uncleared `windows-free-tests.yml/windows-free-shard` obligation, and `ready --dispatch-ci` skips `windows-smoke.yml` there. The lane clears it by passing `windows-free-tests.yml` to `--dispatch-ci` (the old cost) or with a main-merge grant. Since GSTA-203 PR 6, `gstack-pregate clear --run-url` refuses a `windows-smoke.yml` run for that lane (`workflow_mismatch`), so the wrong-workflow clear is blocked. Have the skip note name that route, or dispatch the old workflow in smoke's place. The skip also treats a branch that deletes or renames `windows-smoke.yml` as one that predates it; check the merge base too. **Effort:** S. **Priority:** P2. - **Someone owns the nightly (P2)** — nothing reads the scheduled `windows-free-tests.yml` result or its flake ledgers. Add a test-health-report metric for scheduled runs on main, or have the daily flaky pass read the latest nightly. **Effort:** S. **Priority:** P2. - **Smoke /browse step waits for its page server (P3)** — `windows-smoke.yml` starts `Bun.serve` in the background and calls `browse chain goto` right away; poll the port first. **Effort:** XS. **Priority:** P3. - **Smoke seed ceiling vs the weekly reseed (P3)** — `test/windows-native-workflows.test.ts` holds each smoke file to 5 s in the Windows seed, which `free-durations-record.yml` rewrites weekly; one slow recording turns the seed PR red. Report the overrun there or re-pick files at reseed. **Effort:** S. **Priority:** P3. diff --git a/changelog.d/gsta203-pr6-clear-workflow-check.md b/changelog.d/gsta203-pr6-clear-workflow-check.md new file mode 100644 index 000000000..66af44cac --- /dev/null +++ b/changelog.d/gsta203-pr6-clear-workflow-check.md @@ -0,0 +1 @@ +- **`gstack-pregate clear --run-url` checks the run's workflow.** On a lane with no run profile, `clear` used to record any URL as the receipt, so a `windows-smoke.yml` run could clear `windows-free-tests.yml/windows-free-shard` on a branch cut before #3806. When the base config and the GitHub origin can be read, `clear` now reads the run and refuses `PREGATE_RECEIPT_INVALID` (`workflow_mismatch`) unless its workflow file is the lane's own; a run gh cannot read (`api_error`) or a URL that is not an Actions run on this repository (`run_ref_invalid`) refuses too. A run of the lane's own workflow clears as before, and `test_backend/*` lanes, `--bundle-lane` and non-GitHub origins are unchanged. `test/pregate-clear-runs.test.ts` covers the mismatch, the matching control, the unreadable run and the non-run URL; it failed before the fix. diff --git a/docs/pregate.md b/docs/pregate.md index def98ef6a..eb0e1af10 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -752,7 +752,17 @@ heads= verified=