fix(process): integrate runs every pipeline script from one dev snapshot (#749)

The body of _process.yml is read from dev (@dev, #623), so the flags and
formats it passes to scripts are dev's. After "Опубликовать документ ревью"
the working copy of job integrate is the task branch, and a show/ship branch
with a clean merge is not rebased before review: its scripts/ may lag dev by
days. review-doc-guard.mjs silently ignores unknown flags (the anchor lost
#726 route and #737 usage), and a stale merge-candidate.mjs merges the old
way. Only two calls (#723 push refusal, #726 route) were taken from dev, each
with its own extraction, and on ship/reuse the remaining ones ran dev's
version anyway: the script version depended on the path.

Now one step right after setup-node extracts
`git archive origin/dev scripts .github/workflows/validate.yml` into
$RUNNER_TEMP/dev-tools and every repo script of the job runs from there via
TOOLS (review-result-gate, review-doc-guard, reviews-index, merge-candidate,
process-track route, status-label). validate.yml is part of the snapshot
because workflow-jobs.mjs reads it relative to itself; without it ci-proof
answers `failed (#622)` and every code merge would return to S6. The working
copy stays the material: git, the document and paths are judged there.

PROCESS.md §10.4 gets the paragraph "Скрипты конвейера — из dev": the
model_review exception, merges of pipeline changes judged by dev's version,
and compatible edits of the Validate proof contract.

Tests: test/process-integrate-tools.test.mjs is the job contract (no step
calls scripts/ from the working copy, every call goes through the snapshot,
one archive from origin/dev with validate.yml, and the step as is yields a
directory where ci-proof resolves the job contract); publish-push-refusal
runs the publish step and the #413 step on real bash with a task branch whose
review-doc-guard.mjs exits 7 (red with the old call). Existing harnesses take
the snapshot step before the publish and decide steps; the #706 mutant anchor
follows the status-label call.

Issue: #749
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 15:35:16 +00:00
committed by claude[bot]
parent 5c0fe8e79f
commit ff4e096858
10 changed files with 351 additions and 45 deletions
+53 -29
View File
@@ -1430,6 +1430,28 @@ jobs:
with:
node-version: 22
# #749: тело читается из dev (@dev, #623) — значит, и скрипты, которые
# оно зовёт со своими флагами и форматами, тоже из dev. Рабочая копия
# после публикации документа — ветка задачи, а ветка show/ship с чистым
# слиянием до ревью не ребейзится (§10.4) и несёт scripts/, отставшие на
# дни: неизвестный флаг review-doc-guard.mjs молча теряется, старый
# merge-candidate.mjs сливает по-старому. Рабочая копия — материал, а не
# инструмент: git, документ и пути судятся в ней, скрипты зовутся отсюда.
# Снимок один на job — одна версия на весь заход, а не смесь по шагам.
# validate.yml в снимке обязателен: workflow-jobs.mjs читает его по пути
# от себя, и без него ci-proof.mjs отвечает `failed (#622)` на каждом
# слиянии кода.
- name: Скрипты конвейера — из dev (#749)
id: tools
if: steps.ready.outputs.proceed == 'true'
run: |
set -euo pipefail
tools="$RUNNER_TEMP/dev-tools"
rm -rf "$tools" && mkdir -p "$tools"
git fetch -q origin dev
git archive origin/dev scripts .github/workflows/validate.yml | tar -x -C "$tools"
echo "dir=$tools" >> "$GITHUB_OUTPUT"
- name: Получить результат модели
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' && needs.prepare.outputs.ship != 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
@@ -1462,10 +1484,11 @@ jobs:
SPEC_BODY_CHANGED: ${{ needs.prepare.outputs.spec_body_changed }}
SPEC_BODY_DOC: ${{ needs.prepare.outputs.spec_body_doc }}
SPEC_BODY_RECORDED: ${{ needs.prepare.outputs.spec_body_recorded }}
TOOLS: ${{ steps.tools.outputs.dir }}
run: |
set -euo pipefail
dir="$RUNNER_TEMP/review-result"
node scripts/review-result-gate.mjs --dir="$dir"
node "$TOOLS/scripts/review-result-gate.mjs" --dir="$dir"
printf 'structured_output<<EOF_RESULT\n' >> "$GITHUB_OUTPUT"
cat "$dir/verdict.json" >> "$GITHUB_OUTPUT"
# structured_output не обязан оканчиваться LF: delimiter команды
@@ -1494,6 +1517,8 @@ jobs:
# #737: расход модели — последней строкой блока якорей; строка
# недоверенная, скрипт разбирает её строго.
USAGE: ${{ needs.model_review.outputs.usage }}
# #749: скрипты — из снимка dev, рабочая копия — ветка задачи.
TOOLS: ${{ steps.tools.outputs.dir }}
run: |
verdict=$(printf '%s' "$OUT" | jq -r '.verdict // empty' 2>/dev/null || true)
high=$(printf '%s' "$OUT" | jq -r '.high // empty' 2>/dev/null || true)
@@ -1552,7 +1577,7 @@ jobs:
# выводом — через раунд команда из §2.10 не работала. Машина же
# снимает якоря в момент чтения материала и ошибиться в них не
# может; блок помечен как машинный, чтобы никто не правил его руками.
node scripts/review-doc-guard.mjs --anchor="$doc" \
node "$TOOLS/scripts/review-doc-guard.mjs" --anchor="$doc" \
--sha="$MATERIAL_SHA" --tree="$MATERIAL_TREE" \
--branch="${BRANCH:-dev}" --specs="$MATERIAL_SPECS" \
--issue-body="$MATERIAL_ISSUE_BODY" \
@@ -1571,7 +1596,7 @@ jobs:
# задачи — один документ: индекс там конфликтовал бы с документами
# соседей в dev по построению; его пересоберёт слияние кандидата.
if [ -f "$doc" ] && [ "$target" = "dev" ]; then
node scripts/reviews-index.mjs --dir=docs/reviews
node "$TOOLS/scripts/reviews-index.mjs" --dir=docs/reviews
git add -- docs/reviews/INDEX.md
fi
if git diff --cached --quiet; then
@@ -1592,7 +1617,7 @@ jobs:
exit 1
fi
# Первый рубеж: что вообще проиндексировано.
git diff --cached --name-only | node scripts/review-doc-guard.mjs
git diff --cached --name-only | node "$TOOLS/scripts/review-doc-guard.mjs"
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#723).
msg="$RUNNER_TEMP/review-doc-commit.txt"
{
@@ -1607,25 +1632,21 @@ jobs:
# Второй рубеж, и он главный: что пуш ДОБАВИТ в целевую ветку. Первый
# судит намерение шага, этот — результат, а расходились они именно
# тогда, когда база оказывалась не той.
git diff --name-only "origin/$target...HEAD" | node scripts/review-doc-guard.mjs
git diff --name-only "origin/$target...HEAD" | node "$TOOLS/scripts/review-doc-guard.mjs"
# Публикация в dev идёт из детачнутого состояния поверх ветки задачи
# либо dev, поэтому push нужен с явным перебазированием при гонке:
# dev мог уйти вперёд, пока шло ревью — оно длится до 45 минут.
#
# #723: гонкой считается только устаревший lease. Отказ разбирает код
# слияния (merge-candidate.mjs --push-refusal, #705) — из dev, как у
# стража ребейза: ветка, отставшая от dev, его может не нести. Отказ
# GitHub (право на workflow, правило ветки, хук) ребейз не лечит:
# шаг падает сразу, причина и ответ git без токена — в журнале и в
# сводке шага; метка не меняется.
tools="$RUNNER_TEMP/publish-tools"
rm -rf "$tools" && mkdir -p "$tools"
git fetch -q origin dev
git archive origin/dev scripts | tar -x -C "$tools"
# слияния (merge-candidate.mjs --push-refusal, #705) — из снимка dev
# (#749): ветка, отставшая от dev, его может не нести. Отказ GitHub
# (право на workflow, правило ветки, хук) ребейз не лечит: шаг падает
# сразу, причина и ответ git без токена — в журнале и в сводке шага;
# метка не меняется.
push_err="$RUNNER_TEMP/review-doc-push.stderr"
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
"HEAD:$target" 2> "$push_err"; then
kind=$(node "$tools/scripts/merge-candidate.mjs" --push-refusal="$push_err" --ref="$target" \
kind=$(node "$TOOLS/scripts/merge-candidate.mjs" --push-refusal="$push_err" --ref="$target" \
--stage=review-doc --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
if [ "$kind" != "stale" ]; then
echo "::error::push документа ревью в $target отклонён ($kind) — это не сдвиг $target, ребейз не поможет; причина и ответ git — выше и в сводке шага"
@@ -1642,10 +1663,10 @@ jobs:
fi
# После ребейза набор путей другой — проверяется заново. Форс здесь
# запрещён и не появляется: ветка двигается только вперёд.
git diff --name-only "origin/$target...HEAD" | node scripts/review-doc-guard.mjs
git diff --name-only "origin/$target...HEAD" | node "$TOOLS/scripts/review-doc-guard.mjs"
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
"HEAD:$target" 2> "$push_err"; then
kind=$(node "$tools/scripts/merge-candidate.mjs" --push-refusal="$push_err" --ref="$target" \
kind=$(node "$TOOLS/scripts/merge-candidate.mjs" --push-refusal="$push_err" --ref="$target" \
--stage=review-doc --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
echo "::error::документ ревью не опубликован в $target и после ребейза ($kind); причина и ответ git — выше"
exit 1
@@ -1688,6 +1709,7 @@ jobs:
STAGE: ${{ needs.guard.outputs.stage }}
CYCLE: ${{ needs.guard.outputs.cycle }}
BRANCH: ${{ needs.prepare.outputs.branch }}
TOOLS: ${{ steps.tools.outputs.dir }}
run: |
marker=CODE-REVIEW
if [ "$STAGE" = "spec" ]; then marker=SPEC-REVIEW; fi
@@ -1696,7 +1718,7 @@ jobs:
git fetch -q origin "$target"
# Судится опубликованная версия, а не рабочая копия: именно её прочтёт
# следующий раунд.
git show "origin/$target:$doc" | node scripts/review-doc-guard.mjs --doc=-
git show "origin/$target:$doc" | node "$TOOLS/scripts/review-doc-guard.mjs" --doc=-
- name: Решение по вердикту
id: decide
if: steps.ready.outputs.proceed == 'true'
@@ -1723,6 +1745,7 @@ jobs:
LIMIT: ${{ needs.guard.outputs.limit }}
CYCLE: ${{ needs.guard.outputs.cycle }}
BRANCH: ${{ needs.prepare.outputs.branch }}
TOOLS: ${{ steps.tools.outputs.dir }}
run: |
# ship и повторно применённый зелёный (#499) модель не вызывали:
# маршрута у них нет, решение прежнее — зелёный двигает вперёд.
@@ -1769,18 +1792,14 @@ jobs:
# жёлтый и красный — цикл и возврат автору, на код-ревью show с
# `route: reclassify` — повышение до ask или вопрос владельцу.
# Bash только исполняет: комментарий — из файла скрипта, метки —
# по его выходу. Скрипт — из dev, как у шага трека: рабочая копия
# здесь — ветка задачи после публикации, и ветка show/ship,
# отставшая от dev, его может ещё не нести. Метки — текущие: шаг
# трека мог в этом же прогоне сменить ship на show.
git fetch -q origin dev
tools="$RUNNER_TEMP/route-tools"
rm -rf "$tools" && mkdir -p "$tools"
git archive origin/dev scripts | tar -x -C "$tools"
# по его выходу. Скрипт — из снимка dev (#749): рабочая копия здесь —
# ветка задачи после публикации, и ветка show/ship, отставшая от dev,
# его может ещё не нести. Метки — текущие: шаг трека мог в этом же
# прогоне сменить ship на show.
printf '%s' "$OUT" > "$RUNNER_TEMP/route-verdict.json"
labels=$(gh issue view "$NUM" --repo "${{ github.repository }}" --json labels \
--jq '[.labels[].name] | join(",")') || labels="$LABELS"
route=$(node "$tools/scripts/process-track.mjs" route --stage="$STAGE" --track="$TRACK" \
route=$(node "$TOOLS/scripts/process-track.mjs" route --stage="$STAGE" --track="$TRACK" \
--confirmed="$CONFIRMED" --labels="$labels" --verdict="$RUNNER_TEMP/route-verdict.json" \
--spent="$SPENT" --limit="$LIMIT" --num="$NUM" --cycle="$CYCLE" --branch="$BRANCH" \
--out="$RUNNER_TEMP/route" \
@@ -1857,13 +1876,17 @@ jobs:
MATERIAL_SHA: ${{ needs.prepare.outputs.material_sha }}
# #696: show/ship сливаются по лёгкому Validate на кандидате.
MUTANTS: ${{ needs.prepare.outputs.mutants }}
# #749: из снимка dev — и суждение о Validate кандидата (ci-proof.mjs
# с validate.yml снимка), и reviews-index.mjs, который скрипт зовёт
# по пути от себя.
TOOLS: ${{ steps.tools.outputs.dir }}
run: |
if [ -z "$BRANCH" ]; then
echo "::error::ветки задачи нет — сливать нечего"
echo "merged=false" >> "$GITHUB_OUTPUT"
exit 0
fi
node scripts/merge-candidate.mjs --branch="$BRANCH" --material="$MATERIAL_SHA" \
node "$TOOLS/scripts/merge-candidate.mjs" --branch="$BRANCH" --material="$MATERIAL_SHA" \
--issue="$NUM" --repo="${{ github.repository }}" --mutants="${MUTANTS:-false}"
- name: Переставить метку
@@ -1873,6 +1896,7 @@ jobs:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
NUM: ${{ github.event.issue.number }}
FROM: ${{ steps.decide.outputs.from }}
TOOLS: ${{ steps.tools.outputs.dir }}
# Зелёное код-ревью без слияния ведёт не в S8-merged, а обратно к
# автору: метка утверждала бы, что код в dev, а его там нет.
# Точный кандидат (#492) сам называет исход: S8 после push, S6/S7 —
@@ -1884,7 +1908,7 @@ jobs:
# статуса. Скрипт снимает и ставит её заново, чтобы событие `labeled`
# запустило новый заход.
run: |
node scripts/status-label.mjs --repo="${{ github.repository }}" \
node "$TOOLS/scripts/status-label.mjs" --repo="${{ github.repository }}" \
--issue="$NUM" --from="$FROM" --to="$TO"
- name: Сводка длительности стадий