ci(process): раунд ревью ждёт Validate событием, а не сном раннера (#636)

Стадия prepare спала ≈ 28 минут на раунд, пока шёл Validate с мутантами на
материале (модель работает 10–12); за неделю ≈ 420–500 job-минут простоя и
потолок бюджета стадии 55 минут.

- validate-gate.mjs: `--no-wait` — гейт диспатчит прогон, убеждается, что тот
  встал на материал (#539 сохранён), и возвращает `pending` (код 2) вместо
  ожидания; завершённый зелёный/красный отдаёт сразу, как прежде.
- process.yml prepare: третий исход `proceed=pending`: запечатанный маркер
  `review-pending-<issue>-<run>-<attempt>` (issue, stage, branch, material_sha,
  validate run) и выход; модель и интеграция не запускаются; возврат автору —
  только на явном `false`.
- process-resume.yml + scripts/process-resume.mjs: на `workflow_run: completed`
  Validate по ветке issue/* — если метка S7 стоит, активного прогона нет и
  последний прогон оставил маркер на этот SHA, переставить S7 (HP_PROCESS_TOKEN);
  новый прогон находит завершённый dispatch сразу. Без маркера не будит.
- process-reconcile.mjs: читает маркер и состояние Validate на материале;
  идёт — wait, завершился/пропал без продолжения — retry; без маркера — прежний
  escalate. Общий loadSealedArtifact, экспорт processRuns/artifactNames.
- preflight сверяет process-resume.yml между main и dev наравне с process.yml.
- Тесты: validate-gate (4), process-resume (8, включая контракт трёх workflow),
  process-reconcile (2); мутанты gate-no-wait-still-sleeps,
  resume-wakes-round-without-marker, resume-ignores-active-run,
  reconcile-wakes-pending-while-validate-active. PROCESS.md §10.4, AGENTS.md.

Issue: #636
User-Visible: no
This commit is contained in:
Claude
2026-09-23 08:51:17 +03:00
parent e29dfdeb72
commit 351fef43d6
14 changed files with 580 additions and 32 deletions
+60
View File
@@ -0,0 +1,60 @@
name: Продолжение ревью после Validate
run-name: "resume · ${{ github.event.workflow_run.head_branch }} · ${{ github.event.workflow_run.conclusion }}"
# #636. Стадия `prepare` конвейера (process.yml) больше не ждёт Validate с
# мутантами на материале внутри job — раннер спал ≈ 28 минут на раунд при
# 10–12 минутах работы модели. Она диспатчит прогон, кладёт запечатанный
# маркер `review-pending-…` и выходит. Этот workflow просыпается на завершение
# любого Validate и, если раунд ждал именно этот прогон (маркер на материале,
# метка S7 стоит, активного прогона конвейера нет), переставляет метку S7 —
# новый прогон `prepare` находит завершённый dispatch и продолжает раунд.
# Ничего не оценивает: зелёный/красный разбирает сам конвейер. Страховка на
# потерянное событие — process-reconcile.yml с тем же маркером.
#
# Как и process.yml, файл исполняется из ветки по умолчанию (main): для
# события `workflow_run` GitHub берёт workflow только оттуда. Сверка копий —
# в preflight validate.yml.
on:
workflow_run:
workflows: ["Проверка (CI)"]
types: [completed]
permissions:
contents: read
actions: read
jobs:
resume:
name: "Разбудить раунд, ждавший этот Validate"
if: github.event.workflow_run.event == 'workflow_dispatch' && startsWith(github.event.workflow_run.head_branch, 'issue/')
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: process-resume-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
steps:
# Код берётся из dev, как у reconcile: после штатного слияния действует
# версия, которую проверил CI, а не копия из main.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
# Метка переставляется HP_PROCESS_TOKEN: событие от GITHUB_TOKEN не
# запустило бы process.yml (см. шапку process.yml, п. 1).
- name: Решить по маркеру ожидания и переставить S7
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
SHA: ${{ github.event.workflow_run.head_sha }}
EVENT: ${{ github.event.workflow_run.event }}
STATUS: ${{ github.event.workflow_run.status }}
run: |
node scripts/process-resume.mjs \
--repo="$REPO" --branch="$BRANCH" --sha="$SHA" \
--event="$EVENT" --status="$STATUS" --apply=true | tee -a "$GITHUB_STEP_SUMMARY"
+61 -6
View File
@@ -582,14 +582,64 @@ jobs:
{ echo 'proceed=true'; echo 'result=skipped'; } >> "$GITHUB_OUTPUT"
exit 0
fi
if node scripts/validate-gate.mjs --repo="${{ github.repository }}" --ref="$BRANCH" --sha="$SHA"; then
echo 'proceed=true' >> "$GITHUB_OUTPUT"
else
echo 'proceed=false' >> "$GITHUB_OUTPUT"
fi
# #636: раннер не спит, пока идёт Validate (28 минут на раунд при
# 10–12 минутах работы модели). Гейт диспатчит прогон, убеждается, что
# тот встал на материал, и выходит с кодом 2 — «идёт». Раунд продолжит
# событие завершения Validate (process-resume.yml переставит метку
# S7), страховка — process-reconcile. Зелёный или красный завершённый
# прогон гейт и без ожидания возвращает сразу.
set +e
node scripts/validate-gate.mjs --repo="${{ github.repository }}" --ref="$BRANCH" --sha="$SHA" --no-wait
code=$?
set -e
case "$code" in
0) echo 'proceed=true' >> "$GITHUB_OUTPUT" ;;
2) echo 'proceed=pending' >> "$GITHUB_OUTPUT" ;;
*) echo 'proceed=false' >> "$GITHUB_OUTPUT" ;;
esac
# #636: прогон на материале идёт — записать маркер ожидания и освободить
# раннер. Метка S7 остаётся; событие `workflow_run` по завершении Validate
# переставит её, и новый прогон конвейера найдёт завершённый dispatch
# сразу. Маркер читают process-resume.mjs и process-reconcile.mjs: без
# него ни один из них не имеет права будить раунд — иначе «успешный
# прогон без вердикта» неотличим от потерянного запроса.
- name: Validate идёт — раунд продолжит событие
id: pending
if: steps.rebase.outputs.conflict != 'true' && steps.gate.outputs.proceed == 'pending'
env:
NUM: ${{ github.event.issue.number }}
STAGE: ${{ needs.guard.outputs.stage }}
BRANCH: ${{ steps.branch.outputs.name }}
SHA: ${{ steps.material.outputs.sha }}
VALIDATE_RUN_ID: ${{ steps.gate.outputs.run_id }}
VALIDATE_URL: ${{ steps.gate.outputs.url }}
run: |
dir="$RUNNER_TEMP/review-pending"
mkdir -p "$dir"
jq -n -S \
--arg run_id "$GITHUB_RUN_ID" --arg run_attempt "$GITHUB_RUN_ATTEMPT" \
--arg issue "$NUM" --arg stage "$STAGE" --arg branch "$BRANCH" \
--arg material_sha "$SHA" --arg validate_run_id "$VALIDATE_RUN_ID" \
--arg validate_url "$VALIDATE_URL" \
'{schema:1,run_id:$run_id,run_attempt:$run_attempt,issue:$issue,stage:$stage,branch:$branch,material_sha:$material_sha,validate_run_id:$validate_run_id,validate_url:$validate_url}' \
> "$dir/pending.json"
(cd "$dir" && sha256sum pending.json > manifest.sha256)
echo "artifact=review-pending-${NUM}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
short=$(git rev-parse --short "$SHA")
echo "Validate с мутантами на \`$short\` идёт — раннер освобождён, раунд продолжится по завершении прогона${VALIDATE_URL:+ ($VALIDATE_URL)}." >> "$GITHUB_STEP_SUMMARY"
- name: Сохранить маркер ожидания
if: steps.gate.outputs.proceed == 'pending'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ steps.pending.outputs.artifact }}
path: ${{ runner.temp }}/review-pending
if-no-files-found: error
retention-days: 1
- name: Validate красный — вернуть автору без ревью
if: steps.rebase.outputs.conflict != 'true' && steps.gate.outputs.proceed != 'true'
if: steps.rebase.outputs.conflict != 'true' && steps.gate.outputs.proceed == 'false'
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
NUM: ${{ github.event.issue.number }}
@@ -1181,6 +1231,11 @@ jobs:
echo "::error::стадия deterministic prerequisites завершилась: $PREPARE_RESULT"
exit 1
fi
if [ "$PROCEED" = "pending" ]; then
echo "Validate на материале ещё идёт — раунд продолжит событие завершения (#636); интегрировать нечего"
echo "proceed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$PROCEED" != "true" ]; then
echo "подготовка уже вернула задачу автору; интегрировать нечего"
echo "proceed=false" >> "$GITHUB_OUTPUT"
+3 -2
View File
@@ -101,9 +101,10 @@ jobs:
run: |
git fetch --quiet origin main dev
# #472: расписание mutation-gate.yml тоже исполняется из ветки по
# умолчанию — та же ловушка, что у process.yml. Сверяются оба.
# умолчанию — та же ловушка, что у process.yml; #636 добавил
# process-resume.yml (событие workflow_run). Сверяются все три.
status=0
for file in process.yml mutation-gate.yml; do
for file in process.yml mutation-gate.yml process-resume.yml; do
if diff <(git show "origin/main:.github/workflows/$file") \
<(git show "origin/dev:.github/workflows/$file"); then
echo "$file: main и dev идентичны"