mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
Adds .github/workflows/process.yml. A status label change is the trigger: S4-spec-review runs the spec review, S7-code-review runs the code review, and the verdict decides the next label. Only a green verdict advances; yellow and red return the task to its author. Cycle limits (4, or 2 on the light track) are counted from the verdicts already posted on the issue. Labels are moved with HP_PROCESS_TOKEN, not GITHUB_TOKEN, so the change emits an event and the chain continues. Issue: #114 User-Visible: no
207 lines
12 KiB
YAML
207 lines
12 KiB
YAML
name: Process
|
||
|
||
# Событийный конвейер процесса (PROCESS.md). Смена статусной метки — это
|
||
# сообщение: она порождает событие, событие запускает следующий шаг.
|
||
#
|
||
# S4-spec-review -> ревью ТЗ -> S5-ready | S3-spec
|
||
# S7-code-review -> код-ревью -> S8-merged | S6-in-progress
|
||
#
|
||
# Две вещи, без которых конвейер молча не работает:
|
||
#
|
||
# 1. Метки переставляются токеном HP_PROCESS_TOKEN, а не GITHUB_TOKEN. GitHub
|
||
# намеренно не запускает workflow от событий, вызванных GITHUB_TOKEN, чтобы
|
||
# не было циклов — цепочка оборвалась бы после первого шага.
|
||
# 2. Этот файл обязан лежать в ветке по умолчанию (main). Для события `issues`
|
||
# GitHub берёт workflow только оттуда, независимо от того, что в dev.
|
||
|
||
on:
|
||
issues:
|
||
types: [labeled]
|
||
|
||
concurrency:
|
||
# Два события по одному issue не должны запускать два прогона.
|
||
group: process-issue-${{ github.event.issue.number }}
|
||
cancel-in-progress: false
|
||
|
||
permissions:
|
||
contents: read
|
||
issues: 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: 30
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
ref: dev
|
||
|
||
- uses: actions/setup-node@v4
|
||
with: { node-version: 22 }
|
||
|
||
- 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 и
|
||
указание способа доказательства. Отдельно проверь, что автор не
|
||
выдал догадку за решение: утверждение о поведении, которого нет ни
|
||
в одном документе и которое не помечено как предположение, —
|
||
замечание. Не бывает сложной задачи без единого открытого вопроса.
|
||
|
||
Для этапа code: материал — diff по issue. Ручного тестирования в
|
||
цикле нет, поэтому именно ты отвечаешь на вопрос «оно вообще
|
||
работает». По каждому AC: либо он доказан автотестом и ты убедился,
|
||
что тест умеет падать, либо разобран по коду с явной записью
|
||
«проверено чтением, не исполнением». «Verified» без названной
|
||
команды и её результата доказательством не является. Проверь
|
||
трейлеры Issue и User-Visible, при User-Visible: yes — правки в оба
|
||
changelog в том же коммите.
|
||
|
||
Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь.
|
||
|
||
Серьёзность: High блокирует; Medium обязан стать отдельным issue;
|
||
Low либо правится, либо снимается с записью. Жёлтый вердикт
|
||
допустим при полностью выполненных AC, если изменение не решает
|
||
заявленный сценарий или ухудшает смежный. Продуктовое рассуждение
|
||
расширяет вопросы, но не отменяет AC и не даёт права менять скоуп.
|
||
|
||
Каждую Medium-находку заведи отдельным issue со ссылкой на
|
||
#${{ github.event.issue.number }} и метками: тип, приоритет,
|
||
S1-new. «Оставили в тексте ревью» закрытием не считается и прямо
|
||
запрещено §12.
|
||
|
||
Оставь в issue комментарий с разбором: находки с воспроизведением,
|
||
что проверено и корректно, чего не проверял. Первой строкой —
|
||
вердикт в формате §7.2:
|
||
`Вердикт: зелёный/жёлтый/красный · цикл r${{ needs.guard.outputs.cycle }}/${{ needs.guard.outputs.limit }} · High: N · Medium: N → #…`
|
||
|
||
Затем верни JSON по схеме.
|
||
claude_args: |
|
||
--max-turns 40
|
||
--allowedTools Read,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"]}'
|
||
|
||
- name: Переставить метку
|
||
env:
|
||
# Именно PAT: с GITHUB_TOKEN следующий шаг конвейера не запустится.
|
||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
OUT: ${{ steps.review.outputs.structured_output }}
|
||
STAGE: ${{ needs.guard.outputs.stage }}
|
||
NUM: ${{ github.event.issue.number }}
|
||
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
|
||
case "$STAGE" in
|
||
spec) from=S4-spec-review; to=S5-ready ;;
|
||
code) from=S7-code-review; to=S8-merged ;;
|
||
esac
|
||
else
|
||
case "$STAGE" in
|
||
spec) from=S4-spec-review; to=S3-spec ;;
|
||
code) from=S7-code-review; to=S6-in-progress ;;
|
||
esac
|
||
fi
|
||
|
||
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: |
|
||
gh issue comment "${{ github.event.issue.number }}" --repo "${{ github.repository }}" \
|
||
--body "Автоматическое ревью не отработало: [прогон](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). Статусная метка не менялась, задача осталась на месте."
|