18 KiB
CODE-REVIEW-702-r1
Issue: #702 · Трек: show · Заход: r1 · Блокирующих циклов израсходовано: 0 из 2
Материал ревью: 3a681e24e49038f202fc74cc3f5482e93e43977e (ветка issue/702-delete-merged-branches, один коммит поверх dev@c716bb0f)
Скоуп
Задача — гигиена веток конвейера (issue #702): 368 влитых issue/* веток
висели на origin, список веток переставал что-то значить, агент, искавший
ветку по номеру, мог взять устаревшую. Предложение из тела issue:
- После успешного слияния
merge-candidate.mjsудаляет ветку задачи; удаление — только если вершина ветки равна влитому кандидату. - Разовая чистка уже влитых веток — список владельцу, удаление только после явного согласия.
- Не трогать
codex/*, релизные и невлитые ветки.
Диапазон правки (git diff origin/dev...HEAD):
| Файл | Класс | Что |
|---|---|---|
scripts/merge-candidate.mjs |
B (tooling) | deleteBranch() в realOps; вызов из finish() с branchTip, проброшенным из обеих веток слияния (fast-forward и push после Validate); текст комментария в commentFor() |
PROCESS.md §10.4 |
C (docs) | пункт в списке шагов слияния |
test/merge-candidate.test.mjs |
B | 4 новых теста (fakeOps получил deleteBranch/deleteOk) |
scripts/mutation-registry.mjs |
B | мутант merged-task-branch-kept |
Файлов класса A нет — продуктовый код карточки не тронут, отсюда
User-Visible: no в трейлере коммита (корректно). Пункт 2 (разовая чистка)
в диффе не реализован кодом — по комментарию автора список подготовлен
(branches-to-delete-702.txt), удаление не выполнялось, ждёт согласия
владельца. Это соответствует AC: чистка — ручное действие владельца, не
автоматизация этой задачи.
Ветка к dev не приводилась (трек show, #696): сливается с открытыми
задачами #697–#700 без конфликта (git merge-tree, со слов автора; не
перепроверялось — не относится к AC этой задачи).
Как проверялось
Ревью читало код и намеренно воспроизвело граничный случай, который юнит-тест
подменяет фейком: сам механизм git push --force-with-lease на удаление ветки.
- Прочитан
scripts/merge-candidate.mjsцеликом: путьfinish(), оба места, гдеbranchTipпопадает вextra(строка ~287 — fast-forward, ~332 — после зелёного Validate), и путьcommentFor(). - Прочитан весь
test/merge-candidate.test.mjs:fakeOps, все четыре новых теста и их согласованность сmergeCandidate(). - Выполнен независимый эксперимент на временном bare-репозитории (два
прогона):
git push --force-with-lease=refs/heads/X:<ожидание> origin :refs/heads/X— (а) при совпадении ожидания с реальной вершиной ветка удаляется (exit 0); (б) при устаревшем ожидании push отклоняется с! [rejected] (delete) -> feature (stale info), ветка остаётся. Это подтверждает, что регэксп/stale info|rejected|fetch first|lease/iвdeleteBranch()ловит именно ту ошибку, которую реально отдаёт git, а не придуманную автором строку. - Прогнаны узкие автотесты и гейты, относящиеся к диффу (список — в таблице «Гейты» ниже).
- Проверены трейлеры коммита, сверка чисел (нет числа, видимого дважды — единственное число диффа, SHA/статусы, генерируется кодом, не дублируется руками).
- Проверено, что job
integrateв_process.yml, вызывающаяmerge-candidate.mjs, использует не GITHUB_TOKEN (у которого в этом job толькоcontents: read), а секретHP_PROCESS_TOKENчерез явный URL с токеном — то есть право на push/delete не зависит отpermissions:блока job и не сужено этой задачей. - Проверено, что ни один другой скрипт (
task-packet.mjs,process-gate.mjs,process-metrics.mjs,wait-verdict.mjs) не предполагает существование ветки задачи послеS8-merged— удаление ничего не ломает по цепочке.
AC · чем доказан · чем краснеет
| AC | Чем доказан | Чем краснеет |
|---|---|---|
| Ветка удаляется после успешного слияния (fast-forward и push) | test/merge-candidate.test.mjs (2 теста), исполнено (26/26) |
Мутант merged-task-branch-kept (if (false && merged && …)) — гард node --test --test-name-pattern="#702" test/merge-candidate.test.mjs; прогнан лично: node scripts/mutation-gate.mjs --id=merged-task-branch-kept → «merged-task-branch-kept: заявленный тест покраснел на мутанте», «поймано 1 из 1» |
| Удаление — только если вершина ветки на origin равна вершине, которую видело слияние (lease) | Юнит-тест «сдвинутая вершина — ветка остаётся» (deleteOk: false); плюс независимый ручной эксперимент на настоящем git (см. «Как проверялось») — подтверждает, что --force-with-lease на удаление действительно защищает от гонки, а не только в фейке |
Ручной эксперимент — стерев --force-with-lease до простого git push origin :refs/heads/X, оба ручных прогона поменяли бы поведение (случай (б) удалил бы ветку вместо отказа); отдельного мутанта на этот случай нет, но реальный git-эксперимент закрывает то же самое доказательство подлинным исполнением, а не фейком |
| Красный/незавершённый merge не удаляет ветку (red Validate, conflict, reject-stale, rereview, give-up) | Юнит-тест: красный Validate → calls.filter(delete) == []; устаревшая ветка (#312) → calls.filter(delete) == [] |
Проверено чтением: во всех этих исходах finish() вызывается без branchTip в extra, а merged вычисляется из decision.action, который в этих ветках никогда не push/fast-forward — структурно недостижимо, что делает отдельный мутант избыточным для этой части |
| Разовая чистка 368 влитых веток — не автоматически, только после согласия владельца | Чтение диффа: код чистки в этом коммите отсутствует; issue-комментарий автора подтверждает, что ничего не удалено | Не мутант — это отсутствие функциональности, доказывается отсутствием кода, а не тестом |
Находки
Нет High. Нет Medium в скоупе. Нет Medium вне скоупа.
Low (снята с записью, не блокирует): если deleteBranch() бросает
исключение, НЕ совпадающее с lease-регэкспом (например, реальная ошибка прав
или блокировка ветки правилом защиты) — оно ловится в finish()
(catch (error) { ops.log(...) }), и branchDeleted остаётся null.
Комментарий в issue в этом случае вообще не упоминает попытку удаления —
ни «удалена», ни «оставлена: вершина сдвинулась». Различить в
issue-комментарии «ветка осталась, потому что автор допушил коммит»
(ожидаемо, задокументировано) от «ветка осталась из-за сбоя git/прав»
(неожиданно) можно только по логу job в Actions, который не хранится
вечно и не виден из issue. Не блокирует: инвариант «сбой удаления не
отменяет слияние» соблюдён, риска для данных нет — в худшем случае
лишняя ветка молча остаётся висеть, то есть today's статус-кво. Снимаю
без правки: цена читаемости лога ниже цены нового условного пути в
и так плотной функции finish().
Что проверено и корректно
deleteBranch(ref, expected)вrealOpsиспользует тот жеpushUrl(с токеном), что иpushWithLease, и тот же паттерн--force-with-lease=refs/heads/${ref}:${expected}— синтаксис реально поддерживается git для удаления (подтверждено экспериментом), не придуман.- Оба места вызова
finish(..., { branchTip })передают корректную «последнюю вершину, которую видело слияние»:tip(=actual) в fast-forward-пути, где ветка задачи вообще не пушится, иcandidate(уже реально запушенный вbranchстрокой раньше,pushWithLease(candidate, branch, tip)) в пути с ребейзом — совпадает с ожиданием юнит-тестов (cand-mat-on-dev1и т. п.). - Все исходы, где
merged !== true(conflict,rereview,validation-red,validation-missing,give-up,reject-stale), структурно не передаютbranchTipи не вызывают удаление — проверено и чтением, и тестами. commentFor()добавляет к тексту «слито» одну из двух опциональных фраз строго поctx.branchDeleted === true/false; приnull— исходный текст без изменений, обратной совместимости с существующими тестами комментариев не сломано (assert.match(commentFor('push', ctx), /… слито$/)по-прежнему проходит, т.к.ctx.branchDeletedне задан →undefined).- Мутант
merged-task-branch-keptсинтаксически корректен (findсовпадает один-в-один со строкой файла), гард называет реальный тест, реестр цел (node --test test/mutation-gate.test.mjs— 69/69, структурные проверки реестра проходят). - Трейлеры коммита:
Issue: #702,User-Visible: no— верно, продуктовый код (класс A) не менялся. - PROCESS.md §10.4 обновлён текстом, согласованным с кодом (обе ветки: «после push» покрывает и fast-forward, и обычный push).
- Ни один другой скрипт конвейера не читает ветку задачи после
S8-merged— удаление ничего дальше по цепочке не ломает. - Число, которое дифф делает видимым (368/370 влитых веток), — не дублируется в коде: это только текст комментария issue автора, не число в PROCESS.md или тесте; в самом диффе такого числа нет вовсе — нечему расходиться.
Чего не проверял
npx tsc --noEmit,npm test(полный),npm run build+ сверка трёх копий бандла — не перегонял: зелёный Validate на этом же SHA (3a681e24) уже подтверждён: https://github.com/Matysh/houseplan-card/actions/runs/36482861441- Браузерные смоки — не прогонял:
node scripts/smoke-select.mjs --base origin/dev --head HEADсам ответил «исполняемого frontend-диффа нет (src/**/*.tsне тронут) — смоки этим диффом не выбираются, выбирать нечего». Явных смоков в AC issue не названо. npm run golden:verify— не прогонял, нет изменений рендера/UI.python -m pytest tests_backend -q— не прогонял, нет правокcustom_components/**/*.py.npm run invariants— не прогонял, диффа геометрии/модели нет.- Performance-профили — не названы в AC, не прогонял.
- Реальное поведение
merge-candidate.mjsцеликом на живом GitHub Actions (реальныйgh/GH_TOKEN, реальные праваHP_PROCESS_TOKENна удаление ветки в этом конкретном репозитории) — не проверял; это уже ответственность реального прогонаintegrateпри следующем слиянии, не предмет ревью кода. Логика вокруг него (аргументы, условия, регэксп ошибки) проверена чтением и одним независимым git-экспериментом на временном репозитории (см. «Как проверялось»). - Разовая чистка 368 веток (пункт 2 issue) — не в этом коммите, поэтому не предмет код-ревью; когда владелец даст согласие и появится код/действие, это отдельный материал.
Гейты — сводка
| Гейт | Прогнан | Результат |
|---|---|---|
| typecheck / npm test / build+bundle-policy | нет — Validate на SHA материала | зелёный: run 36482861441 |
node --test test/merge-candidate.test.mjs |
да | 26/26 |
node --test test/process-digests.test.mjs |
да | 5/5 |
node --test test/mutation-gate.test.mjs |
да (целостность реестра) | 69/69 |
node scripts/mutation-gate.mjs --id=merged-task-branch-kept |
да | мутант убит, 1/1 |
node scripts/process-gate.mjs --range origin/dev..HEAD |
да | 0 предупреждений |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
да | нет frontend-диффа, смоки не выбираются |
golden:verify |
нет | не применимо (нет диффа рендера) |
pytest tests_backend |
нет | не применимо (нет правок Python) |
invariants |
нет | не применимо (нет правок геометрии) |
| ручной git-эксперимент force-with-lease на удаление ref | да | подтверждает регэксп ошибки в deleteBranch() соответствует реальному git |
Вердикт
Зелёный. AC выполнены и доказаны (автотестом + мутантом + независимым ручным экспериментом с реальным git на защитную часть); находок, блокирующих или требующих правки в скоупе, нет; единственная Low-находка снята с записью.
Материал раунда
- Ветка:
issue/702-delete-merged-branches, коммит3a681e24e490— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
387871be0d5bb95001506344a933150fb5ca5d16git log --all --format='%H %T' | grep 387871be0d5b - Тело issue:
34ce91f776aff8956b77aea955e69a919ee4f52be89c8e94beb90f1e9beb5e85 - Вердикт конвейера:
green· High 0