21 KiB
CODE-REVIEW-703-r1
Issue: #703 · «stable promotion ложно судит уже проверенную бета-линию»
Трек: show (владелец задал критерии в теле issue)
Материал: 4496c232a0a53186b5db426b584b99a431f7cfbb, ветка
issue/703-promotion-range-base поверх origin/dev
Заход: r1 · блокирующих циклов израсходовано 0 из 2
Скоуп
Инфраструктурная задача (не класс A, входит сразу в код-ревью, AGENTS.md «Tracks»). Три AC в теле issue:
- AC1 (unit, фикстура git) — промоушен SHA, уже зелёного на
dev, даёт пустой диапазон гейтамno-new-any/no-new-private-writesнаmain; без прогоновdevв окне API граница — последний тегv*, а не прошлый stable. - AC2 (unit) — новое нарушение после границы по-прежнему красное (hotfix над кандидатом; повтор упавшего HEAD).
- AC3 (контракт workflow + документация) —
changesи preflight наmainчитают прогоны обеих интеграционных веток, preflight больше не падает наevent.before, граница описана в PROCESS.md §10.2.
Изменение процессное — docs/SCOPE.md им не ограничивается (AGENTS.md,
«infrastructure task … skips analysis and spec»). User-Visible: no в трейлере
коммита — верно, поведения продукта дифф не меняет.
Как проверялось
Прочитаны: AGENTS.md, docs/process/REVIEWER.md, тело issue #703 и оба
комментария («Взял», «Сделано»), PROCESS.md §8, §10.2, §11.5, §11.6.
Дифф разобран целиком (git diff origin/dev...HEAD, 5 файлов):
scripts/classify-base.mjs, .github/workflows/validate.yml,
test/promotion-range.test.mjs (новый), test/validate-workflow.test.mjs,
PROCESS.md §10.2.
Прочитан scripts/classify-base.mjs целиком, не только дифф — проверено, что
pickRangeBase корректно приоритизирует HEAD (только доказанный успехом или
тегом), затем предков по объединению judged ∪ tagged, с верной атрибуцией
reason (green-ancestor vs release-tag).
Прогнал точечно (полный gate:small/tsc/build не гонял — см. «Чего не
проверял»):
node --test test/promotion-range.test.mjs→pass 6.node --test test/validate-workflow.test.mjs→pass 28.- Мутация: временно заменил
if (head && (headGreen.has(head) || tagged.has(head)))наif (false)вscripts/classify-base.mjs, перезапустилpromotion-range.test.mjs→pass 4, fail 2(AC1-тесты красятся), откатил файл (cpбэкапа),git status --shortпосле отката пуст — эксперимент следов не оставил. Тест умеет падать. - Независимо, вне тестовой фикстуры, воспроизвёл парсинг
git for-each-ref --format='%(refname:short) %(objectname) %(*objectname)'на настоящем репозитории с annotated и lightweight тегами — поле «коммит» (peeled || object) извлекается верно для обоих видов. node scripts/smoke-select.mjs --base origin/dev --head HEAD→ «Исполняемого frontend-диффа нет»; дифф не трогаетsrc/**, браузерные смоки не относятся.
Главная проверка этого раунда — воспроизвести производственное поведение
границы по тегам, а не только unit-фикстуру. AC1 второй сценарий
(«без прогонов dev … граница — последний тег») в test/promotion-range.test.mjs
создаёт теги прямо в свежесозданном git-репозитории и вызывает
classify-base.mjs там же — это НЕ то, как коммит попадает в рабочую копию
джобов preflight/changes в реальном Validate: там дерево кладёт
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 (# v7,
validate.yml:71,342) с fetch-depth: 0, filter: 'blob:none' и БЕЗ
fetch-tags: true. Я проверил исходники именно этого запиненного коммита
(curl тела action.yml и dist/index.js с того же SHA):
action.yml: входfetch-tags—default: false;dist/index.js, методfetch():args.push('--no-tags')безусловно, с комментарием «Always use --no-tags for explicit control over tag fetching»; refspec+refs/tags/*:refs/tags/*добавляется только когдаsettings.fetchTags === true(getRefSpec).
Затем эмпирически воспроизвёл именно эту последовательность: git init +
remote add + git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules … на репозитории с тегом, затем — как и делает шаг
changes/preflight дальше по файлу (git fetch -q origin dev,
validate.yml:371,726,841) — простой git fetch БЕЗ явного --no-tags. В обоих
случаях git for-each-ref refs/tags после фетча пуст: явный одиночный refspec
(origin dev) не включает git-механизм auto-follow тегов, а --no-tags на
первом fetch не оставляет вообще ни одного tracking-объекта для тега.
Убедился отдельно, что это не проблема моей среды ревью: в РАБОЧЕЙ КОПИИ этой
сессии (клонированной иначе, не через actions/checkout) теги есть —
несовпадение подтверждает, что дело в конкретном пути checkout-экшена, а не в
git вообще.
Находки
High — release-tag граница (AC1, вторая часть) не работает в реальном Validate: checkout никогда не тянет теги
scripts/classify-base.mjs:88-99 (releaseTaggedShas) и вызов на
scripts/classify-base.mjs:292-294 читают git for-each-ref … refs/tags
локально. Это верно работает в test/promotion-range.test.mjs, потому что
фикстура создаёт теги прямо в том же репозитории, где выполняется CLI. Но ни
один шаг preflight/changes в .github/workflows/validate.yml не
устанавливает fetch-tags: true на checkout (grep -n fetch-tags .github/workflows/validate.yml — пусто), а сам actions/checkout@… (v7,
пин 3d3c42e5aac5ba805825da76410c181273ba90b1) при fetch-tags: false
(дефолт) передаёт git fetch --no-tags безусловно и не добавляет refspec тегов
— подтверждено чтением dist/index.js этого экшена и независимо эмпирическим
повтором той же fetch-команды (см. «Как проверялось»). Последующие явные
git fetch -q origin dev (validate.yml:371,726,841) тоже не подтягивают теги:
явный одиночный refspec не запускает auto-follow.
Воспроизведение (без CI, тем же способом, что и для встроенного unit-теста, но с реальной последовательностью git-команд checkout-экшена):
git init /tmp/src && cd /tmp/src && ... commit ... && git tag -a v1.0.0 -m rel
cd /tmp/dst && git init && git remote add origin /tmp/src
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules \
origin +refs/heads/main*:refs/remotes/origin/main* +refs/heads/main:refs/remotes/origin/main
git for-each-ref refs/tags # пусто
git fetch -q origin dev # как later-шаги validate.yml
git for-each-ref refs/tags # по-прежнему пусто
Следствие. Основной путь AC1 (SHA, зелёный на dev в пределах окна API в
100 прогонов) не затронут — он работает через ответ GitHub API, а не через git
tag. Но именно вторая часть AC1 — «без прогонов dev в списке граница —
последний тег v*, а не прошлый stable» — заявлена как страховка НА СЛУЧАЙ,
когда SHA кандидата выпал из стодневного окна API (этот риск прямо назван
автором в комментарии «Сделано»: «Если SHA кандидата выпал из окна, границей
станет последний тег. Это шире, чем нужно, но не прошлый stable»). В
реальности это неверно: при выпадении из окна tagged пуст, pickRangeBase
падает мимо release-tag сразу в fallback (event.before), т.е.
воспроизводит ИМЕННО тот инцидент, ради которого заведено #703 — просто при
более редком триггере (окно API, а не разница dev/main). Ни один
существующий гейт это не ловит: test/promotion-range.test.mjs и
test/validate-workflow.test.mjs проверяют CLI и текст workflow-файла по
отдельности, ни один не проверяет фактическое поведение actions/checkout с
реальными его флагами.
Чем закрыть (для автора, не для этого ревью): добавить fetch-tags: true
к обоим checkout (validate.yml:71 в preflight, :342 в changes) — они и
так тянут fetch-depth: 0, полную историю, лишних данных это не добавит; и
дополнить test/validate-workflow.test.mjs (или новый тест) assertion на
fetch-tags: true рядом с уже добавленной проверкой fetch-depth: 0
(test/validate-workflow.test.mjs, новый тест «#703 AC3», последняя строка),
чтобы дальнейшая правка checkout не могла тихо снять флаг.
Low — тест AC2 не различает «hotfix на main» и «пуш в dev» на практике
test/promotion-range.test.mjs:121-134, тест
«#703 AC2: новое нарушение после границы красное — hotfix на main и пуш в dev»
вызывает rangeBase(fx, hotfix, { dev, main }, fx.candidate) дважды с
ИДЕНТИЧНЫМИ аргументами (onMain и onDev) — разницы между «событие на
main» и «событие на dev» в самом вызове нет, оба используют один и тот же
merged payload. Это осмысленно отражает инвариант «граница одна независимо от
ветки», но имя и комментарий теста обещают проверку двух разных путей, а
по факту второй ассерт (onDev) — дубликат первого, не отдельный сценарий.
Не блокирует: собственно инвариант («граница одна») проверен по построению
(mergeRunPayloads тестируется отдельно), а не через эту дублирующую
проверку. Можно оставить как есть — записываю, находка Low, не открывает
цикл (трек show, REVIEWER.md «Трек show»: бухгалтерия — Low).
Что проверено и корректно
- AC1, первая часть — исполнением:
test/promotion-range.test.mjs«#703 AC1: промоушен проверенного SHA в main — пустой диапазон и ноль нарушений» зелёный, мутация на строку HEAD-проверки красит его (см. «Как проверялось»). CLI действительно возвращаетbase = head, диапазон пуст,no-new-private-writesнаходит 0 вместо 2 старых записей. Работает через API-ответ (headGreen), не зависит от найденного дефекта с тегами. - AC2 — оба сценария (hotfix над кандидатом, повтор упавшего HEAD)
доказаны исполнением, зелёные, мутация на
firstGreen/headGreenлогику не ставилась отдельно, но покрывающий её путь (проверка «упавший HEAD не граница») в тесте#703: HEAD засчитывает только успех или тег…(test/promotion-range.test.mjs:148-168) явно различает упавший и доказанный HEAD через прямой вызовpickRangeBase— читал построчно, логика корректна. - AC3, контракт workflow — подтверждено чтением
validate.ymlи исполнениемtest/validate-workflow.test.mjs(строковые ассерты на обновлённыйif:preflight, циклfor branch in dev main, изменениеchanges/base-шага сother=dev/main). Preflight действительно больше не падает наevent.beforeдляmain: шаг «База диапазона» теперь выполняется и дляrefs/heads/main,BEFORE_SHAуprovenance/process_gateпереключается наsteps.range.outputs.base. - PROCESS.md §10.2 — текст соответствует коду один в один (три условия
границы, HEAD только по успеху/тегу, ссылка на реализацию и тест); добавлен
в верное место — рядом с уже существующей оговоркой «При продвижении в
mainне перепроверяются коммиты, уже достижимые из prerelease-тега». - Трейлеры коммита —
Issue: #703,User-Visible: no— верны, никакого видимого пользователю поведения дифф не меняет; changelog не тронут и не должен быть. - «Одно число — один источник» (§8) — неприменимо, дифф не вводит величин, видимых пользователю.
releaseTaggedShasкорректно разбирает и annotated, и lightweight теги (проверено отдельным прогономgit for-each-refна реальном репозитории — см. «Как проверялось»), фильтрRELEASE_TAGотсекаетbeta.0и немаркированные теги верно (юнит-тест плюс мой ручной прогон совпали).- Изменение не трогает геометрию модели, Python-бэкенд, golden-эталоны — соответствующие гейты неприменимы.
Чего не проверял
- Полный
npx tsc --noEmit/npm test/npm run buildс сверкой трёх копий бандла не гонял заново: Validate на этом SHA (4496c232) зелёный (https://github.com/Matysh/houseplan-card/actions/runs/36745546331), §8/AGENTS.md разрешают на него сослаться. Прогнал точечно только два изменённых тестовых файла (см. «Как проверялось») — это не замена полного набора. - Браузерные смоки —
smoke-selectсообщает «исполняемого frontend-диффа нет», тело issue не называет ни одного смока в AC; Chromium не поднимал. npm run golden:verify— меткиci:goldenнет,demo/golden/**не тронут.python -m pytest tests_backend—custom_components/**/*.pyне тронут.npm run invariants— дифф не касается геометрии модели плана.- Performance-профили — не названы в AC, дифф не трогает рендер/performance код.
- Мутанты по диффу — трек
show: мутанты в разработке не гоняются ни на каком треке (REVIEWER.md, «Трек show», #709); в этом диффе и нет продуктового кода, защита — чистые юниты с отрицательным случаем в самом тесте (см. таблицу ниже), реестр мутантов не применим. - Живой промоушен
dev → mainна реальном GitHub Actions не воспроизводил — автор прямо пишет «НЕ сделано: живой промоушен в main этим не проверить до следующего stable»; это ожидаемо и не находка само по себе, но именно поэтому расхождение checkout-экшена с ожиданием теста осталось незамеченным до этого ревью.
Таблица «AC · чем доказан · чем краснеет» (защитные AC)
| AC | чем доказан | чем краснеет |
|---|---|---|
| AC1 (пустой диапазон на доказанном SHA) | test/promotion-range.test.mjs тест «#703 AC1: промоушен …» |
мутация if (false) на HEAD-проверку — падает (проверено в этом раунде) |
| AC1 (тег как граница без прогонов dev) | тот же файл, тест «#703 AC1: без прогонов dev …» | не красит в производственном пайплайне — см. находку High: releaseTaggedShas в реальном Validate всегда получает пустой набор тегов, юнит-тест этого не ловит, т.к. не проходит через actions/checkout |
| AC2 (новое нарушение красное) | тесты «#703 AC2: …» ×2 | отрицательный случай внутри самого теста (упавший HEAD vs доказанный) |
| AC3 (workflow-контракт) | test/validate-workflow.test.mjs, новый тест |
строковые ассерты на regex — точечно, но соразмерно (контракт YAML, не поведение) |
Вердикт
Красный. Один High: вторая часть AC1 (release-tag как граница, когда SHA
кандидата выпал из окна API в 100 прогонов) реализована в
scripts/classify-base.mjs корректно, но неработоспособна в реальном
Validate — ни один из задействованных шагов actions/checkout не получает
fetch-tags: true, и пин v7 (3d3c42e5aac5ba805825da76410c181273ba90b1)
безусловно передаёт --no-tags. Это тихо воспроизводит именно тот класс
инцидента, ради которого заведено #703, при более редком, но реальном
триггере. Основной путь AC1 (доказанный SHA в пределах окна API) и оба
сценария AC2 работают верно. Один Low — тестовая избыточность, не блокирует.
Материал раунда
- Ветка:
issue/703-promotion-range-base, коммит4496c232a0a5— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
68cd755f1339d9f4cedc1a28b173b15519540bedgit log --all --format='%H %T' | grep 68cd755f1339 - Тело issue:
d3b8c4a60a2666a458283fb684284710ad5982fe267e8a362f852f3dedaa5761 - Вердикт конвейера:
red· High 1