ci: classify against the last proven-green ancestor

Диапазон классификации брался от `github.event.before` — головы предыдущего
пуша. Это допущение «до этого уже проверено», и оно неверно ровно тогда,
когда прогон предыдущего пуша не завершился. А не завершается он штатно:
concurrency отменяет его следующим пушем.

На #86 (r5) это дало ложный зелёный: push 04da7eb1 тронул dist/** и
frontend/**, его прогон отменили через три минуты; следующий push fa146fb1
тронул только docs/images/**, классификация сравнила эти два коммита и
выставила frontend=false. Job «Фронтенд», а за ней golden, smoke и backend
оказались skipped — прогон при этом success. Маркеры переиспользования эти
гейты тоже не подтверждали: `Cache not found` по всем четырём.

Теперь база — самый новый предок HEAD, для которого Validate ДЕЙСТВИТЕЛЬНО
завершился успешно; если такого нет, диапазон расширяется до merge-base с
dev, то есть до всего вклада ветки. Работает по индукции: цепочка узких
диффов покрывает всё изменённое с последней настоящей проверки, а одно
незавершённое звено теперь расширяет диапазон, а не сужает.

Недоступность API не роняет job: пустой ответ опускает базу до merge-base,
то есть в сторону большего объёма проверок.

Защита от force-push (#347) сохранена: механизм, из-за которого merge-base
врал на переписанной истории, до конца не разобран, и снимать защиту, не
объяснив её, — способ получить #347 второй раз.

Issue: #387
User-Visible: no
This commit is contained in:
Matysh
2026-08-30 15:35:56 +03:00
parent 582d673a2d
commit 50851dc3f9
4 changed files with 337 additions and 18 deletions
+26
View File
@@ -141,3 +141,29 @@ test('упавшая golden называет первопричину, а не
// свидетеля и смысл сообщения перевернётся.
assert.match(golden, /if: failure\(\) && steps\.fail_note\.outputs\.first == 'true'/);
});
test('классификация опирается на завершённый прогон, а не на предыдущий пуш (#387)', () => {
const workflow = read('validate.yml');
const changes = workflow.slice(
workflow.indexOf('\n changes:\n'), workflow.indexOf('\n reuse:\n'),
);
// `github.event.before` — источник ложного «уже проверено»: прогон
// предыдущего пуша штатно отменяется следующим. Классификация читать его
// больше не имеет права. В процессном гейте он законен: там проверяются
// трейлеры именно отправленных коммитов, а не объём проверок.
// `before` остаётся ровно одной проверкой — жив он или переписан (#347).
// Базой диапазона он больше не служит: его прогон штатно отменяется.
assert.equal(/base="\$BEFORE_SHA"/.test(changes), false,
'диапазон не имеет права опираться на голову предыдущего пуша (#387)');
assert.match(changes, /git cat-file -e "\$BEFORE_SHA"/,
'защита от force-push остаётся на месте (#347)');
assert.match(changes, /node scripts\/classify-base\.mjs --head=/);
assert.match(changes, /actions: read/, 'чтение прогонов требует прав');
// База считается только для push вне dev: у PR диапазон задан событием.
assert.match(changes, /if: github\.event_name != 'pull_request' && github\.ref != 'refs\/heads\/dev'/);
// Пустая база означает «доказательства нет» и обязана вести к полному
// прогону, а не к пустому диффу, который выглядел бы как «ничего не менялось».
const empty = changes.slice(changes.indexOf('if [ -z "$base" ]'));
assert.match(empty, /frontend=true\\nbackend=true\\nintegration=true/,
'без базы классификация обязана раскрываться в полный прогон');
});