ci: judge the push range from the last proven-green ancestor

#387 закрыл классификацию — какие job запускать. Здесь остаток того же
дефекта: гейты, которые судят сам диапазон коммитов. Провенанс, процессный
гейт и «новый код не добавляет any» брали диапазон от головы предыдущего
пуша, а concurrency отменяет прогон предыдущего пуша штатно. Тогда его
коммиты не судит никто: свой прогон отменён, а следующий пуш сравнивает уже
с ними. Окно не закрывается никогда.

Уязвим был прямой пуш в dev — основной режим конвейера. На ветках дефекта
нет: no-new-any там всегда считает от merge-base, а resolveValidationRange
подменяет осиротевший before на origin/dev (#315).

База стала последним предком с успешно завершённым Validate. Фолбэк, когда
такого нет, сознательно оставлен прежним — before, но с пометкой в summary
«диапазон недоказуем». Расширять диапазон здесь нельзя: гейт, который сам
красит прогон, лишил бы следующий пуш зелёного предка и запер dev в
красноте навсегда. Фолбэк обязан не зависеть от собственного успеха гейта.
Дыра сужается с «всегда, когда прогон предыдущего пуша отменён» до «когда
во всём окне обхода нет ни одного успешного прогона».

Находки no-new-any теперь называют коммит, добавивший строку: диапазон стал
шире, и без имени источника сообщение обвиняло бы того, кто пушнул
следующим, — ровно то, что чинили в #386 для golden.

Issue: #388
User-Visible: no
This commit is contained in:
Matysh
2026-08-30 15:55:08 +03:00
parent 6542d190ea
commit 65339f63ad
6 changed files with 315 additions and 31 deletions
+29 -1
View File
@@ -128,6 +128,34 @@ export function addedLinesByFile(diff) {
const isProductTypeScript = (path) => /^src\/.*\.ts$/.test(path);
/**
* Коммит, добавивший строку (issue #388).
*
* Диапазон гейта теперь считается от последнего ДОКАЗАННО зелёного предка, а не
* от головы предыдущего пуша, — и это значит, что находка может относиться к
* чужому коммиту, чей прогон был отменён. Без имени источника такое сообщение
* обвиняет того, кто пушнул следующим: ровно то, что пришлось чинить в #386 для
* golden.
*/
export function blameLine(path, line, runner = defaultBlame) {
const out = runner(path, line);
const match = typeof out === 'string' ? out.match(/^([0-9a-f]{7,40})\s/) : null;
return match ? match[1].slice(0, 8) : '';
}
const defaultBlame = (path, line) => {
const run = spawnSync('git', [
'-C', ROOT, 'blame', '-L', `${line},${line}`, '--porcelain', 'HEAD', '--', path,
], { encoding: 'utf8' });
return run.status === 0 ? run.stdout : '';
};
/** Строка отчёта о находке. Источник печатается, только если он известен. */
export function formatViolation(violation, source) {
const where = source ? ` (добавил ${source})` : '';
return ` ${violation.path}:${violation.line}${where} — ${violation.reason}`;
}
function main(argv) {
const value = (name, fallback) => {
const found = argv.find((item) => item.startsWith(`--${name}=`));
@@ -173,7 +201,7 @@ function main(argv) {
}
console.error(`\nНовый явный any: ${violations.length}\n`);
for (const violation of violations) {
console.error(` ${violation.path}:${violation.line} — ${violation.reason}`);
console.error(formatViolation(violation, blameLine(violation.path, violation.line)));
console.error(` ${violation.text}`);
}
console.error('\nЛибо типизируйте, либо обоснуйте на той же строке:');