mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
ci: публикация ревью-дока не имеет права трогать ничего, кроме документа
28.08 коммитbb2919fуехал в dev с тридцатью файлами вместо одного markdown: откатил отревьюженную реализацию #359, вернул старые чанки, оставил в dist/ двойной набор. dev держал откаченное дерево три часа. Сообщение коммита было невинным, и от рутины инцидент отличался только диффом. Механизм воспроизведён локально, а не предположен. `git checkout -- .` восстанавливает рабочее дерево ИЗ ИНДЕКСА, `git clean -fd` убирает неотслеживаемое — ни то, ни другое индекс не трогает. Ревьюер работает с Bash и, проверяя «умеет ли тест падать», вполне может сделать git add; всё оставшееся у него в индексе прежняя уборка сохраняла, и следующий git commit забирал это вместе с документом. Отсюда три рубежа, каждый закрывает свой отрезок пути. База: reset --hard на свежий origin/$target снимает и индекс, и дерево разом. Терять нечего — документ приезжает из RUNNER_TEMP, а не из рабочей копии. Индексируется ровно один путь, а не каталог. Индекс: перед коммитом дифф проверяется allowlist'ом docs/reviews/. Диапазон: перед КАЖДЫМ push проверяется origin/$target...HEAD — то есть то, что пуш добавит в ветку. Проверок две, потому что push делается из двух мест, и второй путь срабатывает ровно тогда, когда dev ушёл вперёд — в тех самых условиях, при которых случилсяbb2919f. Пустой дифф — тоже отказ: публиковать нечего означает, что документа нет, а прежняя редакция шага выходила тут с нулём и оставляла вердикт без артефакта (#171). Сравнение по префиксу каталога, а не подстрокой: docs/reviews-old и docs/reviewsx разрешёнными не считаются. Форс-пуш отсутствует и закреплён тестом. Четыре мутанта проверены руками, два добавлены в реестр. Пятый — «убрать одну из двух проверок диапазона» — сначала выжил: тест требовал наличия, а не количества. Тест усилен до подсчёта, мутант убит. Issue: #365 User-Visible: no
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import {
|
||||
REVIEW_DOC_ALLOWLIST, pathsOutsideAllowlist, reviewDocPushRefusal,
|
||||
} from '../scripts/review-doc-guard.mjs';
|
||||
|
||||
// #365. 28.08 шаг публикации ревью-дока запушил в dev коммит bb2919f с тридцатью
|
||||
// файлами вместо одного markdown: откатил отревьюженную реализацию #359, вернул
|
||||
// старые чанки, оставил в dist/ двойной набор. dev держал откаченное дерево три
|
||||
// часа. Сообщение коммита было невинным — «docs: review document for #359», — и
|
||||
// от рутины инцидент отличался только диффом. Релиз собирается из dev.
|
||||
|
||||
test('чистая публикация проходит (#365 AC1)', () => {
|
||||
assert.equal(reviewDocPushRefusal(['docs/reviews/CODE-REVIEW-359-r1.md']), null);
|
||||
assert.equal(reviewDocPushRefusal([
|
||||
'docs/reviews/SPEC-REVIEW-1-r1.md', 'docs/reviews/SPEC-REVIEW-1-r2.md',
|
||||
]), null);
|
||||
});
|
||||
|
||||
test('посторонний путь отменяет пуш и называет файлы (#365 AC2)', () => {
|
||||
const refusal = reviewDocPushRefusal([
|
||||
'docs/reviews/CODE-REVIEW-359-r1.md',
|
||||
'src/houseplan-card.ts',
|
||||
'dist/houseplan-card.js',
|
||||
]);
|
||||
assert.match(refusal, /задевает 2 путь\(ей\)/);
|
||||
assert.match(refusal, /dist\/houseplan-card\.js/);
|
||||
assert.match(refusal, /src\/houseplan-card\.ts/);
|
||||
// Причина названа, а не только факт: без неё следующий читатель решит, что
|
||||
// проверка придирается, и снимет её.
|
||||
assert.match(refusal, /bb2919f/);
|
||||
});
|
||||
|
||||
test('пустой дифф — тоже отказ, а не тихий успех (#365)', () => {
|
||||
// Публиковать нечего означает, что что-то пошло не так раньше. Прежняя
|
||||
// редакция шага в таком случае выходила с нулём, и вердикт ревью оставался
|
||||
// без артефакта (#171).
|
||||
assert.match(reviewDocPushRefusal([]), /публиковать нечего/);
|
||||
assert.match(reviewDocPushRefusal(['', ' ']), /публиковать нечего/);
|
||||
});
|
||||
|
||||
test('соседний каталог с похожим именем не считается разрешённым (#365)', () => {
|
||||
// Сравнение по префиксу каталога со слэшем: docs/reviews-old подстрокой не
|
||||
// притворяется.
|
||||
assert.deepEqual(
|
||||
pathsOutsideAllowlist(['docs/reviews-old/x.md', 'docs/reviews/y.md']),
|
||||
['docs/reviews-old/x.md'],
|
||||
);
|
||||
assert.deepEqual(pathsOutsideAllowlist(['docs/reviewsx.md']), ['docs/reviewsx.md']);
|
||||
});
|
||||
|
||||
test('allowlist задаётся снаружи и по умолчанию только docs/reviews (#365)', () => {
|
||||
assert.deepEqual(REVIEW_DOC_ALLOWLIST, ['docs/reviews/']);
|
||||
assert.equal(reviewDocPushRefusal(['docs/specs/1.md'], ['docs/specs']), null);
|
||||
assert.match(reviewDocPushRefusal(['docs/specs/1.md']), /docs\/specs\/1\.md/);
|
||||
});
|
||||
|
||||
test('шаг публикации в конвейере проверяет и индекс, и то, что уедет (#365 AC4)', () => {
|
||||
const workflow = readFileSync(
|
||||
new URL('../.github/workflows/process.yml', import.meta.url), 'utf8',
|
||||
);
|
||||
const step = workflow.slice(
|
||||
workflow.indexOf('- name: Опубликовать документ ревью'),
|
||||
workflow.indexOf('- name: Решение по вердикту'),
|
||||
);
|
||||
assert.ok(step.length > 500, 'шаг публикации не найден');
|
||||
// Два рубежа: что проиндексировано и что пуш добавит в ветку. Расходились они
|
||||
// именно тогда, когда база оказывалась не той.
|
||||
assert.equal(
|
||||
(step.match(/git diff --cached --name-only \| node scripts\/review-doc-guard\.mjs/g) || []).length,
|
||||
1, 'индекс проверяется один раз, перед коммитом',
|
||||
);
|
||||
// Дважды: push делается из двух мест — сразу и после ребейза при гонке. Одна
|
||||
// проверка на два пути означала бы, что второй путь не проверен вовсе, а
|
||||
// именно он срабатывает, когда dev ушёл вперёд — то есть в тех самых
|
||||
// условиях, при которых случился bb2919f.
|
||||
assert.equal(
|
||||
(step.match(/git diff --name-only "origin\/\$target\.\.\.HEAD" \| node scripts\/review-doc-guard\.mjs/g) || []).length,
|
||||
2, 'диапазон проверяется перед каждым push',
|
||||
);
|
||||
// Свежая база вместо той, что лежала здесь сорок минут назад.
|
||||
assert.match(step, /git reset -q --hard "origin\/\$target"/);
|
||||
// Форс-пуш запрещён: ветка двигается только вперёд.
|
||||
assert.equal(/--force/.test(step), false, 'в публикации ревью-дока не должно быть force-push');
|
||||
// Индексируется один путь, а не каталог.
|
||||
assert.match(step, /git add -- "\$doc"/);
|
||||
});
|
||||
Reference in New Issue
Block a user