mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 10:40:32 +01:00
test(pregate): the real workflows owe windows-smoke.yml/windows-smoke and only its run clears it; the ready conflict hint names each lane's own workflow (GSTA-203 PR 6)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
fc768d045e
commit
2b3a7981cd
5 files changed
+45
-18
No files matched your search
@@ -1 +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.
|
||||
- **`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, and each refusal names its own fix. A run of the lane's own workflow clears as before, including a job link copied with `?pr=` or `#step`. `test_backend/*` lanes, `--bundle-lane` and non-GitHub origins are unchanged. On main the Windows obligation is `windows-smoke.yml/windows-smoke`, and a test now checks that against the repository's own workflows. The `ready` conflict hint now says to clear each lane with a run of that lane's own workflow. `test/pregate-clear-runs.test.ts` covers the mismatch, the matching control, the unreadable run, the non-run URL and the copied job links; each failed before its fix.
|
||||
+7
-2
@@ -778,8 +778,13 @@ aggregate, `os` Windows). Each accepts `pull_request` and `workflow_dispatch`
|
||||
runs and reads the `tested-tree` artifact, which both aggregates upload only
|
||||
after their strict verification passes. The main-push reuse job and
|
||||
`--from-run` read them. No lane has a profile, so every macOS and Windows lane
|
||||
still clears by `--run-url`, checked only for its workflow file (a
|
||||
`windows-smoke.yml` run cannot clear `windows-free-tests.yml/windows-free-shard`). A pull request cut before these
|
||||
still clears by `--run-url`, checked only for its workflow file. On main the
|
||||
Windows obligation is `windows-smoke.yml/windows-smoke` (the six-shard
|
||||
`windows-free-tests.yml/windows-free-shard` job is `manual`, so it is never an
|
||||
obligation), and a `windows-smoke.yml` run clears it. A branch cut before
|
||||
`windows-smoke.yml` landed still owes `windows-free-tests.yml/windows-free-shard`
|
||||
from its own workflows, and only a `windows-free-tests.yml` run clears that. A
|
||||
pull request cut before these
|
||||
workflows uploaded `tested-tree` has no record a lane profile could verify, so
|
||||
lane profiles wait until open pull requests carry the producer (GST-555).
|
||||
|
||||
|
||||
+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-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.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 each lane pregate names with a run of that lane's own workflow (gstack-pregate clear --remote <workflow.yml/job> --run-url <that workflow's run>; a run of another workflow is refused), 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;
|
||||
}
|
||||
|
||||
@@ -562,7 +562,7 @@ 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-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)`);
|
||||
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 each lane pregate names with a run of that lane's own workflow (gstack-pregate clear --remote <workflow.yml/job> --run-url <that workflow's run>; a run of another workflow is refused), 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-smoke.yml --ref <branch>');
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT, type PregateFile } from '../lib/headless-artifacts';
|
||||
import { buildContext } from '../lib/pregate/context';
|
||||
import { discoverLanes } from '../lib/pregate/workflows';
|
||||
import { clearDecision, clearObligation, runPregate, verifyPregate, type RunCheck } from '../lib/pregate/index';
|
||||
import type { ApiRun, RunDeps, RunProfile } from '../lib/run-evidence';
|
||||
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, readFile, runBin, type FixtureRepo } from './helpers/pregate-fixture';
|
||||
@@ -72,6 +73,16 @@ function check(repo: FixtureRepo, runs: FakeRun[], over: Partial<RunCheck> = {})
|
||||
return { repo: 'fx/fx', deps, profiles: PROFILES, gateRuns: GATE_RUNS, now: NOW, inventory: () => ['test/a.test.ts', 'test/b.test.ts'], ...over };
|
||||
}
|
||||
|
||||
/** A check whose only readable run is 300, a successful run of `workflow` on `head`. */
|
||||
function runOfWorkflow(repo: FixtureRepo, head: string, 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 } };
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('clear --run: Actions runs as remote gate evidence', () => {
|
||||
test('one verified run per profile on the identical tree clears the gate, and verify accepts the receipt offline', async () => {
|
||||
const repo = fixture();
|
||||
@@ -185,24 +196,35 @@ describe.skipIf(process.platform === 'win32')('clear --run: Actions runs as remo
|
||||
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<typeof clearDecision>) => ('refused' in d ? `${d.refused}: ${d.message}; fix: ${d.fix}` : `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; fix: clear ${PLATFORM} with a run of its own workflow: gh workflow run a.yml --ref <branch>, then gstack-pregate clear --remote ${PLATFORM} --run-url <that run's URL>`);
|
||||
expect(clearDecision(file, PLATFORM, { runUrl: url, evidence: ranWorkflow('a.yml') }, repo.work)).toEqual({ receipt: url, attested: false });
|
||||
for (const copied of [`${url}/job/9?pr=3806`, `${url}/job/9#step:3:1`]) expect(clearDecision(file, PLATFORM, { runUrl: copied, evidence: ranWorkflow('a.yml') }, repo.work)).toEqual({ receipt: copied, 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; fix: check gh auth status and that gh can read fx/fx's Actions runs, then rerun the same clear`);
|
||||
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; fix: pass the run's numeric id or https://github.com/fx/fx/actions/runs/<id>`);
|
||||
expect(msg(clearDecision(file, PLATFORM, { runUrl: url, evidence: runOfWorkflow(repo, head, '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; fix: clear ${PLATFORM} with a run of its own workflow: gh workflow run a.yml --ref <branch>, then gstack-pregate clear --remote ${PLATFORM} --run-url <that run's URL>`);
|
||||
expect(clearDecision(file, PLATFORM, { runUrl: url, evidence: runOfWorkflow(repo, head, 'a.yml') }, repo.work)).toEqual({ receipt: url, attested: false });
|
||||
for (const copied of [`${url}/job/9?pr=3806`, `${url}/job/9#step:3:1`]) expect(clearDecision(file, PLATFORM, { runUrl: copied, evidence: runOfWorkflow(repo, head, 'a.yml') }, repo.work)).toEqual({ receipt: copied, attested: false });
|
||||
expect(msg(clearDecision(file, PLATFORM, { runUrl: 'https://github.com/fx/fx/actions/runs/301', evidence: runOfWorkflow(repo, head, 'a.yml') }, repo.work))).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=api_error — reading run 301 failed: HTTP 404; ${PLATFORM} stays uncleared; fix: check gh auth status and that gh can read fx/fx's Actions runs, then rerun the same clear`);
|
||||
expect(msg(clearDecision(file, PLATFORM, { runUrl: 'https://ci.example.com/runs/300', evidence: runOfWorkflow(repo, head, '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; fix: pass the run's numeric id or https://github.com/fx/fx/actions/runs/<id>`);
|
||||
expect(clearDecision(file, PLATFORM, { runUrl: url }, repo.work)).toEqual({ receipt: url, attested: false });
|
||||
}, 120_000);
|
||||
|
||||
test('on this repository\'s own workflows the Windows obligation is windows-smoke.yml/windows-smoke: a windows-smoke.yml run clears it, a run of any other workflow is refused (GSTA-203)', async () => {
|
||||
const lanes = discoverLanes(path.resolve(import.meta.dir, '..'), 'linux');
|
||||
const owed = lanes.filter(l => l.tier === 'platform' && l.platform === 'windows' && l.runsTests && !l.manual);
|
||||
const smoke = owed.find(l => l.workflow === 'windows-smoke.yml');
|
||||
expect(smoke?.id).toBe('windows-smoke.yml/windows-smoke');
|
||||
expect(owed.filter(l => l.workflow === 'windows-free-tests.yml').map(l => l.id)).toEqual([]);
|
||||
expect(lanes.find(l => l.id === 'windows-free-tests.yml/windows-free-shard')).toMatchObject({ manual: true });
|
||||
const repo = fixture();
|
||||
const head = repo.git('rev-parse', 'HEAD');
|
||||
const base = await pregateAt(repo);
|
||||
const file: PregateFile = { ...base, requires_remote: [...base.requires_remote!, { lane: smoke!.id, platform: 'windows', workflow: smoke!.workflow, job: smoke!.job, tests: smoke!.testFiles, receipt: null }] };
|
||||
const url = 'https://github.com/fx/fx/actions/runs/300';
|
||||
expect(clearDecision(file, smoke!.id, { runUrl: url, evidence: runOfWorkflow(repo, head, 'windows-smoke.yml') }, repo.work)).toEqual({ receipt: url, attested: false });
|
||||
for (const other of ['windows-free-tests.yml', 'free-tests.yml']) {
|
||||
const d = clearDecision(file, smoke!.id, { runUrl: url, evidence: runOfWorkflow(repo, head, other) }, repo.work);
|
||||
expect('refused' in d && `${d.refused}: ${d.message}`).toBe(`PREGATE_RECEIPT_INVALID: REUSE: refused reason=workflow_mismatch — run 300 attempt 1 ran .github/workflows/${other}, not .github/workflows/windows-smoke.yml; windows-smoke.yml/windows-smoke stays uncleared`);
|
||||
}
|
||||
}, 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);
|
||||
|
||||
Reference in new issue
Block a user