mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 04:38:55 +00:00
After #705 and #723 two more workflow bodies still treated every failed push as a moved dev: the SHIP-REVIEW publication (_ship-review.yml) retried three times with "dev went ahead", and the derived-artifacts bot commit (_beta-derived.yml) told the release manager to rerun the workflow. A refusal by GitHub itself - a token without the workflow right, a branch rule, a hook - is cured by neither, and neither step said what GitHub answered. Both pushes now keep stderr and hand it to the #705 classifier through the same CLI (merge-candidate.mjs --push-refusal). A stale lease keeps the old behaviour: another attempt for the ship review, the rerun advice for the derived artifacts. Any other outcome stops the step at once: the log gets the git answer and the step summary gets the reason and the git answer without secrets (--summary, refusalSummary with the new ship-review and beta-derived labels). The classifier comes from dev, as for the other steps of these bodies: both jobs check out dev, and the ship review resets to origin/dev before every attempt. The ship review commit message is built line by line into a file instead of a heredoc, as in #723. The thin callers ship-review.yml and beta-derived.yml are untouched. The rebase guard in _process.yml also writes the refusal reason to its step summary now (--summary, label "rebase"); a stale lease writes none. test/publish-push-refusal.test.mjs runs both steps as they are with real bash and real git in temporary repositories (moved dev = a real neighbour push, GitHub refusal = recorded stderr with a token, a credential URL and an Authorization header); on the old bodies 10 of its 12 new tests fail. The #705 execution tests of the rebase guard in rebase-generated.test.mjs now also read the step summary. PROCESS.md names the two steps next to the Issue: #730 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
386 lines
21 KiB
YAML
386 lines
21 KiB
YAML
name: "Бета: пакетное ревью ship · тело (#716)"
|
||
|
||
# #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели —
|
||
# правка в механических рамках и зелёный лёгкий Validate. Их код читает одна
|
||
# сессия модели перед бетой: все ship-задачи диапазона «прошлый тег..кандидат».
|
||
# Гейт публикации беты (`ship-review.mjs check`) требует этот документ, если в
|
||
# диапазоне есть ship-задачи.
|
||
#
|
||
# Три job, как у release-review.yml (#551, #556): детерминированный вход,
|
||
# модель без права записи, детерминированная публикация документа в `dev`.
|
||
#
|
||
# #727: `tag=nightly` — ночной режим. Его запускает `_nightly.yml` после
|
||
# полного Validate с `candidate` = голова `dev` того прогона. Читаются только
|
||
# ship-задачи без покрытия или изменившиеся после ревью (`shipCoverage`), документ —
|
||
# `SHIP-REVIEW-<база>-dev-<sha12>.md`. Бета так же читает только дельту.
|
||
|
||
on:
|
||
# #716: тело вызывается тонким файлом `ship-review.yml` по ссылке `@dev`;
|
||
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
|
||
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
|
||
workflow_call:
|
||
inputs:
|
||
# #727: `nightly` — зарезервированное значение (ночной режим, candidate обязателен).
|
||
tag:
|
||
description: "Beta tag the review is for, for example v1.79.0-beta.1"
|
||
required: true
|
||
type: string
|
||
candidate:
|
||
description: "Exact candidate SHA; empty = the dev tip"
|
||
required: false
|
||
type: string
|
||
default: ""
|
||
force:
|
||
description: "Review again even when the document already exists in dev"
|
||
required: false
|
||
type: boolean
|
||
default: false
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
# Concurrency уровня workflow — у вызывающего `ship-review.yml` (#716).
|
||
|
||
jobs:
|
||
prepare:
|
||
name: "Ship-ревью: задачи диапазона"
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 10
|
||
permissions:
|
||
contents: read
|
||
issues: read
|
||
outputs:
|
||
proceed: ${{ steps.range.outputs.proceed }}
|
||
candidate: ${{ steps.range.outputs.candidate }}
|
||
base: ${{ steps.range.outputs.base }}
|
||
doc: ${{ steps.range.outputs.doc }}
|
||
issues: ${{ steps.range.outputs.issues }}
|
||
mode: ${{ steps.range.outputs.mode }}
|
||
patches: ${{ steps.range.outputs.patches }}
|
||
subject: ${{ steps.range.outputs.subject }}
|
||
steps:
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 0
|
||
ref: dev
|
||
persist-credentials: false
|
||
|
||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||
with:
|
||
node-version: 22
|
||
|
||
- name: Кандидат, база и ship-задачи
|
||
id: range
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
TAG: ${{ inputs.tag }}
|
||
CANDIDATE: ${{ inputs.candidate }}
|
||
FORCE: ${{ inputs.force }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
run: |
|
||
# Отказ prepare за `| tee` не должен читаться как «ship-задач нет» (#727).
|
||
set -o pipefail
|
||
git fetch -q --tags origin
|
||
# #727: tag=nightly без candidate — отказ до подстановки вершины dev.
|
||
node scripts/ship-review.mjs mode --tag="$TAG" --candidate="$CANDIDATE"
|
||
if [ -z "$CANDIDATE" ]; then CANDIDATE=$(git rev-parse origin/dev); fi
|
||
git cat-file -e "$CANDIDATE^{commit}"
|
||
CANDIDATE=$(git rev-parse "$CANDIDATE^{commit}")
|
||
doc=$(node scripts/ship-review.mjs doc --tag="$TAG" --candidate="$CANDIDATE")
|
||
if [ "$FORCE" != "true" ] && git cat-file -e "origin/dev:$doc" 2>/dev/null; then
|
||
echo "::notice::$doc уже есть в dev — повторное ревью не запускается (force=true, чтобы переснять)"
|
||
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||
exit 0
|
||
fi
|
||
out="$RUNNER_TEMP/ship-review-input"
|
||
node scripts/ship-review.mjs prepare --tag="$TAG" --candidate="$CANDIDATE" --force="$FORCE" \
|
||
--out="$out" --repo="${{ github.repository }}" --run-url="$RUN_URL" | tee "$RUNNER_TEMP/range.txt"
|
||
cat "$RUNNER_TEMP/range.txt" >> "$GITHUB_OUTPUT"
|
||
echo "candidate=$CANDIDATE" >> "$GITHUB_OUTPUT"
|
||
issues=$(sed -n 's/^issues=//p' "$RUNNER_TEMP/range.txt")
|
||
if [ -z "$issues" ]; then
|
||
# #727 К5: пустая дельта — все ship-задачи покрыты документами базы.
|
||
note=$(sed -n 's/^note=//p' "$RUNNER_TEMP/range.txt")
|
||
echo "::notice::${note:-ship-задач в диапазоне нет — ревью не нужно}"
|
||
if [ -n "$note" ]; then echo "- $note" >> "$GITHUB_STEP_SUMMARY"; fi
|
||
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||
exit 0
|
||
fi
|
||
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
||
(cd "$out" && sha256sum brief.md ship-issues.json > manifest.sha256)
|
||
cat "$out/brief.md" >> "$GITHUB_STEP_SUMMARY"
|
||
|
||
- name: Передать вход модели
|
||
if: steps.range.outputs.proceed == 'true'
|
||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||
with:
|
||
name: ship-review-input-${{ github.run_id }}-${{ github.run_attempt }}
|
||
path: ${{ runner.temp }}/ship-review-input
|
||
if-no-files-found: error
|
||
retention-days: 3
|
||
|
||
model_review:
|
||
name: "Ship-ревью: работа модели"
|
||
needs: prepare
|
||
if: needs.prepare.outputs.proceed == 'true'
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 45
|
||
# Недоверенная стадия без права записи (#556): документ публикует
|
||
# `publish`. `github_token` у шага Review обязателен — без него action
|
||
# меняет OIDC на собственный App-токен с правом записи.
|
||
permissions:
|
||
contents: read
|
||
steps:
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 0
|
||
ref: ${{ needs.prepare.outputs.candidate }}
|
||
persist-credentials: false
|
||
|
||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||
with:
|
||
node-version: 22
|
||
cache: npm
|
||
|
||
- name: Получить вход
|
||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||
with:
|
||
name: ship-review-input-${{ github.run_id }}-${{ github.run_attempt }}
|
||
path: ${{ runner.temp }}/ship-review-input
|
||
|
||
- name: Проверить вход и кандидата
|
||
env:
|
||
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
|
||
run: |
|
||
(cd "$RUNNER_TEMP/ship-review-input" && sha256sum -c manifest.sha256)
|
||
test "$(git rev-parse HEAD)" = "$CANDIDATE"
|
||
|
||
- name: Установить зависимости
|
||
run: npm ci
|
||
|
||
- name: Кэш браузеров Playwright
|
||
id: pw
|
||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||
|
||
- name: Установить Chromium
|
||
if: steps.pw.outputs.cache-hit != 'true'
|
||
run: npx playwright install chromium
|
||
|
||
# Тот же обход, что у конвейера (_process.yml, anthropics issue 1817).
|
||
- name: Установить Claude Code детерминированно
|
||
id: claude_bin
|
||
run: |
|
||
src=$(ls "$RUNNER_WORKSPACE"/../_actions/anthropics/claude-code-*/v1/src/entrypoints/run.ts 2>/dev/null | head -1)
|
||
ver=$(grep -oE 'claudeCodeVersion = "[0-9]+\.[0-9]+\.[0-9]+"' "$src" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)
|
||
ver="${ver:-2.1.265}"
|
||
base=https://downloads.claude.ai/claude-code-releases
|
||
bin="$HOME/.local/bin/claude"
|
||
mkdir -p "$(dirname "$bin")"
|
||
curl -fsSL --retry 3 "$base/$ver/linux-x64/claude" -o "$bin"
|
||
sum=$(curl -fsSL --retry 3 "$base/$ver/manifest.json" | jq -r '.platforms["linux-x64"].checksum')
|
||
echo "$sum $bin" | sha256sum -c -
|
||
chmod +x "$bin"
|
||
"$bin" --version
|
||
echo "path=$bin" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Review
|
||
id: review
|
||
uses: anthropics/claude-code-action@9cdae7f0d995e3ba7c33f226087fdf82a59cd520 # v1
|
||
env:
|
||
REVIEW_DOC: ${{ runner.temp }}/ship-review.md
|
||
REVIEW_INPUT: ${{ runner.temp }}/ship-review-input
|
||
with:
|
||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||
path_to_claude_code_executable: ${{ steps.claude_bin.outputs.path }}
|
||
prompt: |
|
||
Ты ревьюер кода проекта House Plan. Язык ответа — русский.
|
||
|
||
${{ needs.prepare.outputs.subject }}
|
||
Кандидат ${{ needs.prepare.outputs.candidate }} (рабочая копия уже на нём) · база
|
||
диапазона: ${{ needs.prepare.outputs.base || 'нет' }}.
|
||
Задачи: ${{ needs.prepare.outputs.issues }} — вход в $REVIEW_INPUT/brief.md
|
||
(строка ТЗ каждой задачи и её коммиты) и $REVIEW_INPUT/ship-issues.json.
|
||
|
||
Эти задачи шли по треку ship и слиты в dev без ревью модели: правка
|
||
в механических рамках и зелёный лёгкий Validate. Ты — первый, кто
|
||
читает их код. Правила — docs/process/REVIEWER.md, раздел
|
||
«Пакетное ревью ship», канон — PROCESS.md §5 и §11.7. Прочитай их
|
||
первыми.
|
||
|
||
По каждой задаче: `git show <sha>` каждого коммита; делает ли код
|
||
то, что сказано в строке ТЗ, и только это; не ломает ли соседнее
|
||
поведение; не вышла ли правка из трека ship по смыслу (новый
|
||
UX-контракт, геометрия, конфиг, перф) — это находка Medium с
|
||
предложением трека. Исполняй, где дёшево: `npm test`, смоук,
|
||
который называет строка ТЗ или выбирает
|
||
`node scripts/smoke-select.mjs --base <sha>^ --head <sha>`.
|
||
|
||
Серьёзность: High — дефект, который увидит пользователь беты, или
|
||
правка не делает заявленного; High блокирует публикацию беты.
|
||
Medium — поведение вне заявленного или вне рамок трека; Low —
|
||
остальное. Находка чинится отдельной задачей, ты ничего не правишь.
|
||
|
||
Ты ничего не правишь и не публикуешь: ни код, ни issue, ни
|
||
комментарии. Права на запись у тебя нет. Любые изменения рабочей
|
||
копии будут отброшены — после проверок восстанови её сам
|
||
(`git checkout -- . && git clean -fd`), если что-то менял.
|
||
|
||
Напиши документ в файл по пути из переменной REVIEW_DOC (абсолютный,
|
||
вне репозитория). Первой строкой после заголовка —
|
||
`Итог: High N · Medium N · Low N`. Затем по задаче на раздел
|
||
`## #NN — заголовок`: что проверил (команда → результат), находки с
|
||
воспроизведением, чего не проверял.
|
||
|
||
Затем верни JSON по схеме — последнее обязательное действие.
|
||
claude_args: |
|
||
--max-turns 150
|
||
--allowedTools Read,Write,Grep,Glob,Bash
|
||
--json-schema '{"type":"object","properties":{"high":{"type":"integer"},"medium":{"type":"integer"},"low":{"type":"integer"},"summary":{"type":"string"}},"required":["high","medium","low","summary"]}'
|
||
|
||
- name: Запечатать результат модели
|
||
env:
|
||
SOURCE: ${{ runner.temp }}/ship-review.md
|
||
OUT: ${{ steps.review.outputs.structured_output }}
|
||
run: |
|
||
test -s "$SOURCE" || { echo "::error::модель не оставила документ ревью"; exit 1; }
|
||
dir="$RUNNER_TEMP/ship-review-result"
|
||
mkdir -p "$dir"
|
||
printf '%s' "$OUT" > "$dir/result.json"
|
||
jq -e '(.high|type=="number") and (.medium|type=="number") and (.low|type=="number") and (.summary|type=="string")' \
|
||
"$dir/result.json" >/dev/null
|
||
cp "$SOURCE" "$dir/ship-review.md"
|
||
(cd "$dir" && sha256sum ship-review.md result.json > manifest.sha256)
|
||
|
||
- name: Передать результат публикации
|
||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||
with:
|
||
name: ship-review-result-${{ github.run_id }}-${{ github.run_attempt }}
|
||
path: ${{ runner.temp }}/ship-review-result
|
||
if-no-files-found: error
|
||
retention-days: 3
|
||
|
||
publish:
|
||
name: "Ship-ревью: документ в dev"
|
||
needs: [prepare, model_review]
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 10
|
||
permissions:
|
||
contents: read
|
||
steps:
|
||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||
with:
|
||
fetch-depth: 0
|
||
ref: dev
|
||
persist-credentials: false
|
||
|
||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||
with:
|
||
node-version: 22
|
||
|
||
- name: Получить результат модели
|
||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||
with:
|
||
name: ship-review-result-${{ github.run_id }}-${{ github.run_attempt }}
|
||
path: ${{ runner.temp }}/ship-review-result
|
||
|
||
# Модель пишет только текст. Путь, машинный блок (его читает гейт беты),
|
||
# индекс и коммит решает этот шаг. #727: `mode` и `patches` блока — из
|
||
# prepare (детерминированный патч-набор), не из результата модели.
|
||
- name: Опубликовать документ
|
||
env:
|
||
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
TAG: ${{ inputs.tag }}
|
||
DOC: ${{ needs.prepare.outputs.doc }}
|
||
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
|
||
BASE: ${{ needs.prepare.outputs.base }}
|
||
ISSUES: ${{ needs.prepare.outputs.issues }}
|
||
MODE: ${{ needs.prepare.outputs.mode }}
|
||
PATCHES: ${{ needs.prepare.outputs.patches }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
run: |
|
||
dir="$RUNNER_TEMP/ship-review-result"
|
||
(cd "$dir" && sha256sum -c manifest.sha256)
|
||
test "$DOC" = "$(node scripts/ship-review.mjs doc --tag="$TAG" --candidate="$CANDIDATE")"
|
||
high=$(jq -r '.high' "$dir/result.json")
|
||
medium=$(jq -r '.medium' "$dir/result.json")
|
||
low=$(jq -r '.low' "$dir/result.json")
|
||
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#730, как #723).
|
||
msg="$RUNNER_TEMP/ship-review-commit.txt"
|
||
push_err="$RUNNER_TEMP/ship-review-push.stderr"
|
||
for attempt in 1 2 3; do
|
||
git fetch -q origin dev
|
||
git reset -q --hard origin/dev
|
||
git clean -fdq
|
||
mkdir -p docs/reviews
|
||
{
|
||
cat "$dir/ship-review.md"
|
||
printf '\n\n'
|
||
node -e '
|
||
import("./scripts/ship-review.mjs").then((m) => process.stdout.write(m.anchorBlock({
|
||
tag: process.env.TAG, candidate: process.env.CANDIDATE, base: process.env.BASE || null,
|
||
issues: String(process.env.ISSUES || "").split(",").filter(Boolean).map(Number),
|
||
high: Number(process.argv[1]), medium: Number(process.argv[2]), low: Number(process.argv[3]),
|
||
runUrl: process.env.RUN_URL, mode: process.env.MODE, patches: m.parsePatches(process.env.PATCHES),
|
||
})));
|
||
' "$high" "$medium" "$low"
|
||
} > "$DOC"
|
||
node scripts/reviews-index.mjs --dir=docs/reviews --strict
|
||
git add -- "$DOC" docs/reviews/INDEX.md
|
||
git diff --cached --name-only | node scripts/review-doc-guard.mjs
|
||
{
|
||
echo "docs: ship review for $TAG"
|
||
echo ""
|
||
echo "Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7)."
|
||
echo "Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low."
|
||
echo ""
|
||
echo "Issue: #696"
|
||
echo "User-Visible: no"
|
||
} > "$msg"
|
||
git -c user.name="claude[bot]" \
|
||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||
commit -q -F "$msg"
|
||
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
|
||
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
|
||
echo "### Пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY"
|
||
echo "Задачи $ISSUES · High $high · Medium $medium · Low $low — \`$DOC\` в dev." >> "$GITHUB_STEP_SUMMARY"
|
||
echo "::notice::$DOC опубликован: High $high · Medium $medium · Low $low"
|
||
exit 0
|
||
fi
|
||
# #730: сдвигом dev считается только устаревший lease — отказ
|
||
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705),
|
||
# как у ревью релиза (#723). Скрипт — из dev: рабочая копия только что
|
||
# сброшена на origin/dev. Отказ GitHub (право на workflow, правило
|
||
# ветки, хук) повтор не лечит: шаг останавливается, причина и ответ
|
||
# git без токена — в журнале и в сводке шага.
|
||
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
|
||
--stage=ship-review --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
|
||
if [ "$kind" != "stale" ]; then
|
||
echo "::error::push $DOC в dev отклонён ($kind) — это не сдвиг dev, повтор не поможет; причина и ответ git — выше и в сводке шага"
|
||
exit 1
|
||
fi
|
||
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
|
||
sleep $((attempt * 10))
|
||
done
|
||
echo "::error::документ ревью не опубликован в dev за три попытки"
|
||
exit 1
|
||
|
||
# #727 К8: High ночного документа — одна строка в каждую его задачу.
|
||
# Повтор на тот же документ не пишется (метка hp:ship-review-high doc=…),
|
||
# Medium и Low в issue не идут — их решает владелец (§11.7). Режим и
|
||
# High проверяет и скрипт: шаг вне ночи или без High ничего не пишет.
|
||
- name: High ночью — строка в задачи документа
|
||
if: needs.prepare.outputs.mode == 'nightly'
|
||
env:
|
||
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
|
||
MODE: ${{ needs.prepare.outputs.mode }}
|
||
DOC: ${{ needs.prepare.outputs.doc }}
|
||
ISSUES: ${{ needs.prepare.outputs.issues }}
|
||
run: |
|
||
high=$(jq -r '.high' "$RUNNER_TEMP/ship-review-result/result.json")
|
||
node scripts/ship-review.mjs comment-high --mode="$MODE" --high="$high" --doc="$DOC" \
|
||
--issues="$ISSUES" --repo="${{ github.repository }}"
|