mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
ci: event-driven process pipeline for spec and code review
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
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
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 }}). Статусная метка не менялась, задача осталась на месте."
|
||||
Reference in New Issue
Block a user