fix: exempt pipeline review documents from the branch-name gate rule (#305)

Правило 2 локального process-gate блокировало пуш issue-ветки после того, как
конвейер «Привести ветку к dev» вносил в неё свежую историю dev с чужим
коммитом «docs: review document for #NNN» — локальный origin/dev автора
отставал и не вычитал его из диапазона, а нарушение в истории не чинится
следующим коммитом (единственный выход был --no-verify по §12/17).

Исключение доказуемое и fail-closed: точный subject документа ревью И дифф
только в docs/reviews/ (files обязателен — ownCommits теперь читаются с
filesOf). Любой код рядом с документом или пустой список файлов возвращают
правило 2 в строй; юниты фиксируют оба контура.

Issue: #305
User-Visible: no
This commit is contained in:
Codex
2026-08-26 00:24:22 +03:00
parent b1b8fb9fab
commit 30698d6ec4
2 changed files with 49 additions and 0 deletions
+17
View File
@@ -202,12 +202,26 @@ export function evaluateCommit(c) {
// уезжает назад, и в диапазон попадают коммиты `dev` с чужими номерами issue —
// каждый из них выглядел бы нарушением. Проверено на реальной истории: сидя на
// issue/89 с диапазоном по dev, гейт дал 26 ложных отказов из 26 коммитов.
// Документ ревью, доказанно созданный конвейером (#305): точный subject
// «docs: review document for #NN» и дифф ТОЛЬКО в docs/reviews/. Такой коммит
// попадает в чужую issue-ветку легитимно — шаг «Привести ветку к dev»
// вносит свежую историю dev, где конвейер только что опубликовал документ
// соседней задачи, а локальный origin/dev автора мог отстать и не вычесть его
// из диапазона. Кода в таком коммите нет по построению (files-критерий), и
// судить им правило 2 — ложный отказ, чинимый только --no-verify.
export function isPipelineReviewDocCommit(c) {
return /^docs: review document for #\d+$/.test(c.subject ?? '')
&& (c.files?.length ?? 0) > 0
&& c.files.every((f) => f.startsWith('docs/reviews/'));
}
export function checkBranchRule(branch, commits) {
const m = (branch ?? '').match(/^issue\/(\d+)-/);
if (!m) return [];
const want = `#${m[1]}`;
const out = [];
for (const c of commits) {
if (isPipelineReviewDocCommit(c)) continue;
for (const t of c.issues) {
if (t !== want) {
out.push({
@@ -541,8 +555,11 @@ function main(argv) {
const hasDev = spawnSync('git', ['-C', repo, 'rev-parse', '--verify', 'origin/dev'],
{ encoding: 'utf8' }).status === 0;
if (!hasDev) return checkedCommits;
// filesOf обязателен: исключение #305 доказывается диффом коммита
// (только docs/reviews/), а без файлов оно не срабатывает fail-closed.
const own = parseRecords(
git(['log', '--reverse', `--pretty=format:${LOG_FORMAT}`, 'origin/dev..HEAD'], repo),
filesOf,
);
return commitsNeedingTargetValidation(own, { targetRef, isCommitOnMain });
})()