mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
322 lines
20 KiB
YAML
322 lines
20 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 }}
|
||
AUTHOR: ${{ github.event.issue.user.login }}
|
||
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') }}
|
||
NUM: ${{ github.event.issue.number }}
|
||
run: |
|
||
stage=""
|
||
# Лимит циклов: 4 обычный, 2 на лёгком треке (PROCESS.md §4).
|
||
limit=4; [ "$SMALL" = "true" ] && limit=2
|
||
|
||
# Счётчик — по числу уже опубликованных вердиктов в issue.
|
||
done_cycles=$(gh issue view "$NUM" --repo "${{ github.repository }}" \
|
||
--json comments -q '[.comments[] | select(.body | test("Вердикт:"))] | length')
|
||
|
||
# В процесс идут только issue, созданные владельцем: репозиторий
|
||
# публичный, чужие отчёты бывают невалидны и статусов не несут.
|
||
if [ "$AUTHOR" != "Matysh" ]; then
|
||
echo "issue от $AUTHOR, не от владельца — пропуск"
|
||
elif [ "$BLOCKED" = "true" ]; then
|
||
echo "стоит blocked — конвейер не запускается"
|
||
elif [ "$EXHAUSTED" = "true" ]; then
|
||
echo "стоит review-4 — решение за владельцем"
|
||
elif [ "$done_cycles" -ge "$limit" ]; then
|
||
echo "циклов пройдено $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). Пятого захода нет: решение владельца — разделить задачу, отклонить или арбитраж (PROCESS.md §4)."
|
||
else
|
||
case "$LABEL" in
|
||
S4-spec-review) stage="spec" ;;
|
||
S7-code-review) stage="code" ;;
|
||
*) echo "метка $LABEL конвейер не запускает" ;;
|
||
esac
|
||
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@v4
|
||
with:
|
||
fetch-depth: 0
|
||
ref: dev
|
||
|
||
- uses: actions/setup-node@v4
|
||
with: { node-version: 22 }
|
||
|
||
# Материал ревью живёт в ветке задачи: ТЗ в docs/specs/ и код коммитятся
|
||
# в issue/<NN>-slug. Если ветка запушена — переключаемся на неё, иначе
|
||
# ревьюер прочтёт dev и не найдёт того, что должен оценивать.
|
||
- name: Перейти на ветку задачи
|
||
id: branch
|
||
env:
|
||
NUM: ${{ github.event.issue.number }}
|
||
run: |
|
||
branch=$(git ls-remote --heads origin "issue/${NUM}-*" \
|
||
| head -1 | sed 's|.*refs/heads/||')
|
||
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
|
||
|
||
- 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» без названной команды и её
|
||
результата доказательством не является. Зависимостей в рабочей
|
||
копии нет: перед гейтами выполни `npm ci`. Проверь трейлеры Issue и
|
||
User-Visible, при User-Visible: yes — правки в оба changelog в том же
|
||
коммите.
|
||
|
||
Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь.
|
||
|
||
Серьёзность: High блокирует; Medium обязан стать отдельным 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 → #…`
|
||
|
||
Затем верни 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 }}
|
||
run: |
|
||
if [ -z "$BRANCH" ]; then
|
||
echo "ветки задачи нет — документ некуда класть"; exit 0
|
||
fi
|
||
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
|
||
echo "документ ревью не создан"; exit 0
|
||
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
|
||
git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
|
||
"HEAD:$BRANCH"
|
||
echo "документ опубликован в $BRANCH"
|
||
|
||
- 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. Значит слияние обязано произойти
|
||
# ДО метки, иначе она врờt в промежутке. Конфликт — метка не двигается.
|
||
- name: Слить ветку в dev
|
||
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::ветки задачи нет — сливать нечего"; exit 1
|
||
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
|
||
cat > /tmp/conflict.md <<EOF
|
||
Код-ревью зелёное, но ветка $BRANCH не сливается в dev без конфликта. Статусная метка не менялась: задача осталась в S7-code-review.
|
||
|
||
Разрешить конфликт и запушить dev — на стороне автора, затем снять и вернуть метку S7-code-review для повторного прогона.
|
||
EOF
|
||
gh issue comment "$NUM" --repo "${{ github.repository }}" --body-file /tmp/conflict.md
|
||
exit 1
|
||
fi
|
||
git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev
|
||
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 }}
|
||
TO: ${{ 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
|