mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 22:29:05 +00:00
ci: anchor the review material to content, not to history
#413 закрыл класс «SHA мёртв уже в момент публикации». Остаётся более частый: SHA был жив, а умер потом — по корпусу таких объявлений 98 из 804, потому что ветку задачи после ревью перебазируют, сквошат или удаляют. SHA коммита — свойство истории, а история переписывается. Содержимое не переписывается: git адресует деревья и блобы их хешем. На #403 спец-коммит переехал из 83005c3c в94502d3d, а блоб ТЗ у обоих один — 56a92e12; по нему материал находится одной командой независимо от ребейза. Конвейер снимает якоря там, где читает материал — в шаге перехода на ветку задачи, пока рабочая копия равна тому, что прочтёт ревьюер. В шаге публикации спрашивать поздно: дерево уже сброшено на целевую ветку. При публикации якоря дописываются машинным блоком: дерево материала и блоб каждого ТЗ, каждый со своей исполнимой командой поиска. Блок машинный и помечен как машинный. Ревьюер его не заполняет: дисциплина ручного переписывания SHA здесь уже подвела, и заменять её другой ручной дисциплиной смысла нет. Гейт #413 смягчён ровно там, где обязан: осиротевший SHA при живых якорях — предупреждение, а не отказ. Ронять раунд, который воспроизводим, было бы той же ошибкой в другую сторону. Отказ остаётся, когда не работает ни один объявленный способ найти материал. Проверено на настоящем осиротевшем случае: блок, собранный для94502d3d, находит и дерево, и блоб ТЗ; тот же документ с якорями даёт предупреждение вместо отказа, без якорей — отказ. Issue: #416 User-Visible: no
This commit is contained in:
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import {
|
||||
REVIEW_DOC_ALLOWLIST, citedMaterialShas, danglingMaterialRefusal, pathsOutsideAllowlist, reviewDocPushRefusal,
|
||||
ANCHOR_MARKER, REVIEW_DOC_ALLOWLIST, REVIEW_HEADER_LINES, citedMaterialShas, danglingMaterialRefusal, materialAnchorBlock, materialAnchorsFrom, parseSpecList, pathsOutsideAllowlist, reviewDocPushRefusal, withMaterialAnchors,
|
||||
} from '../scripts/review-doc-guard.mjs';
|
||||
|
||||
// #365. 28.08 шаг публикации ревью-дока запушил в dev коммит bb2919f с тридцатью
|
||||
@@ -137,3 +137,68 @@ test('шапка без объявления материала не судит
|
||||
assert.equal(danglingMaterialRefusal('# CODE-REVIEW-1-r1\n\nтекст\n', () => new Map()), null);
|
||||
assert.deepEqual(citedMaterialShas('# CODE-REVIEW-1-r1\n\nтекст\n'), []);
|
||||
});
|
||||
|
||||
// --- якоря, переживающие ребейз (#414) -------------------------------------
|
||||
|
||||
test('блок якорей содержит исполнимые команды, а не описание (#414)', () => {
|
||||
const block = materialAnchorBlock({
|
||||
sha: '94502d3d67cacf85bdb9f69cd511b342989891fd',
|
||||
tree: '3fc651fcb868eefa28755d01ec2b9377598dcb27',
|
||||
branch: 'issue/403-area-relocation-safety',
|
||||
specs: [{
|
||||
blob: '56a92e12dedc8fa541537ae5908dc6f1dfab43e8',
|
||||
path: 'docs/specs/403-area-relocation-safety.md',
|
||||
}],
|
||||
});
|
||||
// Отчёт обязан быть исполняемым: на #403 канонная команда не работала, и
|
||||
// следующий раунд восстанавливал коммит по содержимому диффа руками.
|
||||
assert.match(block, /git log --all --find-object=56a92e12dedc8fa541537ae5908dc6f1dfab43e8/);
|
||||
assert.match(block, /git log --all --format='%H %T' \| grep 3fc651fcb868/);
|
||||
assert.match(block, /ребейз его осиротит/, 'блок обязан объяснять, зачем он нужен');
|
||||
assert.match(block, /material-anchors: сгенерировано конвейером/);
|
||||
});
|
||||
|
||||
test('без ветки задачи блок честно говорит, что якорей нет (#414)', () => {
|
||||
const block = materialAnchorBlock({ branch: '', sha: '', tree: '', specs: [] });
|
||||
assert.match(block, /Якоря снять не удалось/);
|
||||
});
|
||||
|
||||
test('повторная приписка заменяет блок, а не копит его (#414)', () => {
|
||||
const anchors = { sha: 'a'.repeat(40), tree: 'b'.repeat(40), branch: 'dev', specs: [] };
|
||||
const once = withMaterialAnchors('# отчёт\n\nтекст\n', anchors);
|
||||
const twice = withMaterialAnchors(once, anchors);
|
||||
assert.equal(twice.split(ANCHOR_MARKER).length - 1, 1, 'маркер обязан быть один');
|
||||
assert.match(twice, /# отчёт/);
|
||||
});
|
||||
|
||||
test('список ТЗ разбирается и отсекает мусор (#414)', () => {
|
||||
const parsed = parseSpecList(
|
||||
`${'a'.repeat(40)} docs/specs/403-x.md;короткий docs/specs/y.md;${'b'.repeat(40)} ;`,
|
||||
);
|
||||
assert.deepEqual(parsed, [{ blob: 'a'.repeat(40), path: 'docs/specs/403-x.md' }]);
|
||||
});
|
||||
|
||||
test('осиротевший SHA при живых якорях — предупреждение, не отказ (#414)', () => {
|
||||
const doc = withMaterialAnchors(
|
||||
'- Материал: спец-файл на `HEAD = 83005c3c`\n',
|
||||
{ sha: 'c'.repeat(40), tree: 'd'.repeat(40), branch: 'issue/403-x', specs: [] },
|
||||
);
|
||||
const verdict = danglingMaterialRefusal(
|
||||
doc, () => new Map([['83005c3c', null]]), REVIEW_HEADER_LINES, () => true,
|
||||
);
|
||||
assert.ok(verdict.warning, 'раунд воспроизводим — ронять его нечего');
|
||||
assert.match(verdict.warning, /83005c3c/);
|
||||
assert.match(verdict.warning, /по якорям/);
|
||||
});
|
||||
|
||||
test('осиротевший SHA и мёртвые якоря — по-прежнему отказ (#414)', () => {
|
||||
const doc = withMaterialAnchors(
|
||||
'- Материал: спец-файл на `HEAD = 83005c3c`\n',
|
||||
{ sha: 'c'.repeat(40), tree: 'd'.repeat(40), branch: 'issue/403-x', specs: [] },
|
||||
);
|
||||
const verdict = danglingMaterialRefusal(
|
||||
doc, () => new Map([['83005c3c', null]]), REVIEW_HEADER_LINES, () => false,
|
||||
);
|
||||
assert.equal(typeof verdict, 'string');
|
||||
assert.match(verdict, /не достижим ни из одной ссылки origin/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user