ci: prove the range by judgement, not by a green verdict

Правка #388 уронила dev, и виновата подмена предиката. Я взял доказательством
`conclusion=success`, то есть оправдательный вердикт. Гейтам диапазона нужен
другой факт: судили ли этот коммит вообще. Упавший прогон коммит судил —
вердикт вынесен, автор его видел; переоткрывать такой коммит диапазоном не
надо. Не судил только отменённый.

Цена ошибки была наглядной. Backend на dev красный несколько дней по своей
причине (test_ha_import_export), успешных прогонов нет вовсе, поэтому база
уезжала на десятки коммитов назад. На 83d646c — docs-коммите — гейт
«новый код не добавляет any» предъявил 5 чужих находок из e4e1e370 и
8d431d6d и уронил frontend. Ровно тот сценарий обвинения невиновного, ради
которого делался #386, только устроенный мной.

Теперь `judgedShas` считает судимыми завершённые прогоны с любым вердиктом,
кроме cancelled, а `greenShas` остаётся для классификации (#387): там вопрос
другой — доказано ли, что тяжёлые гейты на этом дереве ПРОШЛИ. Два вопроса,
два предиката, и путать их дорого.

Запрос к API стал `status=completed` — надмножество, нужный предикат
применяет скрипт.

Issue: #388
User-Visible: no
This commit is contained in:
Matysh
2026-08-30 17:11:53 +03:00
parent 3ae3ec0f48
commit 66db451280
4 changed files with 74 additions and 9 deletions
+26 -4
View File
@@ -39,6 +39,29 @@ import { appendFileSync, readFileSync } from 'node:fs';
*/
export const MAX_CANDIDATES = 300;
/**
* SHA прогонов, которые ДОШЛИ ДО КОНЦА — успешно или нет, но не отменённые.
*
* Разница с `greenShas` — суть issue #388, и я её сначала перепутал, чем уронил
* dev. Классификация (#387) спрашивает «доказано ли, что тяжёлые гейты на этом
* дереве прошли» — там нужен именно `success`. Гейты диапазона спрашивают
* другое: «судил ли этот коммит хоть кто-нибудь». Упавший прогон коммит СУДИЛ,
* просто вынес обвинительный вердикт, и переоткрывать его диапазоном не надо.
*
* Цена ошибки была наглядной: backend на dev красный несколько дней по своей
* причине, поэтому «зелёных» прогонов не было вовсе, база уезжала на десятки
* коммитов назад, и `no-new-any` начал предъявлять текущему пушу чужой долг.
*/
export function judgedShas(payload) {
const runs = payload && Array.isArray(payload.workflow_runs) ? payload.workflow_runs : [];
return new Set(
runs
.filter((run) => run && run.status === 'completed' && run.conclusion !== 'cancelled'
&& typeof run.head_sha === 'string')
.map((run) => run.head_sha),
);
}
/**
* SHA прогонов, завершившихся успешно. Вход — тело ответа
* `/actions/workflows/validate.yml/runs`; всё, что не массив прогонов,
@@ -136,7 +159,7 @@ export function baseSummary(choice, { head, mergeBase }) {
if (choice.reason === 'fallback') {
return [
'### База диапазона (#388)',
`Ни у одного из ${choice.skipped} предков нет завершённого зелёного Validate.`
`Ни один из ${choice.skipped} предков не был судим завершённым Validate.`
+ ` Диапазон взят от \`${short(choice.base)}\` — головы предыдущего пуша,`
+ ' и это НЕ доказательство проверенности: прогон того пуша мог быть отменён.'
+ ' Коммиты в этом окне могли не пройти ни одного гейта.',
@@ -184,10 +207,9 @@ function main(argv) {
'rev-list', `--max-count=${MAX_CANDIDATES}`, '--skip=1', span,
], { encoding: 'utf8' }).split('\n').map((line) => line.trim()).filter(Boolean);
const green = greenShas(payload);
const choice = mode === 'range'
? pickRangeBase({ candidates, green, fallback: arg(argv, 'fallback') })
: pickBase({ candidates, green, mergeBase });
? pickRangeBase({ candidates, green: judgedShas(payload), fallback: arg(argv, 'fallback') })
: pickBase({ candidates, green: greenShas(payload), mergeBase });
const summary = baseSummary(choice, { head, mergeBase });
process.stdout.write(`${summary.join('\n')}\n`);
// Имя выхода задаётся явно: одна и та же job считает базу для двух разных