ci: register ship-review and beta-derived as thin callers in main (#716)

`workflow_dispatch` runs the file from the chosen ref, but GitHub lists a
workflow and accepts a dispatch (button, `gh workflow run`, API) only when
its file exists on the default branch. `ship-review.yml` (#696) and
`beta-derived.yml` (#697) lived only in `dev`, so neither could be started
at all, and the comment "the file runs from `--ref dev`, no mirror in
`main` needed" was wrong. Both beta steps are needed before the next
promotion would bring them to `main`.

They now follow the #623 layout instead of a full copy in `main`: a thin
caller (trigger, dispatch inputs, run-name, permission ceiling, concurrency)
calls `_ship-review.yml` / `_beta-derived.yml` at `@dev` with
`secrets: inherit`. A full copy would either need a mirror on every edit or
drift silently, and a dispatch from `main` (the button's default) would run
the stale copy; the thin caller runs the dev body from any ref. The caller
ceiling is the union of the body jobs' permissions (#556): ship-review
`contents: read` + `issues: read`, beta-derived `contents: read` +
`actions: read`; writes to `dev` stay with HP_PROCESS_TOKEN as before.

`workflow_sync` in validate.yml now compares eight files, and
test/default-branch-workflows.test.mjs lists the two dispatch-only files
explicitly with the reason checked (only `workflow_dispatch`). Workflow
tests and the #697 provenance mutant read the bodies. PROCESS.md §10.4,
§8 and §11.7 say how these are run and that a new thin file is mirrored
into `main` before it is merged into `dev`.

Issue: #716
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-09-30 21:01:10 +00:00
committed by claude[bot]
parent 7188db8187
commit d11ad9c1c2
10 changed files with 669 additions and 498 deletions
+215
View File
@@ -0,0 +1,215 @@
name: "Бета: производные артефакты на dev · тело (#716)"
# #697, PROCESS.md §8 и §11.4: ветки задач не коммитят ни отпечаток и кадры
# скриншотов документации, ни эталоны golden. Их обновляет один коммит бота на
# `dev` перед кандидатом беты; коммит проверяет релиз-менеджер.
#
# - Скриншоты: съёмка на `dev` тем же каноническим способом, что
# `docs-screenshots.yml`, и приёмка `docs:accept --reviewed`. Кадры, которые
# менять не собирались, обязаны совпасть байт-в-байт (свидетели среды);
# изменившийся кадр принимается, только если назван в `docs_expect_change`.
# Ничего не изменилось — обновляется один отпечаток исходников.
# - Golden: артефакт `golden-images` полного Validate на `dev`
# (`golden_run`) и `golden:accept --reviewed` с объявленными сценами. Коммит
# несёт `Release:` и `Baseline-Reviewed:` — провенанс, который требует
# validate-commit-provenance.mjs.
#
# Необъявленная разница — отказ с перечнем кадров, артефакт съёмки остаётся
# для просмотра: решение, какие кадры сдвинулись законно, принимает человек.
on:
# #716: тело вызывается тонким файлом `beta-derived.yml` по ссылке `@dev`;
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
workflow_call:
inputs:
tag:
description: "Beta tag these artifacts are for, for example v1.79.0-beta.1"
required: true
type: string
docs_expect_change:
description: "Doc scenario ids expected to change, comma-separated (empty = none)"
required: false
type: string
default: ""
golden_run:
description: "Validate run id on dev whose golden-images artifact holds the shifted frames (empty = skip golden)"
required: false
type: string
default: ""
golden_expect_change:
description: "Golden scenes expected to change, comma-separated"
required: false
type: string
default: ""
golden_expect_new:
description: "New golden scenes, comma-separated"
required: false
type: string
default: ""
permissions:
contents: read
# Concurrency уровня workflow — у вызывающего `beta-derived.yml` (#716).
jobs:
accept:
name: "Отпечаток, кадры и эталоны — одним коммитом в dev"
runs-on: ubuntu-24.04
timeout-minutes: 40
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- 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: Install pinned Chromium
if: steps.pw.outputs.cache-hit != 'true'
run: npx playwright install chromium
- name: Build the bundle the screenshots must come from
run: npm run build
# Тот же закреплённый упаковщик, что у docs-screenshots.yml: другой
# переписал бы все кадры без единого изменённого пикселя.
- name: Установить oxipng
env:
OXIPNG_VERSION: 10.2.0
OXIPNG_SHA256: b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e
run: |
set -euo pipefail
asset="oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL -o "$asset" \
"https://github.com/oxipng/oxipng/releases/download/v${OXIPNG_VERSION}/${asset}"
echo "${OXIPNG_SHA256} ${asset}" | sha256sum -c -
mkdir -p "$HOME/.local/bin"
tar -xzf "$asset" --strip-components=1 -C "$HOME/.local/bin" \
"oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu/oxipng"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/oxipng" --version
- name: "Съёмка воспроизводима между прогонами (#410, #422)"
run: node scripts/capture-determinism.mjs
- name: Кадры документации — съёмка и приёмка
id: docs
env:
EXPECT: ${{ inputs.docs_expect_change }}
run: |
set -euo pipefail
node demo/docs/capture.mjs --stability=3
cand="$RUNNER_TEMP/docs-candidate"
rm -rf "$cand" && mkdir -p "$cand"
cp docs/images/*.png docs/images/screenshots.json "$cand/"
# Приёмка сравнивает кандидата с ЗАКОММИЧЕННЫМИ кадрами: рабочая
# копия возвращается к dev, и заменить файлы может только она.
git checkout -- docs/images
git clean -fdq -- docs/images
args=(--reviewed "--from=$cand")
if [ -n "$EXPECT" ]; then args+=("--expect-change=$EXPECT"); fi
node scripts/docs-accept.mjs "${args[@]}"
if git diff --quiet -- docs/images; then changed=false; else changed=true; fi
echo "changed=$changed" >> "$GITHUB_OUTPUT"
git diff --stat -- docs/images
- name: Сохранить кандидата скриншотов для просмотра
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docs-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/docs-candidate
if-no-files-found: ignore
retention-days: 7
- name: Эталоны golden из прогона Validate
id: golden
if: inputs.golden_run != ''
env:
GH_TOKEN: ${{ github.token }}
RUN: ${{ inputs.golden_run }}
EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
EXPECT_NEW: ${{ inputs.golden_expect_new }}
run: |
set -euo pipefail
case "$RUN" in ''|*[!0-9]*) echo "::error::golden_run — числовой id прогона Validate"; exit 1 ;; esac
meta=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN" \
--jq '[.path, .head_branch, .status, .html_url] | @tsv')
IFS=$'\t' read -r path branch status url <<< "$meta"
# Провенанс эталонов — завершённый Validate на dev: другой workflow
# или ветка задачи доказательством для кандидата беты не являются.
if [ "$path" != ".github/workflows/validate.yml" ] || [ "$branch" != "dev" ] || [ "$status" != "completed" ]; then
echo "::error::прогон $RUN — не завершённый Validate на dev ($path, $branch, $status)"
exit 1
fi
from="$RUNNER_TEMP/golden-candidate"
rm -rf "$from" && mkdir -p "$from"
gh run download "$RUN" --repo "${{ github.repository }}" -n golden-images -D "$from"
# Отчёт обязан быть снят с этого же дерева: accept.mjs сверяет его
# отпечаток с исходниками и откажет, если dev ушёл вперёд.
args=(--reviewed "--from=$from")
if [ -n "$EXPECT_CHANGE" ]; then args+=("--expect-change=$EXPECT_CHANGE"); fi
if [ -n "$EXPECT_NEW" ]; then args+=("--expect-new=$EXPECT_NEW"); fi
node scripts/golden-accept.mjs "${args[@]}"
if git diff --quiet -- demo/golden/baselines; then changed=false; else changed=true; fi
{ echo "changed=$changed"; echo "url=$url"; } >> "$GITHUB_OUTPUT"
git diff --stat -- demo/golden/baselines
- name: Коммит в dev
env:
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
TAG: ${{ inputs.tag }}
DOCS_CHANGED: ${{ steps.docs.outputs.changed }}
DOCS_EXPECT: ${{ inputs.docs_expect_change }}
GOLDEN_CHANGED: ${{ steps.golden.outputs.changed }}
GOLDEN_URL: ${{ steps.golden.outputs.url }}
GOLDEN_EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
GOLDEN_EXPECT_NEW: ${{ inputs.golden_expect_new }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Хук pre-push гоняет process-gate; gate:small здесь не нужен —
# продуктового кода коммит не несёт.
HP_PREPUSH_GATE: "0"
run: |
set -euo pipefail
git add -- docs/images demo/golden/baselines
if git diff --cached --quiet; then
echo "::notice::отпечаток, кадры и эталоны на dev уже свежие — коммитить нечего"
exit 0
fi
msg="$RUNNER_TEMP/message.txt"
{
echo "docs: accept derived artifacts on dev for $TAG"
echo ""
echo "Производные артефакты беты — одним коммитом на dev (PROCESS.md §8, §11.4, #697)."
echo "Прогон: $RUN_URL"
if [ "$DOCS_CHANGED" = "true" ]; then
echo "Скриншоты документации: отпечаток исходников; изменённые кадры: ${DOCS_EXPECT:-нет}."
fi
if [ "$GOLDEN_CHANGED" = "true" ]; then
echo "Golden: изменённые сцены: ${GOLDEN_EXPECT_CHANGE:-нет}; новые: ${GOLDEN_EXPECT_NEW:-нет}."
fi
echo ""
if [ "$GOLDEN_CHANGED" = "true" ]; then
echo "Release: $TAG"
echo "Baseline-Reviewed: $GOLDEN_URL"
fi
echo "Issue: #697"
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 show --stat --format='%H %s' HEAD | head -40
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev; then
echo "::error::dev ушёл вперёд за время съёмки — запустить workflow заново: отпечаток судит дерево, а оно уже другое"
exit 1
fi
echo "### Производные артефакты $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Коммит \`$(git rev-parse --short HEAD)\` в dev — проверить перед кандидатом беты." >> "$GITHUB_STEP_SUMMARY"
+333
View File
@@ -0,0 +1,333 @@
name: "Бета: пакетное ревью ship · тело (#716)"
# #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели —
# правка в механических рамках и зелёный лёгкий Validate. Их код читает одна
# сессия модели перед бетой: все ship-задачи диапазона «прошлый тег..кандидат».
# Гейт публикации беты (`ship-review.mjs check`) требует этот документ, если в
# диапазоне есть ship-задачи.
#
# Три job, как у release-review.yml (#551, #556): детерминированный вход,
# модель без права записи, детерминированная публикация документа в `dev`.
on:
# #716: тело вызывается тонким файлом `ship-review.yml` по ссылке `@dev`;
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
workflow_call:
inputs:
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 }}
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: |
doc=$(node scripts/ship-review.mjs doc --tag="$TAG")
git fetch -q --tags origin
if [ -z "$CANDIDATE" ]; then CANDIDATE=$(git rev-parse origin/dev); fi
git cat-file -e "$CANDIDATE^{commit}"
CANDIDATE=$(git rev-parse "$CANDIDATE^{commit}")
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" \
--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-задач в диапазоне нет — ревью не нужно"
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. Язык ответа — русский.
Бета: ${{ inputs.tag }} · кандидат ${{ 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
# Модель пишет только текст. Путь, машинный блок (его читает гейт беты),
# индекс и коммит решает этот шаг.
- 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 }}
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")"
high=$(jq -r '.high' "$dir/result.json")
medium=$(jq -r '.medium' "$dir/result.json")
low=$(jq -r '.low' "$dir/result.json")
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,
})));
' "$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
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F - <<MSG
docs: ship review for $TAG
Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7).
Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low.
Issue: #696
User-Visible: no
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; 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
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
sleep $((attempt * 10))
done
echo "::error::документ ревью не опубликован в dev за три попытки"
exit 1
+23 -172
View File
@@ -1,22 +1,17 @@
name: "Бета: производные артефакты на dev"
run-name: "Derived artifacts ${{ inputs.tag }}"
# #697, PROCESS.md §8 и §11.4: ветки задач не коммитят ни отпечаток и кадры
# скриншотов документации, ни эталоны golden. Их обновляет один коммит бота на
# `dev` перед кандидатом беты; коммит проверяет релиз-менеджер.
#
# - Скриншоты: съёмка на `dev` тем же каноническим способом, что
# `docs-screenshots.yml`, и приёмка `docs:accept --reviewed`. Кадры, которые
# менять не собирались, обязаны совпасть байт-в-байт (свидетели среды);
# изменившийся кадр принимается, только если назван в `docs_expect_change`.
# Ничего не изменилось — обновляется один отпечаток исходников.
# - Golden: артефакт `golden-images` полного Validate на `dev`
# (`golden_run`) и `golden:accept --reviewed` с объявленными сценами. Коммит
# несёт `Release:` и `Baseline-Reviewed:` — провенанс, который требует
# validate-commit-provenance.mjs.
#
# Необъявленная разница — отказ с перечнем кадров, артефакт съёмки остаётся
# для просмотра: решение, какие кадры сдвинулись законно, принимает человек.
# Тонкий вызывающий файл (#716, устройство #623). `workflow_dispatch` GitHub
# исполняет с выбранной ветки, но кнопку и сам запуск (`gh workflow run`)
# даёт только workflow, чей файл лежит в ветке по умолчанию (`main`). Поэтому
# здесь только то, что обязано жить там: триггер, входы ручного запуска,
# run-name, права и concurrency. Тело — `_beta-derived.yml` по ссылке `@dev`:
# правка съёмки и приёмки — один коммит в `dev`, а запуск с любой ветки (в
# кнопке по умолчанию выбрана `main`) исполняет тело из `dev`.
# Этот файл меняется, только когда меняются входы или потолок прав; тогда он
# зеркалится в `main`, и preflight `workflow_sync` (validate.yml) держит копии
# равными.
on:
workflow_dispatch:
inputs:
@@ -53,162 +48,18 @@ concurrency:
cancel-in-progress: false
jobs:
accept:
name: "Отпечаток, кадры и эталоны — одним коммитом в dev"
runs-on: ubuntu-24.04
timeout-minutes: 40
# Потолок прав тела: объединение job-level прав `_beta-derived.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- 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: Install pinned Chromium
if: steps.pw.outputs.cache-hit != 'true'
run: npx playwright install chromium
- name: Build the bundle the screenshots must come from
run: npm run build
# Тот же закреплённый упаковщик, что у docs-screenshots.yml: другой
# переписал бы все кадры без единого изменённого пикселя.
- name: Установить oxipng
env:
OXIPNG_VERSION: 10.2.0
OXIPNG_SHA256: b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e
run: |
set -euo pipefail
asset="oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL -o "$asset" \
"https://github.com/oxipng/oxipng/releases/download/v${OXIPNG_VERSION}/${asset}"
echo "${OXIPNG_SHA256} ${asset}" | sha256sum -c -
mkdir -p "$HOME/.local/bin"
tar -xzf "$asset" --strip-components=1 -C "$HOME/.local/bin" \
"oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu/oxipng"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/oxipng" --version
- name: "Съёмка воспроизводима между прогонами (#410, #422)"
run: node scripts/capture-determinism.mjs
- name: Кадры документации — съёмка и приёмка
id: docs
env:
EXPECT: ${{ inputs.docs_expect_change }}
run: |
set -euo pipefail
node demo/docs/capture.mjs --stability=3
cand="$RUNNER_TEMP/docs-candidate"
rm -rf "$cand" && mkdir -p "$cand"
cp docs/images/*.png docs/images/screenshots.json "$cand/"
# Приёмка сравнивает кандидата с ЗАКОММИЧЕННЫМИ кадрами: рабочая
# копия возвращается к dev, и заменить файлы может только она.
git checkout -- docs/images
git clean -fdq -- docs/images
args=(--reviewed "--from=$cand")
if [ -n "$EXPECT" ]; then args+=("--expect-change=$EXPECT"); fi
node scripts/docs-accept.mjs "${args[@]}"
if git diff --quiet -- docs/images; then changed=false; else changed=true; fi
echo "changed=$changed" >> "$GITHUB_OUTPUT"
git diff --stat -- docs/images
- name: Сохранить кандидата скриншотов для просмотра
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docs-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/docs-candidate
if-no-files-found: ignore
retention-days: 7
- name: Эталоны golden из прогона Validate
id: golden
if: inputs.golden_run != ''
env:
GH_TOKEN: ${{ github.token }}
RUN: ${{ inputs.golden_run }}
EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
EXPECT_NEW: ${{ inputs.golden_expect_new }}
run: |
set -euo pipefail
case "$RUN" in ''|*[!0-9]*) echo "::error::golden_run — числовой id прогона Validate"; exit 1 ;; esac
meta=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN" \
--jq '[.path, .head_branch, .status, .html_url] | @tsv')
IFS=$'\t' read -r path branch status url <<< "$meta"
# Провенанс эталонов — завершённый Validate на dev: другой workflow
# или ветка задачи доказательством для кандидата беты не являются.
if [ "$path" != ".github/workflows/validate.yml" ] || [ "$branch" != "dev" ] || [ "$status" != "completed" ]; then
echo "::error::прогон $RUN — не завершённый Validate на dev ($path, $branch, $status)"
exit 1
fi
from="$RUNNER_TEMP/golden-candidate"
rm -rf "$from" && mkdir -p "$from"
gh run download "$RUN" --repo "${{ github.repository }}" -n golden-images -D "$from"
# Отчёт обязан быть снят с этого же дерева: accept.mjs сверяет его
# отпечаток с исходниками и откажет, если dev ушёл вперёд.
args=(--reviewed "--from=$from")
if [ -n "$EXPECT_CHANGE" ]; then args+=("--expect-change=$EXPECT_CHANGE"); fi
if [ -n "$EXPECT_NEW" ]; then args+=("--expect-new=$EXPECT_NEW"); fi
node scripts/golden-accept.mjs "${args[@]}"
if git diff --quiet -- demo/golden/baselines; then changed=false; else changed=true; fi
{ echo "changed=$changed"; echo "url=$url"; } >> "$GITHUB_OUTPUT"
git diff --stat -- demo/golden/baselines
- name: Коммит в dev
env:
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
TAG: ${{ inputs.tag }}
DOCS_CHANGED: ${{ steps.docs.outputs.changed }}
DOCS_EXPECT: ${{ inputs.docs_expect_change }}
GOLDEN_CHANGED: ${{ steps.golden.outputs.changed }}
GOLDEN_URL: ${{ steps.golden.outputs.url }}
GOLDEN_EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
GOLDEN_EXPECT_NEW: ${{ inputs.golden_expect_new }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Хук pre-push гоняет process-gate; gate:small здесь не нужен —
# продуктового кода коммит не несёт.
HP_PREPUSH_GATE: "0"
run: |
set -euo pipefail
git add -- docs/images demo/golden/baselines
if git diff --cached --quiet; then
echo "::notice::отпечаток, кадры и эталоны на dev уже свежие — коммитить нечего"
exit 0
fi
msg="$RUNNER_TEMP/message.txt"
{
echo "docs: accept derived artifacts on dev for $TAG"
echo ""
echo "Производные артефакты беты — одним коммитом на dev (PROCESS.md §8, §11.4, #697)."
echo "Прогон: $RUN_URL"
if [ "$DOCS_CHANGED" = "true" ]; then
echo "Скриншоты документации: отпечаток исходников; изменённые кадры: ${DOCS_EXPECT:-нет}."
fi
if [ "$GOLDEN_CHANGED" = "true" ]; then
echo "Golden: изменённые сцены: ${GOLDEN_EXPECT_CHANGE:-нет}; новые: ${GOLDEN_EXPECT_NEW:-нет}."
fi
echo ""
if [ "$GOLDEN_CHANGED" = "true" ]; then
echo "Release: $TAG"
echo "Baseline-Reviewed: $GOLDEN_URL"
fi
echo "Issue: #697"
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 show --stat --format='%H %s' HEAD | head -40
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev; then
echo "::error::dev ушёл вперёд за время съёмки — запустить workflow заново: отпечаток судит дерево, а оно уже другое"
exit 1
fi
echo "### Производные артефакты $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Коммит \`$(git rev-parse --short HEAD)\` в dev — проверить перед кандидатом беты." >> "$GITHUB_STEP_SUMMARY"
uses: Matysh/houseplan-card/.github/workflows/_beta-derived.yml@dev # #716: тело съёмки и приёмки из dev
with:
tag: ${{ inputs.tag }}
docs_expect_change: ${{ inputs.docs_expect_change }}
golden_run: ${{ inputs.golden_run }}
golden_expect_change: ${{ inputs.golden_expect_change }}
golden_expect_new: ${{ inputs.golden_expect_new }}
secrets: inherit
+21 -302
View File
@@ -1,16 +1,17 @@
name: "Бета: пакетное ревью ship"
run-name: "Ship review ${{ inputs.tag }}"
# #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели —
# правка в механических рамках и зелёный лёгкий Validate. Их код читает одна
# сессия модели перед бетой: все ship-задачи диапазона «прошлый тег..кандидат».
# Гейт публикации беты (`ship-review.mjs check`) требует этот документ, если в
# диапазоне есть ship-задачи.
#
# Только `workflow_dispatch`: файл исполняется с ветки прогона (`--ref dev`),
# зеркало в `main` не нужно. Три job, как у release-review.yml (#551, #556):
# детерминированный вход, модель без права записи, детерминированная
# публикация документа в `dev`.
# Тонкий вызывающий файл (#716, устройство #623). `workflow_dispatch` GitHub
# исполняет с выбранной ветки, но кнопку и сам запуск (`gh workflow run`)
# даёт только workflow, чей файл лежит в ветке по умолчанию (`main`). Поэтому
# здесь только то, что обязано жить там: триггер, входы ручного запуска,
# run-name, права и concurrency. Тело — `_ship-review.yml` по ссылке `@dev`:
# правка ревью — один коммит в `dev`, а запуск с любой ветки (в кнопке по
# умолчанию выбрана `main`) исполняет тело из `dev`.
# Этот файл меняется, только когда меняются входы или потолок прав; тогда он
# зеркалится в `main`, и preflight `workflow_sync` (validate.yml) держит копии
# равными.
on:
workflow_dispatch:
inputs:
@@ -37,298 +38,16 @@ concurrency:
cancel-in-progress: false
jobs:
prepare:
name: "Ship-ревью: задачи диапазона"
runs-on: ubuntu-24.04
timeout-minutes: 10
# Потолок прав тела: объединение job-level прав `_ship-review.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
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 }}
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: |
doc=$(node scripts/ship-review.mjs doc --tag="$TAG")
git fetch -q --tags origin
if [ -z "$CANDIDATE" ]; then CANDIDATE=$(git rev-parse origin/dev); fi
git cat-file -e "$CANDIDATE^{commit}"
CANDIDATE=$(git rev-parse "$CANDIDATE^{commit}")
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" \
--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-задач в диапазоне нет — ревью не нужно"
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. Язык ответа — русский.
Бета: ${{ inputs.tag }} · кандидат ${{ 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
# Модель пишет только текст. Путь, машинный блок (его читает гейт беты),
# индекс и коммит решает этот шаг.
- 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 }}
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")"
high=$(jq -r '.high' "$dir/result.json")
medium=$(jq -r '.medium' "$dir/result.json")
low=$(jq -r '.low' "$dir/result.json")
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,
})));
' "$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
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F - <<MSG
docs: ship review for $TAG
Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7).
Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low.
Issue: #696
User-Visible: no
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; 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
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
sleep $((attempt * 10))
done
echo "::error::документ ревью не опубликован в dev за три попытки"
exit 1
uses: Matysh/houseplan-card/.github/workflows/_ship-review.yml@dev # #716: тело ревью из dev
with:
tag: ${{ inputs.tag }}
candidate: ${{ inputs.candidate }}
force: ${{ inputs.force }}
secrets: inherit
+9 -2
View File
@@ -124,9 +124,16 @@ jobs:
# ни одним событием. Тонкий файл сверяется: правка триггера или потолка
# прав, не доехавшая до `main`, действовала бы только в dev-копии.
#
# #716: `ship-review.yml` и `beta-derived.yml` — только `workflow_dispatch`,
# он исполняет файл с выбранной ветки, но запуск GitHub даёт лишь workflow,
# чей файл есть в `main`. Шагам беты он нужен до промоушена, поэтому они
# устроены так же и сверяются здесь же: правка входа или потолка прав,
# не доехавшая до `main`, не действовала бы в запуске с `main`, а именно
# его кнопка выбирает по умолчанию.
#
# `performance.yml` не входит: по расписанию он судит `main` собственным
# телом из `main`, расхождение с `dev` до промоушена законно. Список ниже
# держит равным множеству файлов с такими триггерами
# держит равным множеству тонких файлов
# test/default-branch-workflows.test.mjs.
- name: "Процесс: тонкие вызывающие workflow идентичны в main и dev"
id: workflow_sync
@@ -134,7 +141,7 @@ jobs:
run: |
git fetch --quiet origin main dev
status=0
for file in process.yml mutation-gate.yml process-resume.yml nightly.yml process-reconcile.yml process-metrics.yml; do
for file in process.yml mutation-gate.yml process-resume.yml nightly.yml process-reconcile.yml process-metrics.yml ship-review.yml beta-derived.yml; do
if diff <(git show "origin/main:.github/workflows/$file") \
<(git show "origin/dev:.github/workflows/$file"); then
echo "$file: main и dev идентичны"