feat(process): nightly ship batch review, reused by the beta gate by patch set (#727)

Ship tasks merge without a model review and their code was first read by
the batch review right before a beta: one session over the whole range,
ten to forty-five minutes on the release path, days after the merge. The
gate also knew a single document (SHIP-REVIEW-<tag>.md) and covered tasks
by number only, so a commit that landed after the review under the same
trailer still counted as read.

- scripts/ship-review.mjs: the patch set of a task is the sorted
  `git patch-id --stable` of its range commits, without `Release:`
  commits (the beta candidate carries every Issue: of the line) and
  commits touching only docs/reviews/**; the diff options are explicit
  so a local git config cannot change it. shipCoverage rates every ship
  task from the documents of the same base (candidate and origin/dev,
  latest publication wins): clean, high, stale, none; documents without
  `patches` cover by number. `tag=nightly` is a reserved mode: the
  candidate is required, the document is
  SHIP-REVIEW-<base>-dev-<sha12>.md, only none/stale tasks are read and
  nothing runs when nothing is uncovered. The beta reads the same delta
  (force=true reads everything, as before); the brief names what the
  night already read. The gate refuses none/stale with the command and
  keeps the High refusal with force=true; all clean passes without a tag
  document. The machine block gains `mode` and `patches` at its end.
  comment-high writes one line per task of a nightly document with High,
  once per document (hp:ship-review-high).
- _ship-review.yml: prepare refuses nightly without a candidate before
  defaulting to the dev tip, computes the document from base and SHA and
  no longer reads a prepare failure behind `| tee` as "no ship tasks";
  publish takes mode and patches from prepare, never from the model
  result; a new step comments High at night with HP_PROCESS_TOKEN.
- _nightly.yml: the Validate run SHA is a separate step output before
  the wait; a new job dispatches ship-review.yml -f tag=nightly on it
  whatever Validate's outcome, waits only for the run to appear and
  never colours the night. Thin files in main are unchanged.
- reviews-index/reviews-archive: the nightly name is a ship document
  with nightly: true; a beta base archives with its line, a stable base
  with the nearest archived line newer than the base, or stays.
- PROCESS.md §11.7, §10.4 and REVIEWER.md describe the nightly mode,
  patch set, coverage and beta delta; the digest test pins the key rule.

Tests run the prepare, publish and comment steps and the nightly steps
on real bash with real git in temporary repositories; only push
transport and gh are faked.

Issue: #727
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 03:25:24 +00:00
committed by claude[bot]
parent af339fda3b
commit e58d7d06f8
10 changed files with 1271 additions and 72 deletions
+68 -1
View File
@@ -18,6 +18,11 @@
# успехом в момент постановки Validate в очередь, и красный полный прогон не
# делал ночной workflow красным. Теперь job находит запущенный прогон и ждёт
# его: успешный dispatch — не успешная проверка.
#
# #727: после Validate при любом его исходе ночь запускает пакетное ревью ship
# (`ship-review.yml -f tag=nightly`) на SHA прогона Validate: оно читает код, а
# не прогон. Ship-задача узнаёт о дефектах в ночь после слияния, а бета читает
# только то, что ночь не прочла (PROCESS.md §11.7). Цвет ночи — цвет Validate.
name: "Ночной полный прогон dev · тело (#623)"
on:
@@ -34,6 +39,9 @@ jobs:
name: "Запустить Validate на dev с полным набором и дождаться результата"
runs-on: ubuntu-24.04
timeout-minutes: 90
outputs:
# #727: SHA прогона Validate — отдельным шагом до ожидания, красный Validate его не теряет.
head_sha: ${{ steps.validate.outputs.head_sha }}
steps:
# #658: при плане 02:30 UTC ночь фактически стартовала в 07:42–08:05 и
# кончалась в рабочее время владельца. Сдвиг старта больше часа — видимое
@@ -55,7 +63,9 @@ jobs:
if [ "$lag" -gt 60 ]; then
echo "::warning::ночной прогон стартовал на $lag мин позже расписания ($SCHEDULE UTC) — очередь расписаний GitHub (#658)"
fi
- env:
- name: "Запустить Validate и найти его прогон"
id: validate
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
@@ -80,5 +90,62 @@ jobs:
url="${{ github.server_url }}/$REPO/actions/runs/$run_id"
echo "дочерний прогон: $url"
echo "- Validate(dev, full=true): $url" >> "$GITHUB_STEP_SUMMARY"
head_sha=$(gh run view "$run_id" --repo "$REPO" --json headSha --jq .headSha)
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- name: "Дождаться Validate"
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ steps.validate.outputs.run_id }}
run: |
set -euo pipefail
run_id="$RUN_ID"
# Ждём завершения; красный дочерний прогон — красный ночной.
gh run watch "$run_id" --repo "$REPO" --exit-status --interval 30
# #727 К6: ночное пакетное ревью ship на SHA прогона Validate — при любом его
# исходе. Dispatch тонкого `ship-review.yml` (`github.token` порождает прогон,
# как у Validate; входы и права тонких файлов уже в main). Job ждёт только
# появления прогона, не его конца; не появился — предупреждение, не красная
# ночь: цвет ночи — цвет Validate.
ship_review:
name: "Запустить ночное пакетное ревью ship на SHA прогона Validate"
needs: dispatch
if: always()
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: "Запустить ship-ревью и дождаться появления прогона"
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
CANDIDATE: ${{ needs.dispatch.outputs.head_sha }}
run: |
set -euo pipefail
if [ -z "$CANDIDATE" ]; then
echo "::warning::нет SHA прогона Validate — ночное ship-ревью не запущено"
echo "- Ship-ревью ночью: не запущено — нет SHA прогона Validate" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
since=$(date -u +%FT%TZ)
if ! gh workflow run ship-review.yml --ref dev -f tag=nightly -f candidate="$CANDIDATE" --repo "$REPO"; then
echo "::warning::ship-ревью ночью не запущено: dispatch отклонён — ночь не красится"
echo "- Ship-ревью ночью: dispatch отклонён" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
run_id=""
for _ in $(seq 1 18); do
sleep 10
run_id=$(gh run list --repo "$REPO" --workflow ship-review.yml --branch dev \
--event workflow_dispatch --json databaseId,createdAt --limit 5 \
--jq "[.[] | select(.createdAt >= \"$since\")] | sort_by(.createdAt) | last | .databaseId // empty")
[ -n "$run_id" ] && break
done
if [ -z "$run_id" ]; then
echo "::warning::прогон ship-ревью не появился за 3 минуты — ночь не красится, её цвет — цвет Validate"
echo "- Ship-ревью ночью (\`$CANDIDATE\`): прогон не появился за 3 минуты" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "- Ship-ревью ночью (\`$CANDIDATE\`): ${{ github.server_url }}/$REPO/actions/runs/$run_id" >> "$GITHUB_STEP_SUMMARY"
+44 -8
View File
@@ -8,6 +8,11 @@ name: "Бета: пакетное ревью ship · тело (#716)"
#
# Три 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`;
@@ -15,6 +20,7 @@ on:
# файла в ветке по умолчанию 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
@@ -49,6 +55,9 @@ jobs:
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:
@@ -69,24 +78,31 @@ jobs:
FORCE: ${{ inputs.force }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
doc=$(node scripts/ship-review.mjs doc --tag="$TAG")
# Отказ 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" \
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
echo "::notice::ship-задач в диапазоне нет — ревью не нужно"
# #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
@@ -183,8 +199,9 @@ jobs:
prompt: |
Ты ревьюер кода проекта House Plan. Язык ответа — русский.
Бета: ${{ inputs.tag }} · кандидат ${{ needs.prepare.outputs.candidate }}
(рабочая копия уже на нём) · база диапазона: ${{ needs.prepare.outputs.base || 'нет' }}.
${{ 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.
@@ -271,7 +288,8 @@ jobs:
path: ${{ runner.temp }}/ship-review-result
# Модель пишет только текст. Путь, машинный блок (его читает гейт беты),
# индекс и коммит решает этот шаг.
# индекс и коммит решает этот шаг. #727: `mode` и `patches` блока — из
# prepare (детерминированный патч-набор), не из результата модели.
- name: Опубликовать документ
env:
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
@@ -280,11 +298,13 @@ jobs:
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")"
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")
@@ -301,7 +321,7 @@ jobs:
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,
runUrl: process.env.RUN_URL, mode: process.env.MODE, patches: m.parsePatches(process.env.PATCHES),
})));
' "$high" "$medium" "$low"
} > "$DOC"
@@ -331,3 +351,19 @@ jobs:
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 }}"