mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 02:30:39 +01:00
fix(pregate): clear --run-url accepts a caller run for a workflow_call lane and only suggests dispatching workflows with workflow_dispatch (GST-971)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
8e223b6039
commit
ed27e992af
6 files changed
+59
-5
No files matched your search
@@ -0,0 +1 @@
|
||||
- **`gstack-pregate clear --run-url` accepts a reusable-workflow lane's caller run.** A lane whose workflow file runs only through `workflow_call` (gbrain's `native-locks.yml`, run as a job of `test.yml`) was refused `workflow_mismatch` for every run since v1.92.62.0, because GitHub reports the caller's file as the run's workflow. A run of a workflow in the branch's own `.github/workflows/` with a job that says `uses: ./.github/workflows/<lane workflow>` now clears the lane, a job link inside that run too, and the red and unfinished-run refusals still apply. Fix lines only suggest `gh workflow run` for a file with a `workflow_dispatch` trigger; otherwise they name the callers. An unrelated workflow still refuses. `test/pregate-clear-runs.test.ts` failed before the fix (GST-971).
|
||||
+6
-1
@@ -762,7 +762,12 @@ workflow file is the lane's own `<workflow.yml>`. A run of another workflow
|
||||
refuses with `REUSE: refused reason=workflow_mismatch — run <id> attempt <n>
|
||||
ran .github/workflows/<other>.yml, not .github/workflows/<lane's>.yml`; a
|
||||
run gh cannot read refuses with `api_error`, and a URL that is not an Actions
|
||||
run URL or id on this repository with `run_ref_invalid`. The run must also
|
||||
run URL or id on this repository with `run_ref_invalid`. A reusable-workflow
|
||||
lane (one whose file runs only through `workflow_call`) also clears from a run
|
||||
of a caller: a workflow in the branch's own `.github/workflows/` with a job
|
||||
that says `uses: ./.github/workflows/<lane's>.yml` (GST-971), and fix lines
|
||||
only suggest `gh workflow run` for a file with a `workflow_dispatch` trigger.
|
||||
The run must also
|
||||
have finished green (GSTA-203 PR 8): a run that is still queued or running
|
||||
refuses with `run_in_progress`, and one that concluded anything but `success`
|
||||
(`failure`, `cancelled`, `timed_out`, …) with `run_not_success`; each fix line
|
||||
|
||||
@@ -3770,7 +3770,9 @@ repository), `api_error` (gh could not read the run), `workflow_mismatch` (a
|
||||
run of another workflow), `run_in_progress` (still queued or running) or
|
||||
`run_not_success` (it failed, was cancelled or timed out). Each prints a fix
|
||||
line; after the last three, clear again with a finished green run of the
|
||||
lane's own workflow ([run evidence](pregate.md#run-evidence)).
|
||||
lane's own workflow ([run evidence](pregate.md#run-evidence)). For a lane whose
|
||||
workflow runs only through `workflow_call`, a run of a workflow whose job says
|
||||
`uses: ./.github/workflows/<lane's>.yml` counts as a run of the lane (GST-971).
|
||||
|
||||
Another tree is accepted in exactly one case: it differs from `pregate.json`'s
|
||||
tree only in the repo's stamp rule (`.gstack/ship-policy.json` on the base
|
||||
|
||||
+15
-3
@@ -29,6 +29,7 @@ import { baseBranchOf, baseMismatchFix, describeSubstitution, substituteStampOnl
|
||||
import { resolveBaseBranch } from '../ship-policy';
|
||||
import { parseRunReceipt, parseRunRef, runChanged, runReceiptFields, uncoveredFiles, verifyRun, type ApiRun, type RemoteGateRuns, type RunDeps, type RunEvidence, type RunProfile } from '../run-evidence';
|
||||
import type { CheckDef, CheckResult, PregateContext, Stage } from './types';
|
||||
import { workflowCallers } from './workflows';
|
||||
|
||||
export const CHECKS: readonly CheckDef[] = [regenCheck, secretsCheck, straysCheck, literalsCheck, timingCheck, guardsCheck, lanesCheck];
|
||||
export const CHECK_IDS = CHECKS.map(c => c.id);
|
||||
@@ -267,10 +268,21 @@ export function clearDecision(file: PregateFile, lane: string, input: ClearInput
|
||||
try { run = input.evidence.deps.run(id); }
|
||||
catch (e: any) { return invalid(`api_error — reading run ${id} failed: ${String(e?.message ?? e).slice(0, 300)}`, `check gh auth status and that gh can read ${repo}'s Actions runs, then rerun the same clear`); }
|
||||
const want = `.github/workflows/${target.workflow}`;
|
||||
if (!run || run.path !== want) return invalid(`workflow_mismatch — run ${id} attempt ${run?.run_attempt ?? '?'} ran ${run?.path ?? 'an unknown workflow'}, not ${want}`, `clear ${lane} with a run of its own workflow: gh workflow run ${target.workflow} --ref <branch>, then gstack-pregate clear --remote ${lane} --run-url <that run's URL>`);
|
||||
const { dispatch, callers } = workflowCallers(repoRoot, target.workflow);
|
||||
const runPath = run?.path;
|
||||
const caller = callers.find(c => runPath === `.github/workflows/${c.file}`);
|
||||
if (!run || (run.path !== want && !caller)) {
|
||||
const names = callers.map(c => c.file).join(', ');
|
||||
const dispatchable = dispatch ? [target.workflow] : callers.filter(c => c.dispatch).map(c => c.file);
|
||||
const start = dispatchable.length ? dispatchable.map(f => `gh workflow run ${f} --ref <branch>`).join(' or ')
|
||||
: callers.length ? `neither ${target.workflow} nor its callers have a workflow_dispatch trigger, so push a commit that starts ${callers.map(c => c.file).join(' or ')}`
|
||||
: `${target.workflow} has no workflow_dispatch trigger, so push a commit that starts it`;
|
||||
return invalid(`workflow_mismatch — run ${id} attempt ${run?.run_attempt ?? '?'} ran ${run?.path ?? 'an unknown workflow'}, not ${want}${callers.length ? ` or a workflow that calls it (${names})` : ''}`, `clear ${lane} with a run of its own workflow${callers.length ? ` or of a workflow that calls it (${names})` : ''}: ${start}, then gstack-pregate clear --remote ${lane} --run-url <that run's URL>`);
|
||||
}
|
||||
const ran = caller ?? { file: target.workflow, dispatch };
|
||||
const green = `then gstack-pregate clear --remote ${lane} --run-url <the green run's URL>`;
|
||||
if (run.status !== 'completed') return invalid(`run_in_progress — run ${id} attempt ${run.run_attempt} is ${run.status}`, `wait for run ${id} to finish green (or rerun ${target.workflow} if it fails), ${green}`);
|
||||
if (run.conclusion !== 'success') return invalid(`run_not_success — run ${id} attempt ${run.run_attempt} concluded ${run.conclusion}`, `rerun ${target.workflow} (gh run rerun ${id}, or gh workflow run ${target.workflow} --ref <branch>), ${green}`);
|
||||
if (run.status !== 'completed') return invalid(`run_in_progress — run ${id} attempt ${run.run_attempt} is ${run.status}`, `wait for run ${id} to finish green (or rerun ${ran.file} if it fails), ${green}`);
|
||||
if (run.conclusion !== 'success') return invalid(`run_not_success — run ${id} attempt ${run.run_attempt} concluded ${run.conclusion}`, `rerun ${ran.file} (gh run rerun ${id}${ran.dispatch ? `, or gh workflow run ${ran.file} --ref <branch>` : ''}), ${green}`);
|
||||
return { receipt: `https://github.com/${repo}/actions/runs/${id}/attempts/${run.run_attempt}`, attested: false };
|
||||
}
|
||||
const receipt = input.runUrl ?? (input.bundleLane ? `bundle-lane:${input.bundleLane}` : undefined);
|
||||
|
||||
@@ -183,6 +183,36 @@ export function parseWorkflow(file: string, text: string, platform: NodeJS.Platf
|
||||
});
|
||||
}
|
||||
|
||||
/** A workflow's `on:` trigger names, from `on: x`, `on: [x, y]` or its keys and list items. */
|
||||
function triggers(top: Block[]): string[] {
|
||||
const on = top.find(b => /^['"]?on['"]?$/.test(b.key));
|
||||
if (!on) return [];
|
||||
return [...on.value.replace(/\s+#.*$/, '').replace(/[[\]]/g, '').split(','), ...on.children.map(c => c.key)].map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `workflow` has a workflow_dispatch trigger, and the repo's workflow files with a job that calls it as a local
|
||||
* reusable workflow (`uses: ./.github/workflows/<workflow>`), each with its own workflow_dispatch trigger. A workflow_call
|
||||
* run reports its caller's file, so a caller's run is a run of the lane (GST-971). A missing or unreadable file has no trigger.
|
||||
*/
|
||||
export function workflowCallers(repoRoot: string, workflow: string): { dispatch: boolean; callers: Array<{ file: string; dispatch: boolean }> } {
|
||||
const dir = path.join(repoRoot, '.github', 'workflows');
|
||||
let names: string[] = [];
|
||||
try { names = fs.readdirSync(dir).filter(n => /\.ya?ml$/.test(n)).sort(); } catch { return { dispatch: false, callers: [] }; }
|
||||
const want = `./.github/workflows/${workflow}`;
|
||||
let dispatch = false;
|
||||
const callers: Array<{ file: string; dispatch: boolean }> = [];
|
||||
for (const n of names) {
|
||||
let top: Block[];
|
||||
try { top = outline(fs.readFileSync(path.join(dir, n), 'utf8')); } catch { continue; }
|
||||
const canDispatch = triggers(top).includes('workflow_dispatch');
|
||||
if (n === workflow) dispatch = canDispatch;
|
||||
const calls = (find(top, 'jobs')?.children ?? []).some(job => (find(job.children, 'uses')?.value ?? '').replace(/\s+#.*$/, '').trim().replace(/^['"]|['"]$/g, '') === want);
|
||||
if (calls && n !== workflow) callers.push({ file: n, dispatch: canDispatch });
|
||||
}
|
||||
return { dispatch, callers };
|
||||
}
|
||||
|
||||
/** Every job of every workflow, in file order. */
|
||||
export function discoverLanes(repoRoot: string, platform: NodeJS.Platform = process.platform): Lane[] {
|
||||
const dir = path.join(repoRoot, '.github', 'workflows');
|
||||
|
||||
@@ -194,6 +194,8 @@ describe.skipIf(process.platform === 'win32')('clear --run: Actions runs as remo
|
||||
const repo = fixture();
|
||||
const head = repo.git('rev-parse', 'HEAD');
|
||||
const base = await pregateAt(repo);
|
||||
fs.mkdirSync(path.join(repo.work, '.github/workflows'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, '.github/workflows/a.yml'), 'name: A\non: [pull_request, workflow_dispatch]\njobs:\n job:\n runs-on: windows-latest\n steps:\n - run: echo a\n');
|
||||
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 url = 'https://github.com/fx/fx/actions/runs/300';
|
||||
@@ -210,6 +212,8 @@ describe.skipIf(process.platform === 'win32')('clear --run: Actions runs as remo
|
||||
const repo = fixture();
|
||||
const head = repo.git('rev-parse', 'HEAD');
|
||||
const base = await pregateAt(repo);
|
||||
fs.mkdirSync(path.join(repo.work, '.github/workflows'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, '.github/workflows/a.yml'), 'name: A\non: [pull_request, workflow_dispatch]\njobs:\n job:\n runs-on: windows-latest\n steps:\n - run: echo a\n');
|
||||
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 url = 'https://github.com/fx/fx/actions/runs/300';
|
||||
|
||||
Reference in new issue
Block a user