mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Validate / provenance (push) Successful in 42s
Validate / process-gate (push) Failing after 36s
Validate / changes (push) Successful in 44s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / frontend (push) Skipped
Validate / smoke (push) Skipped
Validate / golden (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped
Full Performance / performance (push) Failing after 2h16m25s
Filing and servicing a separate issue costs far more than fixing a small problem in place — the owner's call of 2026-08-19 (#202). A Medium finding inside the task's scope no longer becomes its own issue: with no High findings the verdict is yellow, the author fixes it and the fix passes another review cycle. Only an out-of-scope Medium is still filed separately, because foreign scope is never patched from a task branch. Applied to the canon (PROCESS.md), the reviewer prompt in process.yml and AGENTS.md; the verdict format now writes "Medium: N -> in-task | #NN". Issue: #202 User-Visible: no
521 lines
36 KiB
YAML
521 lines
36 KiB
YAML
name: Process
|
||
|
||
# Событийный конвейер процесса (PROCESS.md). Смена статусной метки — это
|
||
# сообщение: она порождает событие, событие запускает следующий шаг.
|
||
#
|
||
# S4-spec-review -> ревью ТЗ -> S5-ready | S3-spec
|
||
# S7-code-review -> код-ревью -> слияние в dev -> S8-merged | S6-in-progress
|
||
#
|
||
# Три вещи, без которых конвейер молча не работает:
|
||
#
|
||
# 1. Метки переставляются токеном HP_PROCESS_TOKEN, а не GITHUB_TOKEN. GitHub
|
||
# намеренно не запускает workflow от событий, вызванных GITHUB_TOKEN, чтобы
|
||
# не было циклов — цепочка оборвалась бы после первого шага.
|
||
# 2. Этот файл обязан лежать в ветке по умолчанию (main). Для события `issues`
|
||
# GitHub берёт workflow только оттуда, независимо от того, что в dev.
|
||
# 3. Многострочный текст внутри `run:` — только через heredoc. Строка с нулевым
|
||
# отступом обрывает блок YAML, и скрипт обрезается без ошибки парсера.
|
||
# Проверять не только YAML, но и каждый `run` через `bash -n`.
|
||
|
||
on:
|
||
issues:
|
||
types: [labeled]
|
||
|
||
concurrency:
|
||
# Два события по одному issue не должны запускать два прогона.
|
||
group: process-issue-${{ github.event.issue.number }}
|
||
cancel-in-progress: false
|
||
|
||
permissions:
|
||
contents: read
|
||
issues: write
|
||
# Обязательно: claude-code-action получает OIDC-токен для авторизации
|
||
# GitHub App. Без этого прогон падает с «Could not fetch an OIDC token».
|
||
id-token: write
|
||
|
||
jobs:
|
||
guard:
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
stage: ${{ steps.decide.outputs.stage }}
|
||
cycle: ${{ steps.decide.outputs.cycle }}
|
||
limit: ${{ steps.decide.outputs.limit }}
|
||
steps:
|
||
- id: decide
|
||
env:
|
||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
LABEL: ${{ github.event.label.name }}
|
||
BLOCKED: ${{ contains(github.event.issue.labels.*.name, 'blocked') }}
|
||
EXHAUSTED: ${{ contains(github.event.issue.labels.*.name, 'review-4') }}
|
||
SMALL: ${{ contains(github.event.issue.labels.*.name, 'small') }}
|
||
TRIVIAL: ${{ contains(github.event.issue.labels.*.name, 'trivial') }}
|
||
NUM: ${{ github.event.issue.number }}
|
||
run: |
|
||
# Этап определяется первым: от него зависит, какие вердикты считать.
|
||
stage=""; marker=""
|
||
case "$LABEL" in
|
||
S4-spec-review) stage="spec"; marker="SPEC-REVIEW" ;;
|
||
S7-code-review) stage="code"; marker="CODE-REVIEW" ;;
|
||
*) echo "метка $LABEL конвейер не запускает" ;;
|
||
esac
|
||
|
||
# Лимит циклов: 4 обычный, 2 на лёгком и коротком треке (PROCESS.md §4).
|
||
limit=4
|
||
if [ "$SMALL" = "true" ] || [ "$TRIVIAL" = "true" ]; then limit=2; fi
|
||
|
||
# Счётчик считает вердикты ТОЛЬКО своего этапа. Раньше он брал все
|
||
# подряд, и вердикт по ТЗ съедал цикл из бюджета код-ревью: на #89
|
||
# первое код-ревью получило r2/4. На задаче с двумя циклами ТЗ второе
|
||
# код-ревью упиралось бы в review-4 после одной правки.
|
||
#
|
||
# Этап опознаётся по имени документа в теле комментария. Если документа
|
||
# нет, вердикт не посчитается — недосчёт даёт лишний цикл, а перерасчёт
|
||
# остановил бы работу досрочно; из двух ошибок выбрана обратимая.
|
||
done_cycles=0
|
||
if [ -n "$stage" ]; then
|
||
done_cycles=$(gh issue view "$NUM" --repo "${{ github.repository }}" \
|
||
--json comments \
|
||
-q "[.comments[] | select(.body | test(\"Вердикт:\")) | select(.body | test(\"$marker\"))] | length")
|
||
fi
|
||
|
||
# Отказ обязан быть виден в issue, а не только в логе прогона.
|
||
# Ревьюшная метка обещает работу; если конвейер её не начал и промолчал,
|
||
# задача стоит в этом статусе бесконечно и никто об этом не узнаёт.
|
||
# Так и вышло на #123: чужой issue довели до S4-spec-review, guard
|
||
# отказался за 9 секунд, и в issue не было ни слова.
|
||
#
|
||
# Пишем только когда пытались запустить ревью, то есть stage опознан.
|
||
# Иначе комментарий уходил бы на каждую смену любой метки.
|
||
refuse() {
|
||
echo "$1"
|
||
gh issue comment "$NUM" --repo "${{ github.repository }}" --body \
|
||
"Конвейер ревью не запущен: $2
|
||
|
||
Метка \`$LABEL\` обещает работу, которая не начнётся, поэтому статус лучше вернуть в предыдущий — иначе задача простоит здесь бесконечно. [Прогон](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})."
|
||
stage=""
|
||
}
|
||
|
||
# Автор issue здесь не проверяется (решение владельца 2026-08-13).
|
||
# Проверка стоит на входе в процесс, а не на каждом шаге: как только
|
||
# задача получила статусную метку, она в работе, и кто её завёл — не
|
||
# имеет значения. Само присвоение метки и есть явное подтверждение
|
||
# владельца, причём проверенное платформой: метки может ставить только
|
||
# тот, у кого есть право записи в репозиторий. Прежняя проверка здесь
|
||
# дублировала эту гарантию и заставляла переоформлять чужие отчёты
|
||
# своими issue — чистая работа впустую, как на #123.
|
||
if [ -z "$stage" ]; then
|
||
:
|
||
elif [ "$BLOCKED" = "true" ]; then
|
||
refuse "стоит blocked — конвейер не запускается" \
|
||
"на issue стоит \`blocked\` — задача ждёт внешнего решения. Снять метку, когда решение принято."
|
||
elif [ "$EXHAUSTED" = "true" ]; then
|
||
refuse "стоит review-4 — решение за владельцем" \
|
||
"на issue стоит \`review-4\`: лимит циклов ревью исчерпан, дальше решает владелец — разделить задачу, отклонить или арбитраж (PROCESS.md §4)."
|
||
elif [ "$done_cycles" -ge "$limit" ]; then
|
||
echo "циклов этапа $stage пройдено $done_cycles из $limit — лимит исчерпан"
|
||
gh issue edit "$NUM" --repo "${{ github.repository }}" --add-label review-4
|
||
gh issue comment "$NUM" --repo "${{ github.repository }}" --body \
|
||
"Лимит циклов ревью исчерпан ($done_cycles из $limit на этапе \`$stage\`). Пятого захода нет: решение владельца — разделить задачу, отклонить или арбитраж (PROCESS.md §4)."
|
||
stage=""
|
||
else
|
||
echo "этап $stage, цикл $((done_cycles + 1)) из $limit"
|
||
fi
|
||
echo "stage=$stage" >> "$GITHUB_OUTPUT"
|
||
echo "cycle=$((done_cycles + 1))" >> "$GITHUB_OUTPUT"
|
||
echo "limit=$limit" >> "$GITHUB_OUTPUT"
|
||
|
||
review:
|
||
needs: guard
|
||
if: needs.guard.outputs.stage != ''
|
||
runs-on: ubuntu-latest
|
||
# Время — единственный настоящий ограничитель зациклившегося прогона.
|
||
timeout-minutes: 45
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
ref: dev
|
||
# Иначе в конфиге git остаётся креденшел GITHUB_TOKEN, и push с
|
||
# мёртвым PAT молча уходит от github-actions[bot] — 403 при
|
||
# contents: read. Отказ обязан быть громким и правильным.
|
||
persist-credentials: false
|
||
|
||
# Живость PAT проверяется ДО ревью. На #150 истёкший токен обнаружился
|
||
# только на публикации документа — после сорока минут работы ревьюера.
|
||
- name: Секрет HP_PROCESS_TOKEN жив
|
||
env:
|
||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
run: |
|
||
if [ -z "$GH_TOKEN" ]; then
|
||
echo "::error::HP_PROCESS_TOKEN пуст — секрет удалён или недоступен"
|
||
exit 1
|
||
fi
|
||
if ! login=$(gh api user -q .login 2>/dev/null); then
|
||
echo "::error::HP_PROCESS_TOKEN не аутентифицируется — истёк или отозван. Обновить: Settings -> Secrets and variables -> Actions -> HP_PROCESS_TOKEN"
|
||
exit 1
|
||
fi
|
||
echo "токен жив, действует от: $login"
|
||
|
||
# Окружение готовит workflow, а не модель своими ходами. Раньше промпт
|
||
# велел ревьюеру самому выполнить `npm ci`: минуты уходили на установку без
|
||
# кэша, платились из бюджета 45 минут и из лимитов подписки, а ходы модели
|
||
# тратились на работу инфраструктуры. В validate.yml кэш стоит на всех
|
||
# тяжёлых job, здесь его не было.
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: 22
|
||
cache: npm
|
||
|
||
# Материал ревью живёт в ветке задачи: ТЗ в docs/specs/ и код коммитятся
|
||
# в issue/<NN>-slug. Если ветка запушена — переключаемся на неё, иначе
|
||
# ревьюер прочтёт dev и не найдёт того, что должен оценивать.
|
||
- name: Перейти на ветку задачи
|
||
id: branch
|
||
env:
|
||
NUM: ${{ github.event.issue.number }}
|
||
run: |
|
||
# Свежая по последнему коммиту, а не первая по алфавиту: на #150 рядом
|
||
# жили ветка ТЗ и ветка реализации, и head -1 выбрал устаревшую.
|
||
git fetch -q origin "+refs/heads/issue/${NUM}-*:refs/remotes/origin/issue/${NUM}-*" || true
|
||
branches=$(git for-each-ref --sort=-committerdate \
|
||
--format='%(refname:lstrip=3)' "refs/remotes/origin/issue/${NUM}-*")
|
||
branch=$(printf '%s\n' "$branches" | head -1)
|
||
if [ "$(printf '%s\n' "$branches" | grep -c .)" -gt 1 ]; then
|
||
echo "::warning::веток issue/${NUM}-* несколько ($(echo $branches | tr '\n' ' ')) — выбрана свежая по коммиту: $branch. Устаревшую следует удалить."
|
||
fi
|
||
if [ -n "$branch" ]; then
|
||
git checkout -q "origin/$branch"
|
||
echo "материал ревью: ветка $branch, $(git rev-parse --short HEAD)"
|
||
echo "name=$branch" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "::warning::ветка issue/${NUM}-* не найдена на origin — ревью пойдёт по dev"
|
||
echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY"
|
||
fi
|
||
|
||
# Зависимости ставятся ПОСЛЕ переключения на ветку задачи: lockfile мог
|
||
# измениться именно в ней, и установка по копии из dev дала бы не то дерево.
|
||
- name: Установить зависимости
|
||
run: npm ci
|
||
|
||
# Браузер нужен не всякому ревью (см. правило выбора гейтов в промпте),
|
||
# но когда нужен — качать его заново дороже, чем держать в кэше.
|
||
- name: Кэш браузеров Playwright
|
||
id: pw
|
||
uses: actions/cache@v6
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
|
||
- name: Установить Chromium
|
||
if: steps.pw.outputs.cache-hit != 'true'
|
||
# Без --with-deps: системные библиотеки Chromium предустановлены в
|
||
# образе ubuntu-latest, а apt при промахе кэша съедал минуты из бюджета
|
||
# ревью и подолгу перебирал недоступное azure-зеркало (#175). Если
|
||
# библиотека когда-нибудь пропадёт из образа, Chromium не запустится с
|
||
# внятной ошибкой — тогда флаг вернуть.
|
||
run: npx playwright install chromium
|
||
|
||
- name: Review
|
||
id: review
|
||
uses: anthropics/claude-code-action@v1
|
||
with:
|
||
# Подписка, а не отдельный счёт API: токен выпускается через
|
||
# `claude setup-token` (Pro/Max). Действуют лимиты подписки.
|
||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||
prompt: |
|
||
Ты ревьюер проекта House Plan. Язык ответа — русский.
|
||
|
||
Issue: #${{ github.event.issue.number }}
|
||
Репозиторий: ${{ github.repository }}
|
||
Этап: ${{ needs.guard.outputs.stage }}
|
||
spec — ревью ТЗ (PROCESS.md §2.4)
|
||
code — код-ревью (PROCESS.md §2.7)
|
||
|
||
Прочитай в этом порядке, прежде чем судить:
|
||
1. docs/SCOPE.md — зачем продукт существует и для кого. Он
|
||
ограничитель: «features are built, improved and accepted only
|
||
if they serve a job listed here». Первый вопрос к задаче —
|
||
какую строку Core user jobs она закрывает.
|
||
2. AGENTS.md и PROCESS.md — процесс, классы изменений, трейлеры,
|
||
лимит циклов, формат вердикта.
|
||
3. Тело issue #${{ github.event.issue.number }} и все комментарии.
|
||
4. Если меняется видимое поведение — docs/USER-GUIDE.ru.md:
|
||
терминология интерфейса берётся оттуда, а не изобретается.
|
||
5. Канонический документ затронутой подсистемы: docs/SUN.md,
|
||
LIGHT.md, CANVAS.md, WALL-THICKNESS.md, UX-MODES.md,
|
||
CONFIG-COMPATIBILITY.md, TOUCH-SUPPORT.md.
|
||
|
||
Для этапа spec: если issue помечен small, ТЗ живёт в теле issue и
|
||
файла в docs/specs/ быть не должно. Иначе ТЗ — docs/specs/<NN>-*.md.
|
||
Проверь обязательные разделы §7.1, однозначность каждого AC и
|
||
указание способа доказательства. Отдельно проверь, что автор не
|
||
выдал догадку за решение: утверждение о поведении, которого нет ни
|
||
в одном документе и которое не помечено как предположение, —
|
||
замечание. Не бывает сложной задачи без единого открытого вопроса.
|
||
|
||
Владельцу задаются только продуктовые вопросы: что человек видит или
|
||
делает и каков объём видимых изменений в этом issue. Технический
|
||
вопрос, вынесенный владельцу, — тоже замечание: ты его снимаешь и
|
||
решаешь по существу в своём вердикте.
|
||
|
||
Для этапа code: материал — диапазон `git log --oneline origin/dev..HEAD`
|
||
и `git diff origin/dev...HEAD`. Ручного тестирования в цикле нет,
|
||
поэтому именно ты отвечаешь на вопрос «оно вообще работает».
|
||
По каждому AC: либо он доказан автотестом и ты убедился, что тест
|
||
умеет падать, либо разобран по коду с явной записью «проверено
|
||
чтением, не исполнением». «Verified» без названной команды и её
|
||
результата доказательством не является. Зависимости уже установлены
|
||
workflow, Chromium тоже — `npm ci` выполнять не нужно. Проверь
|
||
трейлеры Issue и User-Visible, при User-Visible: yes — правки в оба
|
||
changelog в том же коммите.
|
||
|
||
**Объём гейтов соразмерен задаче.** Прогонять весь набор на каждой
|
||
правке — не тщательность, а потеря времени: полные наборы это
|
||
предрелизный гейт (PROCESS.md §8), а не гейт ревью.
|
||
|
||
Всегда, они дешёвые:
|
||
`npx tsc --noEmit`, `npm test`, `npm run build` со сверкой трёх
|
||
копий бандла.
|
||
|
||
По необходимости, и «необходимость» определяется diff'ом и AC:
|
||
- браузерные смоки `demo/smoke_*.mjs` — названные в AC плюс
|
||
относящиеся к тронутым поверхностям. Их 127; прогон всех уместен
|
||
только когда задача действительно задевает всё;
|
||
- `npm run golden:verify` — если diff может изменить видимый
|
||
результат: рендер, геометрия, стили, слои;
|
||
- `python -m pytest tests_backend -q` — если тронут
|
||
`custom_components/**/*.py`;
|
||
- performance-профили — если названы в AC либо тронуты
|
||
чувствительные к перфу пути.
|
||
|
||
Дисциплина «тест должен уметь падать» не отменяется, но применяется к
|
||
тем тестам, которые ты прогонял.
|
||
|
||
**В комментарии обязателен перечень: какие гейты прогнал, какие нет и
|
||
почему.** Это условие честности такого сужения: непрогнанный гейт
|
||
становится видимым решением, а не молчаливым пропуском. Раздел «чего
|
||
не проверял» в документе ревью — не формальность, а главный его
|
||
раздел на коротких задачах.
|
||
|
||
Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь.
|
||
|
||
Серьёзность: High блокирует; Medium В СКОУПЕ задачи чинится в ней
|
||
же — без High это жёлтый вердикт и возврат автору, отдельный issue
|
||
НЕ заводится (решение владельца 2026-08-19, #202: заведение и
|
||
обслуживание issue дороже правки на месте); Low либо правится,
|
||
либо снимается с записью. Жёлтый вердикт допустим и при полностью
|
||
выполненных AC, если изменение не решает заявленный сценарий или
|
||
ухудшает смежный. Продуктовое рассуждение расширяет вопросы, но не
|
||
отменяет AC и не даёт права менять скоуп.
|
||
|
||
Только Medium-находку ВНЕ скоупа задачи (попутный дефект соседнего
|
||
поведения, который в этой ветке чинить нельзя) заведи отдельным
|
||
issue со ссылкой на #${{ github.event.issue.number }} и метками:
|
||
тип, приоритет, S1-new. «Оставили в тексте ревью» закрытием не
|
||
считается и прямо запрещено §12.
|
||
|
||
Напиши полный документ ревью в файл
|
||
docs/reviews/<SPEC|CODE>-REVIEW-${{ github.event.issue.number }}-r${{ needs.guard.outputs.cycle }}.md
|
||
(SPEC для этапа spec, CODE для code): скоуп, как проверялось,
|
||
находки с воспроизведением, что проверено и корректно, чего не
|
||
проверял. Каталог docs/reviews/ создай, если его нет. Больше не
|
||
пиши ничего: любой файл вне docs/reviews/ опубликован не будет.
|
||
|
||
Затем оставь в issue краткий комментарий: вердикт, ключевые находки
|
||
и ссылка на документ. Первой строкой — вердикт в формате §7.2:
|
||
`Вердикт: зелёный/жёлтый/красный · цикл r${{ needs.guard.outputs.cycle }}/${{ needs.guard.outputs.limit }} · High: N · Medium: N → в задаче | #…`
|
||
(«→ #…» — только у Medium вне скоупа; находки в скоупе возвращаются автору жёлтым)
|
||
|
||
Затем верни JSON по схеме. Это последнее действие и оно обязательно:
|
||
без него метка не переставится и конвейер встанет.
|
||
claude_args: |
|
||
--max-turns 150
|
||
--allowedTools Read,Write,Grep,Glob,Bash,mcp__github__add_issue_comment,mcp__github__issue_write,mcp__github__issue_read
|
||
--json-schema '{"type":"object","properties":{"verdict":{"type":"string","enum":["green","yellow","red"]},"high":{"type":"integer"},"medium":{"type":"integer"},"summary":{"type":"string"}},"required":["verdict","high","medium","summary"]}'
|
||
|
||
# Ревьюер пишет только в docs/reviews/. Что именно попадёт в коммит,
|
||
# решает этот шаг, а не модель: всё остальное откатывается.
|
||
- name: Опубликовать документ ревью
|
||
env:
|
||
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
BRANCH: ${{ steps.branch.outputs.name }}
|
||
NUM: ${{ github.event.issue.number }}
|
||
STAGE: ${{ needs.guard.outputs.stage }}
|
||
CYCLE: ${{ needs.guard.outputs.cycle }}
|
||
run: |
|
||
# Ветки задачи может не быть: у задач, размеченных до появления
|
||
# конвейера, ТЗ лежит прямо в dev. Раньше шаг в этом случае молча
|
||
# выходил с нулём, и разбор ревью терялся — оставался только вердикт
|
||
# комментарием. Это тот же тихий отказ: шаг сообщал об успехе тем, что
|
||
# ничего не сделал. Документ ложится туда же, где лежит само ТЗ.
|
||
target="${BRANCH:-dev}"
|
||
if [ -z "$BRANCH" ]; then
|
||
echo "::warning::ветки задачи нет — документ ревью ляжет в dev"
|
||
fi
|
||
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
|
||
git clean -fd -e docs/reviews -e node_modules >/dev/null 2>&1 || true
|
||
git add docs/reviews 2>/dev/null || true
|
||
if git diff --cached --quiet; then
|
||
# Пустая рабочая копия — ещё не провал: ревьюер иногда коммитит
|
||
# документ сам, своим app-токеном мимо этого шага (CODE-REVIEW-150-r1,
|
||
# коммиттер GitHub). Провал — когда файла нет и на ветке.
|
||
git fetch -q origin "$target"
|
||
if git cat-file -e "origin/$target:$doc" 2>/dev/null; then
|
||
echo "документ уже опубликован ревьюером: $doc"
|
||
exit 0
|
||
fi
|
||
# Ревью без артефакта запрещено (PROCESS.md §2.4/§10.4/§12). Раньше
|
||
# здесь стоял warning с exit 0: на #150 оба вердикта ревью ТЗ
|
||
# остались только комментариями, метки переставились, и пропажу
|
||
# заметило лишь следующее ревью — issue #171. Падение ДО шага с
|
||
# меткой сохраняет инвариант «метка не сменилась = прогон упал».
|
||
echo "::error::вердикт есть, а документа $doc нет ни в рабочей копии, ни в $target — ревью без артефакта (#171)"
|
||
exit 1
|
||
fi
|
||
git -c user.name="claude[bot]" \
|
||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||
commit -q -F - <<EOF
|
||
docs: review document for #$NUM
|
||
|
||
Issue: #$NUM
|
||
User-Visible: no
|
||
EOF
|
||
# Публикация в dev идёт из детачнутого состояния поверх ветки задачи
|
||
# либо dev, поэтому push нужен с явным перебазированием при гонке:
|
||
# dev мог уйти вперёд, пока шло ревью — оно длится до 45 минут.
|
||
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
|
||
"HEAD:$target"; then
|
||
git fetch -q origin "$target"
|
||
if ! git -c user.name="claude[bot]" \
|
||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||
rebase "origin/$target"; then
|
||
git rebase --abort || true
|
||
# Тоже вердикт без артефакта: раньше exit 0 переставил бы метку.
|
||
echo "::error::документ ревью не удалось опубликовать в $target: конфликт (#171)"
|
||
exit 1
|
||
fi
|
||
git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
|
||
"HEAD:$target"
|
||
fi
|
||
# Постусловие: до ветки дошёл именно ожидаемый файл. Коммит с
|
||
# документом, названным не по формату, — тот же вердикт без
|
||
# артефакта, только дороже в обнаружении.
|
||
git fetch -q origin "$target"
|
||
if ! git cat-file -e "origin/$target:$doc" 2>/dev/null; then
|
||
echo "::error::коммит в $target опубликован, но ожидаемого $doc в нём нет — файл назван не по формату (#171)"
|
||
exit 1
|
||
fi
|
||
echo "документ опубликован в $target: $doc"
|
||
|
||
- name: Решение по вердикту
|
||
id: decide
|
||
env:
|
||
OUT: ${{ steps.review.outputs.structured_output }}
|
||
STAGE: ${{ needs.guard.outputs.stage }}
|
||
run: |
|
||
verdict=$(echo "$OUT" | jq -r '.verdict')
|
||
high=$(echo "$OUT" | jq -r '.high')
|
||
echo "вердикт: $verdict, High: $high"
|
||
|
||
# Вперёд двигает ТОЛЬКО зелёный. Жёлтый и красный возвращают
|
||
# автору: на прогоне #111 жёлтый означал, что AC описывает неверное
|
||
# изменение контракта — реализовать такое ТЗ значит сделать ошибку
|
||
# по инструкции. Оба считаются циклом.
|
||
if [ "$verdict" = "green" ] && [ "$high" -eq 0 ]; then
|
||
green=true
|
||
case "$STAGE" in
|
||
spec) from=S4-spec-review; to=S5-ready ;;
|
||
code) from=S7-code-review; to=S8-merged ;;
|
||
esac
|
||
else
|
||
green=false
|
||
case "$STAGE" in
|
||
spec) from=S4-spec-review; to=S3-spec ;;
|
||
code) from=S7-code-review; to=S6-in-progress ;;
|
||
esac
|
||
fi
|
||
echo "green=$green" >> "$GITHUB_OUTPUT"
|
||
echo "from=$from" >> "$GITHUB_OUTPUT"
|
||
echo "to=$to" >> "$GITHUB_OUTPUT"
|
||
|
||
# S8-merged утверждает, что код в dev. Значит слияние обязано произойти
|
||
# ДО метки, иначе она врёт в промежутке.
|
||
#
|
||
# При конфликте шаг НЕ падает и метку не оставляет на месте. Первая
|
||
# редакция делала именно так, и это оказалось тупиком: автор ждёт смену
|
||
# метки, метка не менялась, и он тридцать раз опрашивал впустую, чтобы
|
||
# затем отчитаться «лимит исчерпан» — при зелёном вердикте. Инвариант
|
||
# теперь жёстче: ПОСЛЕ ПРОГОНА РЕВЬЮ МЕТКА МЕНЯЕТСЯ ВСЕГДА.
|
||
- name: Слить ветку в dev
|
||
id: merge
|
||
if: needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true'
|
||
env:
|
||
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
BRANCH: ${{ steps.branch.outputs.name }}
|
||
NUM: ${{ github.event.issue.number }}
|
||
run: |
|
||
if [ -z "$BRANCH" ]; then
|
||
echo "::error::ветки задачи нет — сливать нечего"
|
||
echo "merged=false" >> "$GITHUB_OUTPUT"
|
||
exit 0
|
||
fi
|
||
git fetch -q origin dev
|
||
git checkout -q -B merge-into-dev "origin/$BRANCH"
|
||
if ! git -c user.name="claude[bot]" \
|
||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||
rebase origin/dev; then
|
||
git rebase --abort || true
|
||
echo "merged=false" >> "$GITHUB_OUTPUT"
|
||
echo "::warning::ветка $BRANCH не сливается в dev без конфликта"
|
||
cat > /tmp/conflict.md <<EOF
|
||
**Код-ревью зелёное — вердикт выше в силе, переделывать работу не нужно.** Не удалось только слияние: ветка \`$BRANCH\` конфликтует с \`dev\`.
|
||
|
||
Задача переведена в \`S6-in-progress\`, потому что работа вернулась к автору. Осталась не правка кода, а ребейз:
|
||
|
||
1. \`git fetch origin\`, затем \`git rebase origin/dev\` в ветке задачи, разрешить конфликт;
|
||
2. запушить ветку;
|
||
3. вернуть метку \`S7-code-review\`.
|
||
|
||
Повторный прогон ревью — не формальность: после ребейза на новый \`dev\` это другой код, и принимать его без проверки нельзя. Цикл считается по этапу, лимит на код-ревью тратится отдельно от ревью ТЗ.
|
||
EOF
|
||
gh issue comment "$NUM" --repo "${{ github.repository }}" --body-file /tmp/conflict.md
|
||
exit 0
|
||
fi
|
||
git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev
|
||
echo "merged=true" >> "$GITHUB_OUTPUT"
|
||
echo "слито в dev: $(git rev-parse --short HEAD)"
|
||
|
||
- name: Переставить метку
|
||
env:
|
||
# Именно PAT: с GITHUB_TOKEN следующий шаг конвейера не запустится.
|
||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
NUM: ${{ github.event.issue.number }}
|
||
FROM: ${{ steps.decide.outputs.from }}
|
||
# Зелёное код-ревью без слияния ведёт не в S8-merged, а обратно к
|
||
# автору: метка утверждала бы, что код в dev, а его там нет.
|
||
TO: ${{ (needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' && steps.merge.outputs.merged != 'true') && 'S6-in-progress' || steps.decide.outputs.to }}
|
||
run: |
|
||
gh issue edit "$NUM" --repo "${{ github.repository }}" \
|
||
--add-label "$TO" --remove-label "$FROM"
|
||
echo "$FROM -> $TO"
|
||
|
||
- name: Позвать владельца, если ревью упало
|
||
if: failure()
|
||
env:
|
||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
run: |
|
||
# Тело через heredoc, а не многострочный --body: строка с нулевым
|
||
# отступом обрывает блок YAML и оставляет незакрытую кавычку.
|
||
cat > /tmp/failure.md <<EOF
|
||
Автоматическое ревью не отработало: [прогон]($RUN_URL). Статусная метка не менялась, задача осталась на месте.
|
||
|
||
Если вердикт выше всё же опубликован — сбой произошёл после него. Перестановку метки в этом случае выполняет чат обслуживания или владелец, но не автор задачи: автор не толкует вердикт о своей же работе.
|
||
EOF
|
||
gh issue comment "${{ github.event.issue.number }}" \
|
||
--repo "${{ github.repository }}" --body-file /tmp/failure.md
|