ci: a cancelled Validate dispatch proves nothing to the review gate

Code review r1 (M1): validate-gate.mjs and merge-candidate's waitValidate
read a `cancelled` dispatch run on the material as red, so a dispatch
replaced by the next one in the `validate-dispatch-<ref>` concurrency
group would have returned the task S7→S6 for nothing — the same class
#511 fixed in release-gate.mjs. Cancelled runs are now ignored: the gate
follows the replacement dispatch, or starts its own when there is none.

Mutant: review-returns-task-on-cancelled-dispatch.

Issue: #510
User-Visible: no
This commit is contained in:
Codex
2026-09-09 18:33:24 +03:00
parent f6e64ba985
commit 00130e08aa
5 changed files with 42 additions and 3 deletions
+3 -1
View File
@@ -140,7 +140,9 @@ export function realOps({ repo, token, workflow = 'validate.yml', sleep = (ms) =
while (now() - started < VALIDATE_TOTAL_MS) {
const r = sh('gh', ['run', 'list', '--repo', repo, '--workflow', workflow, '--commit', sha, '--json', 'databaseId,status,conclusion,url,event', '--limit', '10']);
const all = r.status === 0 && r.stdout ? JSON.parse(r.stdout) : [];
const runs = all.filter((x) => !event || x.event === event);
// Отменённый прогон ничего не доказывает (#511): его заменил следующий
// dispatch на той же ветке — ждём его, а не красим кандидата.
const runs = all.filter((x) => (!event || x.event === event) && x.conclusion !== 'cancelled');
const run = runs.find((x) => x.databaseId === runId) || runs[0];
if (run) {
runId = run.databaseId;
+11
View File
@@ -8147,6 +8147,17 @@ const MUTANT_DEFINITIONS = [
replace: " // mutant: completed means green — a red dispatch falls through to the job check",
}],
},
{
id: 'review-returns-task-on-cancelled-dispatch',
guard: 'node --test test/validate-gate.test.mjs',
because: 'a dispatch cancelled by its replacement in the same concurrency group proves nothing; '
+ 'reading it as red sends the task back to S6 for no reason (#510 review r1 M1, #511)',
patches: [{
file: 'scripts/validate-gate.mjs',
find: " if (run.conclusion === 'cancelled') {",
replace: " if (false) { // mutant: cancelled counts as red",
}],
},
{
id: 'review-trusts-push-run-without-mutants',
guard: 'node --test test/validate-gate.test.mjs',
+9 -1
View File
@@ -48,7 +48,7 @@ export function provesMutants(jobs) {
*/
export async function validateGate({ ref, sha, ops, appearMs = VALIDATE_APPEAR_MS, totalMs = VALIDATE_TOTAL_MS, pollMs = POLL_MS }) {
const started = ops.now();
const ignored = new Set(); // завершённые зелёные dispatch без исполненных мутантов
const ignored = new Set(); // завершённые dispatch, которые ничего не доказывают: отменённые и зелёные без мутантов
let tracked = null;
let dispatchedAt = null;
while (ops.now() - started < totalMs) {
@@ -57,6 +57,14 @@ export async function validateGate({ ref, sha, ops, appearMs = VALIDATE_APPEAR_M
if (run) {
tracked = run.databaseId;
if (run.status === 'completed') {
if (run.conclusion === 'cancelled') {
// Отменённый прогон ничего не доказывает (#511, ревью r1 M1): его
// заменил другой dispatch в той же concurrency-группе — ждём его,
// а если замены нет, запускаем свой.
ignored.add(run.databaseId);
tracked = null;
continue;
}
if (run.conclusion !== 'success') return { result: 'red', url: run.url, note: `dispatch-прогон завершился: ${run.conclusion}` };
if (provesMutants(await ops.jobs(run.databaseId))) return { result: 'green', url: run.url, note: 'dispatch-прогон с исполненными мутантами зелёный' };
// зелёный, но мутанты не исполнялись (чужой dispatch без mutants=true) — не доказательство