name: "Бета: пакетное ревью ship · тело (#716)" # #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели — # правка в механических рамках и зелёный лёгкий Validate. Их код читается # пакетно ночью; перед бетой — только непрочитанные или изменённые задачи. # Гейт публикации беты (`ship-review.mjs check`) требует покрытие всех ship-задач # диапазона «прошлый тег..кандидат»; ночного документа с тем же патч-набором # достаточно, отдельный документ тега беты нужен не всегда (#727). # # Три job, как у release-review.yml (#551, #556): детерминированный вход, # модель без права записи, детерминированная публикация документа в `dev`. # # #727: `tag=nightly` — ночной режим. Его запускает `_nightly.yml` после # полного Validate с `candidate` = голова `dev` того прогона. Читаются только # ship-задачи без покрытия или изменившиеся после ревью (`shipCoverage`), документ — # `SHIP-REVIEW-<база>-dev-.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 # #737: строка расхода сессии модели (scripts/model-usage.mjs). outputs: usage: ${{ steps.usage.outputs.line }} # Недоверенная стадия без права записи (#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 ` каждого коммита; делает ли код то, что сказано в строке ТЗ, и только это; не ломает ли соседнее поведение; не вышла ли правка из трека ship по смыслу (новый UX-контракт, геометрия, конфиг, перф) — это находка Medium с предложением трека. Исполняй, где дёшево: `npm test`, смоук, который называет строка ТЗ или выбирает `node scripts/smoke-select.mjs --base ^ --head `. Серьёзность: 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"]}' # #737: как у конвейера (_process.yml) — расход выходом job, не через # artifact; execution_file не выгружается и не печатается. - name: Снять расход модели id: usage if: always() continue-on-error: true env: EXEC: ${{ steps.review.outputs.execution_file }} run: | line=$(node scripts/model-usage.mjs --execution-file="$EXEC") echo "line=$line" >> "$GITHUB_OUTPUT" echo "- расход модели: \`$line\`" >> "$GITHUB_STEP_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 }} # #737: расход модели — строкой сразу после блока; недоверенный ввод. USAGE: ${{ needs.model_review.outputs.usage }} 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). # #748: ночью — своё: имя документа без префикса, задача ночного режима. if [ "$MODE" = "nightly" ]; then night=${DOC##*/SHIP-REVIEW-} subject="docs: nightly ship review ${night%.md}" lead="Ночное пакетное ревью задач track:ship (PROCESS.md §11.7)." issue="#727" else subject="docs: ship review for $TAG" lead="Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7)." issue="#696" fi 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), usage: process.env.USAGE ?? "", }))); ' "$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 "$subject" echo "" echo "$lead" echo "Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low." echo "" echo "Issue: $issue" 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 if [ "$MODE" = "nightly" ]; then echo "### Ночное пакетное ревью ship" >> "$GITHUB_STEP_SUMMARY" else echo "### Предрелизное пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY" fi 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 }}"