From 7d29ca388b91739502e19c69f475b01ad2a31d39 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 27 Aug 2026 18:46:53 +0300 Subject: [PATCH 1/6] ci: every workflow and job carries a human-readable Russian name Owner decision (chat, 2026-08-27): the running check's name must say what it does, in Russian. Scripts locate workflows by file name (release-gate.mjs -> validate.yml), so display names are free; job ids and needs are untouched. The same content is cherry-picked to main because release workflows execute from the default branch and process.yml must stay identical in main and dev. Issue: #327 User-Visible: no (cherry picked from commit 1a8b48035507bdfe6451f214cf95306af9b62cfa) --- .github/workflows/announce.yml | 3 ++- .github/workflows/docs-screenshots.yml | 3 ++- .github/workflows/mutation-gate.yml | 3 ++- .github/workflows/performance.yml | 3 ++- .github/workflows/process.yml | 4 +++- .github/workflows/publish-prerelease.yml | 6 +++++- .github/workflows/release-zip.yml | 3 ++- .github/workflows/release.yml | 5 ++++- .github/workflows/validate.yml | 16 +++++++++++++++- 9 files changed, 37 insertions(+), 9 deletions(-) diff --git a/.github/workflows/announce.yml b/.github/workflows/announce.yml index 66c6f39d..08327464 100644 --- a/.github/workflows/announce.yml +++ b/.github/workflows/announce.yml @@ -1,4 +1,4 @@ -name: Announce release +name: Анонс релиза # Telegram notifications for t.me/ha_houseplan (owner request, 2026-08-07). # Stable releases are announced; prereleases are deliberately silent. # workflow_dispatch exists purely as a connectivity test button and therefore @@ -36,6 +36,7 @@ permissions: contents: read jobs: telegram: + name: Оповещение в Telegram (только стабильные) if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && github.event.release.prerelease == false) || (github.event_name == 'workflow_call' && inputs.prerelease == false) }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/docs-screenshots.yml b/.github/workflows/docs-screenshots.yml index 92de4ad8..3a021777 100644 --- a/.github/workflows/docs-screenshots.yml +++ b/.github/workflows/docs-screenshots.yml @@ -9,7 +9,7 @@ # локально через `npm run docs:accept -- --reviewed --from=<распакованный>`. # Та же конструкция, что у golden-эталонов, и по той же причине: картинки # попадают в репозиторий через явное решение, а не через бота. -name: Docs screenshots +name: Скриншоты документации on: workflow_dispatch: @@ -24,6 +24,7 @@ permissions: jobs: capture: + name: Съёмка и сверка скриншот-индекса runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/mutation-gate.yml b/.github/workflows/mutation-gate.yml index 55d77621..d9af24e8 100644 --- a/.github/workflows/mutation-gate.yml +++ b/.github/workflows/mutation-gate.yml @@ -1,4 +1,4 @@ -name: Mutation gate +name: Мутационный гейт # Реестр известных поломок (issue #85): каждый мутант ломает продуктовый код # известным способом, и объявленный тест ОБЯЗАН на этом покраснеть. Тест, @@ -31,6 +31,7 @@ concurrency: jobs: mutants: + name: "Мутанты: каждый обязан красить тесты" runs-on: ubuntu-latest # Все мутанты × (сборка + свой guard) — это десятки минут, и это нормально: # гейт предрелизный. Час — потолок против зависшего Chromium. diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 599648ba..3fd73776 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -1,4 +1,4 @@ -name: Full Performance +name: Полные бенчмарки производительности on: # Every main promotion is a stable-release candidate and must have an @@ -24,6 +24,7 @@ concurrency: jobs: performance: + name: Бенчмарки рендера и геометрии # Every profile keeps base and candidate sequential on one hosted runner. # Independent profile pairs run in parallel: cross-profile timing is never # compared, while serialising all five pairs cannot fit the job timeout. diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 0604ef5a..ec41b543 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -1,4 +1,4 @@ -name: Process +name: Ревью-конвейер # Событийный конвейер процесса (PROCESS.md). Смена статусной метки — это # сообщение: она порождает событие, событие запускает следующий шаг. @@ -35,6 +35,7 @@ permissions: jobs: guard: + name: "Страж: ребейз на dev и предпосылки ревью" runs-on: ubuntu-latest outputs: stage: ${{ steps.decide.outputs.stage }} @@ -162,6 +163,7 @@ jobs: echo "limit=$limit" >> "$GITHUB_OUTPUT" review: + name: "Ревью (Claude): вердикт в issue" needs: guard if: needs.guard.outputs.stage != '' runs-on: ubuntu-latest diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 7fd6be79..19991da3 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -1,4 +1,4 @@ -name: Publish prerelease +name: Публикация пре-релиза (ручная) run-name: Publish ${{ inputs.tag }} on: @@ -19,6 +19,7 @@ concurrency: jobs: gate: + name: "Гейт: зелёная Проверка и релизный контракт" runs-on: ubuntu-latest outputs: sha: ${{ steps.candidate.outputs.sha }} @@ -61,6 +62,7 @@ jobs: run: node scripts/release-gate.mjs "$SHA" publish: + name: Публикация тега и релиза needs: gate runs-on: ubuntu-latest outputs: @@ -198,6 +200,7 @@ jobs: # A manual step after a successful release is the worst kind: by the time it is # due, the work already looks finished, which is exactly why it gets forgotten. close-merged: + name: Закрытие вошедших issue needs: [gate, publish] if: ${{ needs.publish.outputs.newly_published == 'true' }} runs-on: ubuntu-latest @@ -245,6 +248,7 @@ jobs: } announce: + name: Комментарий о публикации needs: [gate, publish] if: ${{ needs.publish.outputs.newly_published == 'true' }} uses: ./.github/workflows/announce.yml diff --git a/.github/workflows/release-zip.yml b/.github/workflows/release-zip.yml index c1547ecd..a2545897 100644 --- a/.github/workflows/release-zip.yml +++ b/.github/workflows/release-zip.yml @@ -1,4 +1,4 @@ -name: Attach HACS zip to release +name: HACS-zip к релизу # hacs.json declares zip_release + filename=houseplan.zip, so every release # (prereleases included) must carry the asset — HACS installs from it and # GitHub's public download counter becomes a free per-version install metric @@ -19,6 +19,7 @@ permissions: contents: write jobs: zip: + name: Собрать houseplan.zip и приложить к релизу runs-on: ubuntu-latest steps: - name: Resolve tag diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee070acc..ba5e34d1 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release +name: "Релиз: ассеты после зелёной проверки" on: release: types: [published] @@ -14,6 +14,7 @@ jobs: # Needs a push with a token that has the `workflow` scope (the ordinary # Personal Access Token used for `git push` refuses workflow file updates). gate: + name: "Гейт: зелёная Проверка точного SHA тега" runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -44,6 +45,7 @@ jobs: SHA=$(git rev-parse HEAD) node scripts/release-gate.mjs "$SHA" --workflow=performance.yml --label="Full Performance" build: + name: Сборка бандла и загрузка ассетов needs: gate runs-on: ubuntu-latest steps: @@ -71,6 +73,7 @@ jobs: with: files: dist/houseplan-card.js hacs-discovery: + name: HACS-видимость пре-релиза (порядок бет) # HACS 2.0.x takes the first prerelease in GitHub's response instead of # sorting SemVer. A valid asset can therefore be invisible to beta users # (beta.10 appeared after beta.9). Keep the release asset, but diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index eef37a4c..515fbc62 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,4 +1,4 @@ -name: Validate +name: Проверка (CI) on: push: @@ -22,6 +22,7 @@ concurrency: jobs: docs: + name: "Документация: гайды, ченджлоги, скриншот-индекс" runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -34,6 +35,7 @@ jobs: # совпадать в `main` и `dev`. До этой проверки совпадение держалось на # дисциплине: каждая правка требовала двух пушей и ручной сверки. process-workflow-sync: + name: "Процесс: process.yml идентичен в main и dev" runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -53,6 +55,7 @@ jobs: fi provenance: + name: "Провенанс коммитов: трейлеры и эталоны" runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -76,6 +79,7 @@ jobs: # где нарушение правила №1 ловится машиной. Job независимый: краснеет сам и # не роняет остальные, откат — удалить его отсюда, скрипт остаётся рабочим. process-gate: + name: "Процессный гейт: диапазон, трейлеры, статусы issue" runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -104,6 +108,7 @@ jobs: # экономят на ветках задач, где Validate — ранний сигнал: настоящую приёмку # там делает код-ревью, которое гоняет гейты само (#127). changes: + name: Классификация изменённых файлов runs-on: ubuntu-latest outputs: frontend: ${{ steps.classify.outputs.frontend }} @@ -158,6 +163,7 @@ jobs: # CARD_VERSION и package.json входят в фингерпринт, поэтому ключи кандидата # заведомо новые и полный набор гейтов перед бетой и релизом идёт всегда. reuse: + name: "Переиспользование: это дерево уже проверено" runs-on: ubuntu-latest outputs: smoke: ${{ steps.probe.outputs.smoke }} @@ -237,6 +243,7 @@ jobs: waive backend "$BACKEND" hacs: + name: "HACS: валидация репозитория" needs: changes if: needs.changes.outputs.integration == 'true' runs-on: ubuntu-latest @@ -248,6 +255,7 @@ jobs: category: integration hassfest: + name: "Hassfest: манифест интеграции" needs: changes if: needs.changes.outputs.integration == 'true' runs-on: ubuntu-latest @@ -257,6 +265,7 @@ jobs: uses: home-assistant/actions/hassfest@master frontend: + name: "Фронтенд: типы, юниты, мутанты, синхрон бандла" needs: changes if: needs.changes.outputs.frontend == 'true' runs-on: ubuntu-latest @@ -278,6 +287,7 @@ jobs: run: cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js smoke: + name: Смоки в браузере (шард ${{ matrix.shard }} из 3) # Gated on `frontend` so a typecheck failure does not burn browser minutes. needs: [frontend, reuse] if: needs.reuse.outputs.smoke != 'true' @@ -350,6 +360,7 @@ jobs: # Маркер переиспользования пишется ОДИН раз и только когда прошли все шарды: # частично прогнанная матрица не имеет права выглядеть как выполненная работа. smoke_done: + name: "Смоки: все шарды зелёные" needs: [smoke, reuse] if: needs.reuse.outputs.smoke != 'true' runs-on: ubuntu-latest @@ -370,6 +381,7 @@ jobs: key: reuse-smoke-${{ needs.reuse.outputs.smoke_key }} golden: + name: Golden-кадры против принятых эталонов # Deterministic visual correctness stays in every prerelease gate: it is # inexpensive and catches a different class of regressions than timings. needs: [frontend, reuse] @@ -432,6 +444,7 @@ jobs: key: reuse-golden-${{ needs.reuse.outputs.golden_key }} performance_smoke: + name: "Перф-смок: бюджет времени кадра" # Candidate-only catastrophic-regression guard for ordinary pushes and # prereleases. The expensive same-runner comparison lives in performance.yml. needs: [frontend, reuse] @@ -494,6 +507,7 @@ jobs: key: reuse-performance_smoke-${{ needs.reuse.outputs.performance_smoke_key }} backend: + name: "Бэкенд: pytest в Home Assistant" needs: [changes, reuse] if: needs.changes.outputs.backend == 'true' && needs.reuse.outputs.backend != 'true' runs-on: ubuntu-latest From 2d1e69b0c167aad52018ba1bed30d2cb427e68d8 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 27 Aug 2026 18:50:02 +0300 Subject: [PATCH 2/6] ci: the performance-workflow contract test follows the renamed labels The test pins the literal workflow name and the release-gate label; both moved to the Russian names of #327. Issue: #327 User-Visible: no (cherry picked from commit 6a3ac52258a321471922d27c4f5ce48a38ab41d3) --- .github/workflows/release.yml | 2 +- test/performance-workflow.test.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba5e34d1..703683cc 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: run: | set -euo pipefail SHA=$(git rev-parse HEAD) - node scripts/release-gate.mjs "$SHA" --workflow=performance.yml --label="Full Performance" + node scripts/release-gate.mjs "$SHA" --workflow=performance.yml --label="Полные бенчмарки производительности" build: name: Сборка бандла и загрузка ассетов needs: gate diff --git a/test/performance-workflow.test.mjs b/test/performance-workflow.test.mjs index 5021cf39..065acd10 100644 --- a/test/performance-workflow.test.mjs +++ b/test/performance-workflow.test.mjs @@ -22,7 +22,7 @@ test('ordinary Validate keeps only the bounded candidate performance smoke', () test('full performance is isolated to stable, scheduled and manual entry points', () => { const workflow = readWorkflow('performance.yml'); for (const contract of [ - 'name: Full Performance', + 'name: Полные бенчмарки производительности', 'branches:', '- main', 'schedule:', @@ -44,5 +44,5 @@ test('full performance is isolated to stable, scheduled and manual entry points' const release = readWorkflow('release.yml'); assert.ok(release.includes('if: ${{ !github.event.release.prerelease }}')); - assert.ok(release.includes('--workflow=performance.yml --label="Full Performance"')); + assert.ok(release.includes('--workflow=performance.yml --label="Полные бенчмарки производительности"')); }); From 55155b0d4f4989f7da368c7dda0d93e37bdb4e20 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 28 Aug 2026 01:34:07 +0300 Subject: [PATCH 3/6] ci: mutation-gate workflow runs four shards with a warm test-build (#332) Mirror of the dev-side change: the scheduled run executes this file from main while checking out the registry from dev, so the shard matrix must live here too. Issue: #332 User-Visible: no --- .github/workflows/mutation-gate.yml | 31 ++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/mutation-gate.yml b/.github/workflows/mutation-gate.yml index d9af24e8..34af413c 100644 --- a/.github/workflows/mutation-gate.yml +++ b/.github/workflows/mutation-gate.yml @@ -5,11 +5,17 @@ name: Мутационный гейт # оставшийся зелёным на сломанном коде, ничего не защищает — он лишь выглядит # защитой, и это хуже его отсутствия. # -# Прогон дорогой: пересборка бандла на каждого мутанта. Поэтому он не входит в -# Validate и не идёт на каждый push. Его место — перед стабильным релизом -# (PROCESS.md §8) и раз в неделю по расписанию, чтобы дрейф тестов не копился -# до релиза. Дешёвая половина — «якоря патчей живы, guard-файлы существуют» — -# идёт с обычными юнитами: test/mutation-gate.test.mjs. +# Прогон дорогой, поэтому он не входит в Validate и не идёт на каждый push. +# Его место — перед стабильным релизом (PROCESS.md §8) и раз в неделю по +# расписанию, чтобы дрейф тестов не копился до релиза. Дешёвая половина — +# «якоря патчей живы, guard-файлы существуют» — идёт с обычными юнитами: +# test/mutation-gate.test.mjs. +# +# #332: бандл собирается только мутантам с браузерным гвардом (guardNeedsBundle), +# компиляция тестов в worktree стартует с тёплого test-build (инкрементальный +# tsc), а реестр режется на четыре чересполосных шарда — полный прогон +# укладывается в десятки минут вместо часов. Локальный дифф-режим: +# node scripts/mutation-gate.mjs --changed origin/dev..HEAD on: workflow_dispatch: @@ -31,10 +37,14 @@ concurrency: jobs: mutants: - name: "Мутанты: каждый обязан красить тесты" + name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 4)" runs-on: ubuntu-latest - # Все мутанты × (сборка + свой guard) — это десятки минут, и это нормально: - # гейт предрелизный. Час — потолок против зависшего Chromium. + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] + # Шард ~64 мутантов × свой guard; бандл собирают только браузерные гварды. + # Час — потолок против зависшего Chromium. timeout-minutes: 60 steps: - uses: actions/checkout@v7 @@ -70,5 +80,8 @@ jobs: - name: Реестр применим к текущему коду run: node scripts/mutation-gate.mjs --check + - name: Тёплый test-build для инкрементальной компиляции мутантов + run: npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs + - name: Каждый тест ловит свою поломку - run: node scripts/mutation-gate.mjs + run: node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/4 From 22dc15c6021e0b4df5ea956e1c75e70e2bf95e9a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 02:15:36 +0300 Subject: [PATCH 4/6] =?UTF-8?q?ci:=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?process.yml=20=D1=81=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Конвейер исполняет версию из ветки по умолчанию, поэтому файл обязан совпадать в main и dev побайтово — это проверяет шаг `process.yml идентичен в main и dev` в Validate. Содержательная правка сделана в dev (#343), здесь только копия. Issue: #343 User-Visible: no --- .github/workflows/process.yml | 41 +++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index ec41b543..818423bc 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -327,6 +327,41 @@ jobs: --add-label S6-in-progress --remove-label S7-code-review echo "S7-code-review -> S6-in-progress (ревью не запускалось)" + # Ревьюер перегонял tsc, юниты и сборку заново в каждом раунде, хотя + # Validate на том же SHA уже зелёный (#343). Это не тщательность: бюджет + # ревью тратится на повторение CI вместо чтения кода. + # + # Доказательство здесь такое же строгое, как у reuse-маркеров (#208): не + # «недавно было зелено», а «completed success ровно на этом SHA». После + # ребейза SHA другой, прогона для него нет — и ревьюер честно гоняет сам. + - name: Зелёные гейты на этом SHA + id: validated + if: steps.rebase.outputs.conflict != 'true' + env: + GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + run: | + sha=$(git rev-parse HEAD) + short=$(git rev-parse --short HEAD) + row=$(gh run list --repo "${{ github.repository }}" --workflow validate.yml \ + --commit "$sha" --limit 5 \ + --json status,conclusion,url \ + --jq '[.[] | select(.status=="completed" and .conclusion=="success")][0] // empty') + { + echo 'note<s+=d).on("end",()=>process.stdout.write(JSON.parse(s).url||""))') + echo "**Дешёвые гейты на этом SHA уже подтверждены** (#343). Validate на \`$short\` завершился success: $url" + echo "" + echo "Значит \`npx tsc --noEmit\`, \`npm test\` и \`npm run build\` со сверкой копий бандла перегонять не нужно — сошлись на этом прогоне, назвав его ссылкой. Бюджет раунда тратится на чтение кода." + echo "" + echo "Что Validate НЕ покрывает и остаётся за тобой: смоки, выбранные по диффу; golden, если diff трогает рендер; инварианты модели на конкретной конфигурации; и любой гейт, который требуют AC задачи." + else + echo "**Зелёного Validate на этом SHA (\`$short\`) нет** — прогон не найден, не завершён либо не success. Дешёвые гейты прогоняешь сам и называешь результат." + fi + echo 'EOF_NOTE' + } >> "$GITHUB_OUTPUT" + if [ -n "$row" ]; then echo "Validate на $short: зелёный"; else echo "Validate на $short: зелёного нет"; fi + # Зависимости ставятся ПОСЛЕ переключения на ветку задачи: lockfile мог # измениться именно в ней, и установка по копии из dev дала бы не то дерево. - name: Установить зависимости @@ -453,8 +488,10 @@ jobs: правке — не тщательность, а потеря времени: полные наборы это предрелизный гейт (PROCESS.md §8), а не гейт ревью. - Всегда, они дешёвые, и в повторном раунде тоже: код изменился, - а стоят они минуты: + ${{ steps.validated.outputs.note }} + + Если зелёного прогона на этом SHA нет — прогоняешь сам, они дешёвые, + и в повторном раунде тоже: код изменился, а стоят они минуты: `npx tsc --noEmit`, `npm test`, `npm run build` со сверкой трёх копий бандла. Плюс `node scripts/check-docs.mjs`, если diff трогает `src/**`: отпечаток скриншотов документации считается по всему From d2f4f941590594750222503966b6861c7cdb2008 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 10:07:07 +0300 Subject: [PATCH 5/6] =?UTF-8?q?ci:=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?process.yml=20=D1=81=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Конвейер исполняет версию из ветки по умолчанию, поэтому файл обязан совпадать в main и dev побайтово — это проверяет шаг предполётных проверок в Validate. Содержательная правка сделана в dev (#364), здесь только копия. Issue: #364 User-Visible: no --- .github/workflows/process.yml | 42 ++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 818423bc..21a1dcf9 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -266,9 +266,18 @@ jobs: before=$(git rev-parse "origin/$BRANCH") echo "dev впереди на $behind коммит(ов) — привожу ветку" if ! git rebase origin/dev; then + # Список снимается ДО abort: он же снимает состояние конфликта, и + # тогда автору достаётся «не ребейзится» без единого имени файла (#364). + files=$(git diff --name-only --diff-filter=U | sort -u | paste -sd'\n' -) git rebase --abort || true - echo "conflict=true" >> "$GITHUB_OUTPUT" + { + echo 'conflict=true' + echo 'conflicts<> "$GITHUB_OUTPUT" echo "::warning::ветка $BRANCH не ребейзится на dev без конфликта — ревью не запускается" + printf 'конфликтуют:\n%s\n' "${files:-(git не назвал файлы)}" exit 0 fi # --force-with-lease с явным ожидаемым значением обязателен: между @@ -308,10 +317,17 @@ jobs: GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} NUM: ${{ github.event.issue.number }} BRANCH: ${{ steps.branch.outputs.name }} + CONFLICTS: ${{ steps.rebase.outputs.conflicts }} run: | cat > /tmp/stale.md <> "$GITHUB_OUTPUT" echo "to=$to" >> "$GITHUB_OUTPUT" + # Ревью идёт десятки минут, а dev за это время двигается (28 августа — + # четыре раза за день). Вердикт при этом вынесен по дереву, которое уже не + # совпадает с вершиной линии, и слияние приведёт ветку к dev — то есть в + # dev уедет код, отличный от прочитанного (§7.2). Молчать об этом нельзя, + # но и шуметь на каждом прогоне ни к чему: строка появляется только когда + # dev действительно ушёл и вердикт зелёный, то есть слияние вот-вот + # случится (#364). + - name: dev ушёл вперёд, пока шло ревью + if: steps.rebase.outputs.conflict != 'true' && needs.guard.outputs.stage == 'code' + env: + GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + NUM: ${{ github.event.issue.number }} + MATERIAL: ${{ steps.material.outputs.sha }} + GREEN: ${{ steps.decide.outputs.green }} + run: | + git fetch -q origin dev + moved=$(git rev-list --count "$MATERIAL..origin/dev") + echo "dev продвинулся на $moved коммит(ов) с момента фиксации материала" + echo "- dev продвинулся на **$moved** коммит(ов) во время ревью" >> "$GITHUB_STEP_SUMMARY" + if [ "$moved" -eq 0 ] || [ "$GREEN" != "true" ]; then exit 0; fi + short=$(git rev-parse --short "$MATERIAL") + gh issue comment "$NUM" --repo "${{ github.repository }}" --body \ + "Пока шло ревью, \`dev\` продвинулся на $moved коммит(ов). Материал ревью — \`$short\`. Вердикт вынесен по дереву, которое уже не совпадает с вершиной линии: слияние приведёт ветку к dev, и это другой код (§7.2)." + # S8-merged утверждает, что код в dev. Значит слияние обязано произойти # ДО метки, иначе она врёт в промежутке. # From 88dc61dd40a5a728f103b3e56c606399b601090a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 10:38:57 +0300 Subject: [PATCH 6/6] =?UTF-8?q?ci:=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?process.yml=20=D1=81=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Конвейер исполняет версию из ветки по умолчанию, поэтому файл обязан совпадать в main и dev побайтово. Содержательная правка сделана в dev (#365), здесь копия. Issue: #365 User-Visible: no --- .github/workflows/process.yml | 51 ++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 21a1dcf9..cf99cfb2 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -642,14 +642,31 @@ jobs: marker=CODE-REVIEW if [ "$STAGE" = "spec" ]; then marker=SPEC-REVIEW; fi doc="docs/reviews/${marker}-${NUM}-r${CYCLE}.md" - # Рабочая копия отбрасывается ДО того, как документ попадёт в дерево: - # ревьюер правит код, проверяя «умеет ли тест падать», и его правки - # публиковаться не должны. - git checkout -- . 2>/dev/null || true - # docs/reviews исключён из уборки: ревьюер мог написать документ по - # старому пути, и клин не должен его съесть до `git add` — ровно так - # оба пути остаются работоспособными. - git clean -fd -e docs/reviews -e node_modules >/dev/null 2>&1 || true + # Документ спасается ПЕРВЫМ делом. Ревьюер мог написать его по старому + # пути прямо в рабочую копию, а дальше эта копия будет отброшена + # целиком — и вместе с ней пропал бы артефакт (#220). + if [ ! -f "$SOURCE" ] && [ -f "$doc" ]; then + cp "$doc" "$SOURCE" + echo "документ найден в рабочей копии и сохранён в $SOURCE" + fi + # Reset, а не checkout+clean, и вот почему (#365). + # + # 28.08 коммит bb2919f уехал в dev с тридцатью файлами вместо одного + # markdown: откатил отревьюженную реализацию #359, вернул старые чанки + # и держал dev откаченным три часа. Механизм воспроизведён: + # `git checkout -- .` восстанавливает рабочее дерево ИЗ ИНДЕКСА, а + # `git clean -fd` убирает неотслеживаемое — ни то, ни другое индекс не + # трогает. Ревьюер работает с Bash и в ходе проверки «умеет ли тест + # падать» вполне может сделать `git add`; всё, что осталось у него в + # индексе, прежняя уборка сохраняла, и следующий же `git commit` + # забирал это вместе с документом. Сообщение при этом невинное, и от + # рутины инцидент отличается только диффом. + # + # `reset --hard` снимает и индекс, и дерево разом. Терять нечего: + # документ приезжает извне репозитория, из RUNNER_TEMP. + git fetch -q origin "$target" + git reset -q --hard "origin/$target" + git clean -fdq -e node_modules >/dev/null 2>&1 || true # Документ приезжает извне репозитория (#220). Три раунда подряд он # терялся, пока лежал некоммитнутым файлом в том же дереве, которое # ревьюер мутирует и затем восстанавливает: `git checkout -- .` плюс @@ -661,11 +678,12 @@ jobs: cp "$SOURCE" "$doc" echo "документ взят из $SOURCE ($(wc -c < "$doc") байт)" else - # Совместимость: ревьюер мог написать по старому пути, если промпт - # ещё не обновился в этой ветке. - echo "::warning::$SOURCE не найден — ищу документ в рабочей копии" + echo "::warning::$SOURCE не найден — документа для публикации нет" fi - git add docs/reviews 2>/dev/null || true + # Индексируется ровно один путь, а не каталог: `git add docs/reviews` + # забрал бы всё, что там окажется, а после reset там не должно быть + # ничего постороннего — но полагаться на «не должно» здесь нельзя. + git add -- "$doc" 2>/dev/null || true if git diff --cached --quiet; then # Пустая рабочая копия — ещё не провал: ревьюер иногда коммитит # документ сам, своим app-токеном мимо этого шага (CODE-REVIEW-150-r1, @@ -683,6 +701,8 @@ jobs: echo "::error::вердикт есть, а документа нет: ни $SOURCE, ни $doc в рабочей копии, ни $doc в $target — ревью без артефакта (#171, #220)" exit 1 fi + # Первый рубеж: что вообще проиндексировано. + git diff --cached --name-only | node scripts/review-doc-guard.mjs git -c user.name="claude[bot]" \ -c user.email="209825114+claude[bot]@users.noreply.github.com" \ commit -q -F - <