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
+27 -8
View File
@@ -7,10 +7,15 @@
* доказательство для ревью — dispatch-прогон на точном SHA материала. Push-
* прогон на том же SHA зелёный не считается: в нём мутантов нет.
*
* node scripts/validate-gate.mjs --repo=<owner/repo> --ref=<ветка> --sha=<sha> [--workflow=validate.yml]
* node scripts/validate-gate.mjs --repo=<owner/repo> --ref=<ветка> --sha=<sha> [--workflow=validate.yml] [--no-wait]
*
* Печатает `result=green|failed|missing` и `url=…` (и в $GITHUB_OUTPUT, если он
* задан); код выхода 0 только при green. Логика — чистая функция `validateGate`
* Печатает `result=green|failed|missing|pending`, `url=…`, `run_id=…` (и в
* $GITHUB_OUTPUT, если он задан); код выхода 0 только при green, 2 — pending.
* `--no-wait` (#636): гейт диспатчит прогон и убеждается, что тот появился на
* материале, но не ждёт его завершения — раннер конвейера освобождается, а
* раунд продолжает `process-resume.yml` по событию `workflow_run` (страховка —
* reconcile). Зелёный или красный завершённый прогон и с `--no-wait`
* возвращается сразу. Логика — чистая функция `validateGate`
* поверх инъектируемых `ops`, чтобы тесты и мутанты гоняли её без gh.
*/
import { spawnSync } from 'node:child_process';
@@ -58,9 +63,14 @@ export function provesMutants(jobs) {
* @param {string} p.ref ветка, на которой запускать
* @param {string} p.sha SHA материала
* @param {object} p.ops GitHub run/proof operations plus dispatch, sleep and clock.
* @returns {Promise<{result:'green'|'failed'|'missing', url:string|null, note:string}>}
* @param {boolean} [p.wait] `false` — не ждать идущий прогон, а вернуть `pending` (#636):
* раннер конвейера не спит 28 минут; продолжение разбудит событие
* `workflow_run` (process-resume.yml) либо reconcile.
* @returns {Promise<{result:'green'|'failed'|'missing'|'pending', url:string|null, note:string, runId?:number}>}
*/
export async function validateGate({ ref, sha, ops, appearMs = VALIDATE_APPEAR_MS, totalMs = VALIDATE_TOTAL_MS, pollMs = POLL_MS }) {
export async function validateGate({
ref, sha, ops, appearMs = VALIDATE_APPEAR_MS, totalMs = VALIDATE_TOTAL_MS, pollMs = POLL_MS, wait = true,
}) {
const started = ops.now();
const candidateTree = await ops.candidateTree(sha);
const ignored = new Set(); // завершённые dispatch без применимого proof
@@ -84,6 +94,13 @@ export async function validateGate({ ref, sha, ops, appearMs = VALIDATE_APPEAR_M
tracked = null;
continue;
}
if (!wait) {
// #636: прогон найден и идёт — ждать его будет событие, не раннер.
return {
result: 'pending', url: run.url || null, runId: run.databaseId,
note: `Validate с мутантами идёт (${run.status}); продолжение — по завершении прогона`,
};
}
} else if (dispatchedAt === null) {
await ops.dispatch(ref);
dispatchedAt = ops.now();
@@ -151,9 +168,11 @@ if (invokedDirectly) {
console.error('usage: validate-gate.mjs --repo=<owner/repo> --ref=<branch> --sha=<sha> [--workflow=validate.yml]');
process.exit(2);
}
const outcome = await validateGate({ ref, sha, ops: realOps({ repo, workflow: arg('workflow') || 'validate.yml' }) });
const lines = [`result=${outcome.result}`, `url=${outcome.url || ''}`, `note=${outcome.note}`];
const wait = !process.argv.includes('--no-wait');
const outcome = await validateGate({ ref, sha, wait, ops: realOps({ repo, workflow: arg('workflow') || 'validate.yml' }) });
const lines = [`result=${outcome.result}`, `url=${outcome.url || ''}`, `run_id=${outcome.runId || ''}`, `note=${outcome.note}`];
for (const line of lines) console.log(line);
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `${lines.join('\n')}\n`);
process.exit(outcome.result === 'green' ? 0 : 1);
// 0 — зелёный, 2 — идёт (только с --no-wait), 1 — красный/пропавший.
process.exit(outcome.result === 'green' ? 0 : outcome.result === 'pending' ? 2 : 1);
}