21 KiB
CODE-REVIEW-514-r4
- Issue: https://github.com/Matysh/houseplan-card/issues/514 — «E2E на реальном HA как гейт стабильного релиза»
- ТЗ:
docs/specs/514-e2e-stable-release-gate.md(зелёное ревью ТЗ,docs/reviews/SPEC-REVIEW-514-r1.md, r1, High 0/Medium 0) - Материал ревью: ровно
0ce5e4eed3d1d2e3a52f07c35dbc9a8297dd01a2(рабочая копия на нём).git log --oneline origin/dev..HEAD— 9 коммитов.git diff origin/dev...HEAD— 15 файлов, +1033/−3. - Заход: r4 · блокирующих циклов израсходовано 1 из 4 (израсходован жёлтым вердиктом r3; зелёный вердикт бюджет не тратит, #227)
Материал предыдущего раунда (SHA не резолвится — обычное дело, §2.10)
r3 (docs/reviews/CODE-REVIEW-514-r3.md, вердикт жёлтый, комментарий issue 2026-09-09T21:16:28Z) объявил материал d166bad3eb293d70cdd37cc715c639449742d988. Этот объект в текущем репозитории уже не резолвится:
git cat-file -t d166bad3eb293d70cdd37cc715c639449742d988
→ fatal: git cat-file: could not get object info
Между r3 и этим заходом случился ещё один сдвиг базы (см. комментарий issue 2026-09-09T21:21:59Z: workflow_sync увидел, что main отстал от dev по process.yml после слияния #515, конвейер сделал зеркальный коммит aeb0a473 и пере-опубликовал ветку задачи) — commit-хэши коммитов задачи пересчитались от нового родителя, содержимое не изменилось. Якоря по дереву/блобу, которые пережили бы это (git log --all --format='%H %T' | grep <дерево>), r3 не оставил — его собственный ручной блок «Материал раунда» назвал только commit SHA, а машинный блок конвейера в конце документа относится к более раннему, никогда не рецензированному состоянию (6d28e170, до второго ребейза). Восстановление сделано по содержимому, не по имени:
git diff origin/dev...a8a9b7d9 --stat
→ 14 files changed, 824 insertions(+), 3 deletions(-)
(тот же список файлов и те же числа, что в шапке CODE-REVIEW-514-r3.md)
git merge-base --is-ancestor a8a9b7d9 HEAD && echo yes → yes
git log --oneline a8a9b7d9..HEAD
→ 0ce5e4ee ci: the E2E gate fails loudly when the release list cannot be read
ed6d1d76 docs: review document for #514
a8a9b7d9 — content-эквивалент d166bad3 (тот же диапазон правок, то же число файлов/строк, тот же список путей), переживший последующий ребейз под другим именем. Это и есть материал, на котором был вынесен вердикт r3; от него считается дельта этого раунда.
Почему разбор по дельте, а не полный. База origin/dev не продвинулась между r3 и r4 (git merge-base origin/dev HEAD и git merge-base origin/dev a8a9b7d9 — оба ad2858a8, один и тот же коммит): ребейза на ушедший вперёд dev в этот раз не было, только пересборка ветки поверх той же базы (мираж-коммит aeb0a473 ушёл в main, не в диапазон задачи). Контракт поведения не менялся, новая подсистема не задета, объём дельты (2 коммита, 3 файла, +24/−5 строк) на порядок меньше исходной задачи. Условие «разбор остаётся полным» (§2.9/§2.10) не выполнено ни по одному пункту — сокращённый разбор оправдан.
Дельта r3 → r4
git diff a8a9b7d9..HEAD --stat
| Коммит | Класс | Содержимое |
|---|---|---|
ed6d1d76 |
C (docs) | публикация docs/reviews/CODE-REVIEW-514-r3.md конвейером — только новый файл, продуктовых строк нет |
0ce5e4ee |
B+C | scripts/e2e-gate.mjs (+11/−2), test/e2e-gate.test.mjs (+12), PROCESS.md (+3/−3) — фикс M1(r3) и формулировки L4(r3) |
Класс A не задет (как и во всём диапазоне origin/dev..HEAD); трейлеры Issue: #514 / User-Visible: no на обоих коммитах корректны (git show -s --format=full).
Закрытие раунда r3
| Находка r3 | Чем закрыта | Где это видно |
|---|---|---|
M1 (Medium, в скоупе) — фолбэк-токен, scoped только на houseplan-e2e, молча ломает previousStable через проглоченную ошибку gh release list в releases(), воспроизводя дефект 4143f998 под чужим диагнозом |
scripts/e2e-gate.mjs: releases() теперь проверяет r.status !== 0 и бросает Error с текстом gh release list <repo>: <stderr> — так же, как уже делал dispatch(); исключение попадает в тот же catch блок e2eGate → result: 'error' с TOKEN_HINT. TOKEN_HINT переписан: называет оба репозитория и обе нужные операции («Actions: write на houseplan-e2e И чтение релизов houseplan-card»), а не только первый |
scripts/e2e-gate.mjs:28,80,119-123; новый тест test/e2e-gate.test.mjs:117-127 (#514 r3 M1) — см. проверку ниже |
L4 (Low, новая в r3) — PROCESS.md:704-706 путал подлежащее и дополнение («E2E … запускает и ждёт release.yml», хотя наоборот) |
Строка переписана: «release.yml сам запускает e2e.yml в houseplan-e2e на теге и ждёт его зелёного (#514)» |
PROCESS.md:704-706 (diff выше) |
| L1 (унаследована из r1/r2, подтверждена чтением в r3) | Не требовала правки — Low принят без изменений | код classifyRun/цикл ожидания в e2eGate (строки ~62-97) не тронут дельтой r3→r4, см. «Унаследовано» |
| L2 (унаследована из r1/r2) | Не требовала правки — файл вне материала процесса houseplan-card | не тронут |
| L3 (унаследована из r1/r2) | Не требовала правки — принятый паттерн из validate-gate.mjs |
не тронут |
Мутация подтверждает закрытие M1, а не заявление автора. Временно откатил фикс (вернул releases() к варианту «проглотить ошибку», без throw) и прогнал новый тест:
node --test test/e2e-gate.test.mjs (с откаченным releases())
→ not ok 11 - #514 r3 M1: ...
error: 'Missing expected rejection.'
Тест краснеет на снятой защите — «умеет падать» подтверждено, не заявлено. Восстановил scripts/e2e-gate.mjs из бэкапа, git status --short — пусто, рабочая копия чиста.
С фиксом на месте:
node --test test/e2e-gate.test.mjs test/release-workflow.test.mjs
→ # tests 15 # pass 15 # fail 0 (было 14/14 на материале r3 — новый тест M1 прибавил один)
| AC / защита | Чем доказан | Чем краснеет |
|---|---|---|
AC1, ветка отказа releases() (M1) |
test/e2e-gate.test.mjs:117-127, тест #514 r3 M1 |
откат releases() к варианту без throw → not ok, Missing expected rejection (прогнано лично, см. выше) |
Мутанты scripts/mutation-gate.mjs дельту не затронули (файл не менялся между r3 и r4) — прогнаны все три для очистки:
node scripts/mutation-gate.mjs --id=release-ships-on-red-e2e → поймано 1 из 1
node scripts/mutation-gate.mjs --id=release-upgrades-stable-onto-itself → поймано 1 из 1
node scripts/mutation-gate.mjs --id=release-trusts-foreign-e2e-run → поймано 1 из 1
Унаследовано из r3
Без повторной проверки в этом раунде принято (делта их не задевает):
- AC2 (опознание своего прогона по
HP <tag>) и AC4 (journeys-devтолько по расписанию) — кодclassifyRun/isOurRun(scripts/e2e-gate.mjs:62-97) и контрактhouseplan-e2e/e2e.ymlдельтой не тронуты; проверены в r3 напрямую (gh api repos/Matysh/houseplan-e2e/contents/...), см.docs/reviews/CODE-REVIEW-514-r3.md, материал (content-эквивалент)a8a9b7d9. - AC3 (пре-релизы шаг не выполняют) —
.github/workflows/release.ymlне входит в дельту r3→r4 (git diff a8a9b7d9..HEAD -- .github/workflows/release.ymlпуст);test/release-workflow.test.mjsне менялся и зелёный (см. прогон выше, 2/15 из общего числа). - AC5 (три мутанта ловят снятую защиту) — код мутантов не менялся; перепрогнан в этом раунде для очистки (см. таблицу выше), не унаследован голословно.
- L1 (плановая job
houseplan-e2e/planупадёт раньше первой именованной суб-job →missing) — строкиscripts/e2e-gate.mjs:62-97, где живёт этот путь, не входят в дельтуa8a9b7d9..HEAD. Инвариант «не публиковать на красном» не нарушается. - L2 (README
houseplan-e2eможет буквально описыватьupgrade_from=stable) — внешний файл, вне диапазонаorigin/dev...HEADв обоих раундах. - L3 (окно
gh run list --limit 10) — код опроса не менялся. - Порядок
gh release list(новые первыми), на котором стоитpreviousStable— кодpreviousStableне в дельте r3→r4, подтверждён в r1/r2/r3 (см. цепочку вCODE-REVIEW-514-r3.md, раздел «Унаследовано из r2»). - Документация вне
PROCESS.md:704-706(AGENTS.md,docs/DEVELOPMENT.md,docs/TESTING.md,docs/specs/README.md) — не в дельте, была сверена построчно в r3.
Дешёвые гейты — уже подтверждены на этом SHA
Validate на 0ce5e4ee зелёный: https://github.com/Matysh/houseplan-card/actions/runs/34406969841 (в тексте раунда это дано как факт; проверил head_sha не переспрашивал повторно — ссылка та же, что в условии раунда). Значит npx tsc --noEmit, npm test (полный), npm run build со сверкой бандла не перегонялись — правило сужения гейтов прямо это разрешает. Дополнительно лично прогнано (диф это код меняет):
node --test test/e2e-gate.test.mjs test/release-workflow.test.mjs→ 15/15.- Мутация на M1 (откат защиты → тест краснеет) — см. выше.
- Три именованных мутанта
mutation-gate.mjs→ 1/1 каждый. node scripts/process-gate.mjs --issues→ «гейт пройден, предупреждений 1» (п.8, ожидаемо — класс A не задет).
Не прогонялось и почему: golden:verify (дельта не меняет рендер), check-docs.mjs/model-invariants.mjs/браузерные смоки (дельта не трогает src/**, геометрию или layout), pytest tests_backend (Python не тронут), performance-профили (не названы в AC, перф-путь не тронут). Полный список demo/smoke_*.mjs/smoke-select.mjs не запрашивал — выбирать нечего, src/** не в диапазоне ни разу за все 4 раунда.
Одно число — один источник. Неприменимо: дельта не добавляет и не меняет ни одной пользователем видимой величины (изменения — в тексте ошибки CI-скрипта и в формулировке PROCESS.md).
Новая находка
L5 (Low, новая в r4) — docs/specs/514-e2e-stable-release-gate.md:38 дословно цитирует старый текст TOKEN_HINT, который правка этого раунда изменила
Спецификация иллюстрирует ошибку отказа токена цитатой: «текстом «нужен секрет E2E_DISPATCH_TOKEN с правом Actions: write на houseplan-e2e»». После фикса M1 фактический TOKEN_HINT (scripts/e2e-gate.mjs:28) — «нужен секрет E2E_DISPATCH_TOKEN: Actions: write на houseplan-e2e И чтение релизов houseplan-card (fine-grained PAT — оба репозитория в списке)». Цитата в ТЗ устарела относительно кода.
Не блокирует: ТЗ — иллюстративный, не нормативный текст в этом месте (описывает пример сообщения, а не контракт с проверяемым AC на точную строку), спецификации не переоткрываются задним числом при каждой правке реализации (§2.3 — статус ТЗ не меняется после S5-ready), и ни один AC/тест не сверяет спек-текст с TOKEN_HINT дословно. Оставляю Low без правки, с записью: если задача вернётся на ещё один правочный цикл по другой причине, эту строку стоит поправить заодно.
Что проверено и корректно
- M1(r3) закрыт по существу, а не косметически:
releases()теперь симметриченdispatch()— оба бросают наr.status !== 0и оба ловятся одним и тем жеcatchвe2eGate, даваяresult: 'error'с подсказкой по токену. Подтверждено мутацией (откат → тест краснеет), не только чтением. TOKEN_HINTтеперь называет оба репозитория и обе операции — точное описание того, что нужно фолбэк-токену, чтобы не наступить на M1 повторно.- L4(r3) исправлена текстуально верно: подлежащее (
release.yml) и дополнение (houseplan-e2e/e2e.yml) на своих местах. - Дельта не расширяет скоуп: оба коммита правят ровно то, что назвал вердикт r3 (M1 + L4), новых файлов/модулей не добавлено, кроме публикации самого документа r3.
- Трейлеры обоих коммитов корректны (
Issue: #514,User-Visible: no);User-Visible: noпо-прежнему верно — правка видна только в тексте ошибки CI-гейта и вPROCESS.md, не пользователю продукта. - База
origin/devне сдвигалась между r3 и r4 — условие для сокращённого делта-разбора выполнено, а не предположено.
Чего не проверял и почему
tsc --noEmit,npm test(полный),npm run buildс попиксельной сверкой бандла — зелёный Validate на0ce5e4eeуже покрывает (ссылка на прогон дана в условии раунда).golden:verify,check-docs.mjs,model-invariants.mjs,pytest tests_backend, браузерные смоки — дельта не трогаетsrc/**, геометрию/layout/толщины или Python; так было во всех четырёх раундах этой задачи.- Реальный dispatch
release.ymlпо событиюrelease: publishedс настоящимE2E_DISPATCH_TOKEN— секрет ещё не заведён владельцем (это и был контекст M1 — риск в конфигурации, которая пока не существует), недоступно ревьюеру. Первая живая проверка — следующий stable-релиз, как честно называет ТЗ. - README
houseplan-e2e— не перечитывал повторно, вне диапазонаorigin/dev...HEADво всех раундах, контракт имени job (единственное, от чего зависит код) был сверен напрямую в r3 и код, от которого он зависит, не менялся.
Продуктовое рассуждение
Дельта не меняет продуктовую оценку r3: задача по-прежнему укрепляет цепочку поставки (гейт стабильного релиза дожидается настоящего E2E), в скоуп docs/SCOPE.md не входит напрямую (инфраструктура процесса), беты не задевает. M1 был риском в конфигурации, которую владелец ещё не завёл; фикс убирает этот риск заранее, не дожидаясь, пока он реализуется в проде. Ни один AC не ослаблен правкой — только усилен (releases() больше не может тихо соврать).
Вопросы владельцу
Нет открытых продуктовых вопросов.
Материал раунда
- Ветка:
issue/514-e2e-stable-release-gate, коммит0ce5e4eed3d1d2e3a52f07c35dbc9a8297dd01a2. - Диапазон:
origin/dev..HEAD(9 коммитов),origin/dev=ad2858a80cc7be36a8577c8b05ea330dca66eba5. - ТЗ:
docs/specs/514-e2e-stable-release-gate.md; ревью ТЗ:docs/reviews/SPEC-REVIEW-514-r1.md(зелёное, r1). - Предыдущий код-ревью: r3, жёлтый, материал
d166bad3eb293d70cdd37cc715c639449742d988(осиротел после ребейза; content-эквивалент восстановлен какa8a9b7d99e61af685149c903d34fab84c1008d90, см. раздел выше). - Дельта r3→r4:
git diff a8a9b7d9..HEAD— 2 коммита (ed6d1d76docs-only,0ce5e4eeM1+L4 фикс), 3 файла кода/процесса, +24/−5. - Validate на материале раунда: https://github.com/Matysh/houseplan-card/actions/runs/34406969841 (success, дано условием раунда).
- Вердикт этого раунда: зелёный · High 0 · Medium 0 · Low 4 (L1-L3 унаследованы; L4 закрыта фиксом этого раунда; L5 новая, принята без правки).
Материал раунда
- Ветка:
issue/514-e2e-stable-release-gate, коммит0ce5e4eed3d1— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
55fd573338326d3dbde36a2e4444439cce493833git log --all --format='%H %T' | grep 55fd57333832 - ТЗ
docs/specs/514-e2e-stable-release-gate.md, блоб30795778fee11c8df610440dd86b745112e120c4git log --all --find-object=30795778fee11c8df610440dd86b745112e120c4 -- docs/specs/514-e2e-stable-release-gate.md - Вердикт конвейера:
green· High 0