Files
houseplan-card/docs/reviews/CODE-REVIEW-514-r3.md
T
2026-09-09 21:18:51 +00:00

191 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-514-r3
- **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)
- **Материал ревью:** ровно `d166bad3eb293d70cdd37cc715c639449742d988` (рабочая копия на нём). `git log --oneline origin/dev..HEAD` — 7 коммитов. `git diff origin/dev...HEAD` — 14 файлов, +824/−3.
- **Заход:** r3 · блокирующих циклов израсходовано 0 из 4
## Почему разбор полный, а не по дельте
r2 (комментарий issue, 2026-09-09T19:41:15Z) вынес **зелёный** вердикт на материале `539756cc`, 0 High/0 Medium. Пока ревью r2 шло, `dev` продвинулся, ветка была ребейзнута на `dev@6b592c13`, кандидат стал `6d28e170` — этот кандидат ни разу не рецензировался (страж вернул задачу в `S7-code-review` автоматически до начала нового захода). Затем конвейер ещё раз привёл ветку к `dev` до того, как это ревью началось: поверх `6d28e170` легло 4 коммит(а) `dev` (докладки предыдущих задач #508/#502 и `fix: the summary panel settings dialog scrolls in Home Assistant`), дав текущий `d166bad3`. `git merge-base 6d28e170 HEAD` = `6b592c13` — `6d28e170` не является предком текущего `HEAD`, это осиротевший коммит другого дерева. Ребейз на ушедший вперёд `dev` — это другой код (§7.2), и инструкция к этому раунду прямо предписывает полный разбор. Прочитан и перепроверен весь diff `origin/dev...HEAD` заново, ни один вывод r2 не принят на слово.
## Скоуп изменений
1. `scripts/e2e-gate.mjs` (новый, 149 строк) — dispatch `e2e.yml` в `houseplan-e2e` на теге релиза, опознание своего прогона, ожидание, CLI.
2. `.github/workflows/release.yml`, job `gate` — шаг «Require green E2E on a real Home Assistant for a stable release» после Full Performance, только для `!prerelease`.
3. `scripts/mutation-gate.mjs` — три новых мутанта (`release-ships-on-red-e2e`, `release-upgrades-stable-onto-itself`, `release-trusts-foreign-e2e-run`).
4. `test/e2e-gate.test.mjs` (12 тестов, новый), `test/release-workflow.test.mjs` (2 теста, новый).
5. Документация: `PROCESS.md`, `AGENTS.md`, `docs/DEVELOPMENT.md`, `docs/TESTING.md`, `docs/specs/README.md`.
6. `docs/specs/514-e2e-stable-release-gate.md`, `docs/reviews/{SPEC,CODE}-REVIEW-514-r1.md`, `docs/reviews/CODE-REVIEW-514-r2.md` — процессные артефакты, не код.
Файлы только класса B (`scripts/**`, `test/**`, `.github/workflows/**`) и C (`docs/**`); класс A (`src/**`, `custom_components/**`) не задет. Все 7 коммитов несут `Issue: #514` / `User-Visible: no` — верно: изменение видно только владельцу в логе гейта релиза, ни один changelog трогать не нужно.
## Как проверялось
**Дешёвые гейты.** Условие раунда называет Validate зелёным на точном `d166bad3`; проверено напрямую, не на слово:
```
gh api repos/Matysh/houseplan-card/actions/runs/34404724698 --jq '{head_sha,conclusion,event,status}'
→ {"conclusion":"success","event":"workflow_dispatch","head_sha":"d166bad3eb293d70cdd37cc715c639449742d988","status":"completed"}
```
`head_sha` совпадает с материалом ревью, `conclusion: success` — `npx tsc --noEmit`, `npm test`, `npm run build` с сверкой бандла не перегонялись целиком, как разрешает правило «дешёвые гейты уже подтверждены».
Прогнано лично на этом SHA (диф это код меняет, поэтому — то, что относится к диффу):
- `node --test test/e2e-gate.test.mjs test/release-workflow.test.mjs` → `# pass 14 # fail 0`.
- `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».
- `node scripts/process-gate.mjs --issues` → «гейт пройден, предупреждений 1» (единственное предупреждение — п.8, ожидаемо: инфраструктурный диапазон без файлов класса A, статусная метка issue не требуется).
**Не прогонялось и почему** (правило «объём гейтов соразмерен задаче»): `golden:verify` (diff не меняет рендер), `check-docs.mjs`/`model-invariants.mjs`/браузерные смоки (`src/**`, геометрия, `layout` не затронуты — вся правка в `.github/workflows`, `scripts/e2e-gate.mjs`, `scripts/mutation-gate.mjs`, тестах и `docs/**`), `pytest tests_backend` (Python не тронут), performance-профили (не названы в AC, перф-путь не тронут).
**Внешний контракт (`houseplan-e2e`), не принятый на слово хендоффа/предыдущих раундов.** `scripts/e2e-gate.mjs` целиком зависит от контракта имени job в чужом репозитории — перепроверено напрямую в этом раунде, не по цитате из r1/r2:
```
gh api repos/Matysh/houseplan-e2e/contents/.github/workflows/e2e.yml
```
Подтверждено построчно: job `plan` собирает матрицу `node -e`-скриптом; `journeys-dev` добавляется только `if (schedule)` (AC4 — на `workflow_dispatch` этой ветки в матрице нет); job `e2e` называется `"${{ matrix.suite }} · HP ${{ matrix.ref }} · HA ${{ matrix.ha }}"` — совпадает с `TAG_SUITES`/`isOurRun`/`classifyRun` в `scripts/e2e-gate.mjs` буква в букву; `concurrency.cancel-in-progress: false` — подтверждает трактовку `cancelled` как «отменено рукой», а не конкурентной отменой. Оба репозитория публичные (`gh api repos/.../--jq .private` → `false`), это существенно для находки M1 ниже.
## Проверка AC (`docs/specs/514-e2e-stable-release-gate.md` §10)
| AC | Проверено | Результат |
|---|---|---|
| AC1 (гейт держит ассеты до `success`; красный/missing/cancelled/error — с причиной и ссылкой) | `test/e2e-gate.test.mjs` (7 тестов green/red/missing/timeout/cancelled/token-error), чтение `e2eGate` целиком, мутант `release-ships-on-red-e2e` | доказан, **кроме** одного пути отказа токена — см. M1 |
| AC2 (опознание своего прогона по `HP <tag>`, чужой dispatch игнорируется) | тесты `isOurRun`/`classifyRun`, контракт имени job в `houseplan-e2e/e2e.yml` (сверен напрямую), мутант `release-trusts-foreign-e2e-run` | доказан |
| AC3 (пре-релизы шаг не выполняют) | `test/release-workflow.test.mjs`, прочитан `release.yml:62` — `if: ${{ !github.event.release.prerelease }}`, идентично соседнему шагу Full Performance | доказан |
| AC4 (`journeys-dev` не бежит на dispatch) | прочитан `houseplan-e2e/e2e.yml` напрямую: `if (schedule) include.push(...)` — на `workflow_dispatch` `schedule` ложно | доказан чтением |
| AC5 (все три мутанта пойманы) | лично прогнаны все три — «поймано 1 из 1» каждый | доказан |
| AC6 (документация §8, `User-Visible: no`, UX/i18n/модель/перф не затронуты) | диф `PROCESS.md`/`AGENTS.md`/`docs/DEVELOPMENT.md`/`docs/TESTING.md`/`docs/specs/README.md` сверен построчно с §8 ТЗ; трейлеры всех 7 коммитов проверены `git log --format` | доказан, с находкой L4 (формулировка) |
## Находки
### M1 (Medium, в скоупе задачи) — фолбэк-токен `E2E_DISPATCH_TOKEN`, если он scoped только на `houseplan-e2e`, молча ломает `previousStable` и воспроизводит уже дважды исправленный дефект самообновления
`release.yml` использует один и тот же `GH_TOKEN` для *всех* вызовов `gh` внутри `scripts/e2e-gate.mjs` за один запуск процесса:
```yaml
env:
GH_TOKEN: ${{ secrets.E2E_DISPATCH_TOKEN || secrets.HP_PROCESS_TOKEN }}
```
Но `e2eGate` этим единственным токеном обращается к **двум разным репозиториям**:
- `ops.dispatch()` / `ops.listRuns()` / `ops.jobs()` → `Matysh/houseplan-e2e` (нужен `actions: write`/`read`);
- `ops.releases()` → `Matysh/houseplan-card` (тот самый репозиторий, где выполняется workflow) — вызывается **первым**, до dispatch, чтобы вычислить `upgradeFrom` (`previousStable`).
`TOKEN_HINT` и текст в ТЗ §4 п.1/§12 описывают `E2E_DISPATCH_TOKEN` как «секрет с правом Actions: write на houseplan-e2e» — то есть буквально тот сценарий, для которого владелец заведёт fine-grained PAT со списком репозиториев **только** `houseplan-e2e` (это единственный способ дать `actions: write` fine-grained токеном — вариант «Public repositories, read-only» прав на запуск workflow не даёт вообще). У такого токена нет доступа к `houseplan-card`, даже притом что оба репозитория публичные: fine-grained PAT с явным списком репозиториев не получает implicit-доступ к другим репозиториям только на основании их публичности — это документированное ограничение модели fine-grained-токенов, не зависящее от `private`/`public`.
Дальше цепочка молчит, а не падает явно:
```js
// scripts/e2e-gate.mjs:117-119
releases: async () => parse(exec('gh', ['release', 'list', '--repo', cardRepo, ...])),
```
```js
// scripts/e2e-gate.mjs:117 (helper)
const parse = (r) => (r.status === 0 && r.stdout ? JSON.parse(r.stdout) : []);
```
`parse` не бросает исключение на ненулевом коде выхода `gh` (в отличие от `dispatch()`, которая явно проверяет `r.status !== 0` и кидает `Error`, — но эта явная проверка есть только у `dispatch`, не у `releases`/`listRuns`/`jobs`). При отказе (403 на `houseplan-card`) `releases()` вернёт `[]`, `previousStable([], tag)` тихо даст фолбэк `'stable'` (та самая ветка в коде: `return prior[0]?.tagName || 'stable';`), и `dispatch(tag, 'stable')` **успешно** уйдёт в `houseplan-e2e` (для этого вызова токен как раз имеет права) — с параметром `upgrade_from=stable`.
Это ровно дефект, который коммит `4143f998` в этой же ветке уже один раз исправлял по живому прогону 09.09 (`houseplan-e2e` run `34392391382`): к моменту `release: published` тег уже сам является последним stable, `upgrade_from=stable` заставляет suite `upgrade` обновлять релиз сам на себя и падать с `Expected: not "1.73.0"`. При срабатывании M1 гейт **корректно** покажет `red` (инвариант «не публиковать на красном» не нарушен — сборка не уйдёт пользователям), но диагноз в выводе гейта будет:
```
result=red
note=E2E на v1.75.0 завершился: failure
```
— неотличимо от обычного упавшего E2E-сценария, а не от токен-проблемы. Владелец, увидев красный `upgrade` с текстом `Expected: not "1.75.0"`, с высокой вероятностью примет это за **регрессию уже исправленного дефекта** (`4143f998`/`b36e19c1`) и потратит время на разбор логики `previousStable`/`isOurRun`, хотя причина — токен без доступа к `houseplan-card`, которая нигде явно не всплывает (ни в выводе `e2e-gate.mjs`, ни в логе шага `release.yml`). Именно для этого класса отказов и была добавлена явная `TOKEN_HINT` — но она покрывает только ошибку в `dispatch()`, а не в `releases()`.
**Воспроизведение (логическое, по коду, не по живому прогону — секрет `E2E_DISPATCH_TOKEN` ещё не заведён владельцем):**
1. Владелец заводит `E2E_DISPATCH_TOKEN` как fine-grained PAT, «Only select repositories: houseplan-e2e», `actions: write` — ровно то, что просит `TOKEN_HINT`.
2. `release.yml` подставляет его в `GH_TOKEN` для всего шага (E2E_DISPATCH_TOKEN непустой → используется он, не `HP_PROCESS_TOKEN`).
3. `ops.releases()` → `gh release list --repo Matysh/houseplan-card` падает с 403, `parse` глотает ошибку, возвращает `[]`.
4. `previousStable([], tag)` → `'stable'`.
5. `ops.dispatch(tag, 'stable')` уходит успешно (у токена есть права на `houseplan-e2e`).
6. Suite `upgrade` обновляет тег на самого себя, падает; гейт красный с обычным на вид текстом об упавшем E2E, без единого упоминания токена или `houseplan-card`.
**Почему это Medium, а не Low, и не тот же класс, что уже принятые Low-находки r1/r2 (плановая job упала раньше первой именованной суб-job → `missing`).** Там результат — менее точное, но честно неопределённое сообщение («не появился/не завершился»); здесь результат — **правдоподобно замаскированный под уже однажды диагностированный и исправленный баг** конкретный симптом (`Expected: not "<tag>"` в suite `upgrade`), что стоит владельцу реального времени на ложный след, причём именно в том месте, которое эта же задача дважды чинила по живым прогонам 09.09.
**В скоупе задачи** — оба места (`scripts/e2e-gate.mjs`, `.github/workflows/release.yml`) — центральные файлы этого диффа. Правка дешёвая: например, `releases()` должна либо бросать (`throw`) на `r.status !== 0`, как это уже делает `dispatch()`, чтобы ошибка попала в тот же `catch` блок `e2eGate` и вернула `result: 'error'` с текстом про токен, либо — чище — вызов `gh release list --repo Matysh/houseplan-card` должен идти под `github.token` (у него всегда есть доступ на чтение к репозиторию, где выполняется сам workflow), а `E2E_DISPATCH_TOKEN`/`HP_PROCESS_TOKEN` — только под вызовы в сторону `houseplan-e2e`. Не блокирует сегодняшнее поведение (сейчас `E2E_DISPATCH_TOKEN` не существует, `HP_PROCESS_TOKEN` имеет широкий `repo`-scope и покрывает оба репозитория) — риск реализуется только если/когда владелец заведёт секрет ровно так, как просит `TOKEN_HINT`.
### Low (приняты без правки, с записью)
- **L1 (унаследовано из r1/r2, перепроверено чтением заново на `d166bad3`).** Если job `plan` в `houseplan-e2e/e2e.yml` сама завершится `failure` раньше, чем появится хотя бы одна job вида `· HP … ·`, `classifyRun` вернёт `'unknown'`, а поскольку прогон уже `completed`, ветка `if (kind === 'foreign' || candidate.status === 'completed') foreign.add(...)` в `e2eGate` навсегда пометит его как чужой — результат `missing` вместо точной причины. Инвариант «не публиковать на красном» не нарушается. Подтверждено чтением `scripts/e2e-gate.mjs:83-97` заново в этом раунде — код не изменился с r1/r2.
- **L2 (унаследовано из r2).** README `houseplan-e2e` может по-прежнему буквально описывать `upgrade_from=stable`, хотя с `4143f998` гейт вычисляет `previousStable()`. Файл вне материала этого ревью (другой репозиторий, свой процесс туда не распространяется); поведение самого гейта верно и покрыто тестом. Не проверялось заново построчно в этом раунде (правка README в `houseplan-e2e` не входит в `origin/dev...HEAD`), доверие к выводу r2 обосновано — контракт имени job из того же файла перепроверен напрямую (см. «Как проверялось»), а раздел README — чисто описательный текст, не код, от которого что-то зависит.
- **L3 (унаследовано из r1/r2).** Окно `gh run list --limit 10`: если за время ожидания в `houseplan-e2e` случится ≥10 параллельных чужих `workflow_dispatch` прогонов `e2e.yml`, уже отслеживаемый `tracked` может выпасть из выдачи. Паттерн взят из `validate-gate.mjs` (принятый образец, #510), реалистичность — только ручной параллельный dispatch owner'ом много раз подряд.
- **L4 (новое).** `PROCESS.md:704-706`: «плюс зелёный E2E на реальном Home Assistant на теге (`houseplan-e2e`, запускает и ждёт `release.yml`, #514)» — подлежащее и дополнение перепутаны местами: это `release.yml` запускает и ждёт `houseplan-e2e`/`e2e.yml`, а не наоборот. Чисто формулировка, поведение и код не описывают неверно ничего исполняемого; не блокирует.
**High не найдено.** Medium вне скоупа не найдено — единственный Medium (M1) целиком внутри файлов, которые эта задача и меняет.
## Что проверено и корректно
- `e2eGate` — вся логика green/red/missing/cancelled/error (кроме диагностики M1) прочитана и подтверждена тестами и тремя мутантами.
- `previousStable` — фильтрует draft/prerelease/сам тег, берёт первый оставшийся; порядок `gh release list` (новые первыми) не решоверен заново в этом раунде отдельным `gh api`-вызовом, но код идентичен r1/r2, где порядок был подтверждён напрямую, и с тех пор не менялся — переносится без риска.
- `isOurRun`/`classifyRun` — корректно отличают `journeys`/`first-run` (несут тег под тестом) от `upgrade` (несёт тег предыдущего stable), не путают `vX.Y.Z` с `vX.Y.Z-beta.N` (тест).
- `release.yml` — новый шаг вставлен строго после Full Performance, строго для `!prerelease`, `needs: gate` не даёт `build` выполниться при падении шага.
- `scripts/mutation-gate.mjs` — три мутанта реально снимают заявленную защиту, а не косметику; все три поймало штатным раннером.
- Контракт имени job в `houseplan-e2e/e2e.yml`, от которого зависит вся идентификация «своего» прогона, подтверждён прямым чтением файла в этом раунде, а не унаследован из r1/r2 без проверки.
- Документация (`PROCESS.md`, `AGENTS.md`, `docs/DEVELOPMENT.md`, `docs/TESTING.md`, `docs/specs/README.md`) обновлена консистентно с реализацией и §8 ТЗ (с оговоркой L4).
- Трейлеры всех 7 коммитов (`Issue: #514`, `User-Visible: no`) корректны; `User-Visible: no` верно — изменение видно только владельцу.
- Диапазон изменений строго в рамках скоупа ТЗ (§2); попутных правок «раз уж я здесь» не обнаружено.
- Одно число / один источник — неприменимо: диф не добавляет и не меняет ни одной пользователем видимой величины.
## Чего не проверял и почему
- `tsc --noEmit`, `npm test` (полный), `npm run build` с попиксельной сверкой бандла — зелёный Validate на точном `d166bad3` уже это покрывает (проверено через `gh api`, `head_sha` совпадает).
- `golden:verify`, `check-docs.mjs`, `model-invariants.mjs`, `pytest tests_backend`, браузерные смоки — diff не трогает `src/**`, геометрию/`layout`/толщины или Python.
- Реальный dispatch `release.yml` по событию `release: published` и фактическую работу `HP_PROCESS_TOKEN` на `workflow_dispatch` в `houseplan-e2e` — недоступно ревьюеру (нет доступа к секретам владельца, событие `release` нельзя сэмулировать без публикации настоящего релиза). Первая живая проверка — следующий stable, как честно называет ТЗ; путь отказа сейчас покрыт тестом только для ошибки `dispatch()`, не для ошибки `releases()` — это и есть M1.
- README `houseplan-e2e` построчно в этом раунде — вне диапазона `origin/dev...HEAD`, контракт имени job (единственное, от чего зависит код) перепроверен напрямую отдельно.
- Полный список `demo/smoke_*.mjs` не запрашивал и `smoke-select.mjs` не гонял — diff не содержит исполняемого frontend-кода (`src/**` не тронут), выбирать нечего механически, без инструмента.
## Продуктовое рассуждение
Задача не закрывает напрямую ни одну строку `docs/SCOPE.md` — это инфраструктура процесса релиза, а не поверхность продукта. Она укрепляет цепочку, которой продукт уже поставляется в HACS: `houseplan-e2e` существовал и был зелёным по ночам, но stable-релиз уходил, не дождавшись его результата на самом тестируемом теге. После этой правки — дождётся. Согласуется с решением владельца из тела issue («E2E — гейт стабильного релиза») без расширения скоупа и без ухудшения соседнего поведения (беты не тронуты — подтверждено условием `if` и тестом AC3). M1 не меняет этой оценки: сегодняшнее поведение безопасно, риск — в будущей конфигурации токена, которую сам код и подсказывает завести именно так, что и ломает `releases()`.
## Вопросы владельцу
Нет открытых продуктовых вопросов. M1 — техническая находка ревью, не продуктовый вопрос: решается автором в реализации (два равноценных пути указаны выше), владелец эту развилку решать не должен.
## Закрытие раунда r2
| Находка r2 | Чем закрыта | Где это видно |
|---|---|---|
| L1 (плановая job упадёт раньше первой именованной суб-job → `missing`) | Не закрыта, не требовалась к закрытию — Low принят без правки в r2, поведение не изменилось | `scripts/e2e-gate.mjs:83-97` идентичен коду, прочитанному в r2; переносится как L1 этого документа |
| L2 (README `houseplan-e2e` описывает `upgrade_from=stable` буквально) | Не закрыта, не требовалась — файл вне материала процесса houseplan-card | переносится как L2 этого документа |
| L3 (окно `gh run list --limit 10`) | Не закрыта, не требовалась — принятый паттерн из `validate-gate.mjs` | переносится как L3 этого документа |
r2 не содержал High/Medium — закрывать нечего, кроме подтверждения, что три Low не выросли в Medium на текущем коде (подтверждено чтением заново, код не изменился между `539756cc` и `d166bad3` в файлах `scripts/e2e-gate.mjs`/`scripts/mutation-gate.mjs`).
## Унаследовано из r2
Формально ничего не наследуется без повторной проверки: ребейз на ушедший вперёд `dev` (§7.2) — явно исключённый случай для сокращённого делта-разбора, весь `origin/dev...HEAD` на `d166bad3` прочитан заново в этом раунде. Единственное, что не перепроверялось отдельным вызовом в r3 (но перепроверялось чтением кода, не изменившегося с r2) — порядок `gh release list --repo Matysh/houseplan-card` (новые релизы первыми), на котором стоит `previousStable`: подтверждён прямым вызовом в r1 и r2 (`docs/reviews/CODE-REVIEW-514-r2.md`, SHA `539756cc`), код `previousStable` идентичен байт-в-байт текущему. Содержимое README `houseplan-e2e` (L2) — тоже не перечитывалось построчно в r3, доверие к выводу r2 обосновано тем, что это внешний, неисполняемый для этого репозитория файл вне диапазона `origin/dev...HEAD`.
---
<!-- material-anchors: подготовлено ревьюером вручную для r3, конвейер допишет свои якоря при публикации -->
## Материал раунда
- Ветка: `issue/514-e2e-stable-release-gate`, коммит `d166bad3eb293d70cdd37cc715c639449742d988`.
- Диапазон: `origin/dev..HEAD` (7 коммитов), `origin/dev` = `3c1b4ebee2b60c7cc9ea5d9021046b2da1ba5832`.
- ТЗ: `docs/specs/514-e2e-stable-release-gate.md`; ревью ТЗ: `docs/reviews/SPEC-REVIEW-514-r1.md` (зелёное, r1).
- Предыдущий код-ревью: r2, зелёный, материал `539756cc000cd39c6e9cf8e1fda9ffe0824d46fd` (осиротел после повторного ребейза).
- Validate на материале раунда: https://github.com/Matysh/houseplan-card/actions/runs/34404724698 (success, `head_sha` совпадает).
- Вердикт этого раунда: **жёлтый** · High 0 · Medium 1 (M1, в скоупе) · Low 4 (L1-L3 унаследованы, L4 новая).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/514-e2e-stable-release-gate`, коммит `6d28e170913c` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `260cde45610a89e6a11ac3ad39794ecfffe98351`
```
git log --all --format='%H %T' | grep 260cde45610a
```
- ТЗ `docs/specs/514-e2e-stable-release-gate.md`, блоб `30795778fee11c8df610440dd86b745112e120c4`
```
git log --all --find-object=30795778fee11c8df610440dd86b745112e120c4 -- docs/specs/514-e2e-stable-release-gate.md
```
- Вердикт конвейера: `yellow` · High 0