fix(process): prepare and the usage step run pipeline scripts from one pinned dev snapshot (#765)

The body of _process.yml is read from dev (@dev, #623). After "Перейти на
ветку задачи" the working copy of job prepare is the task branch, and a
show/ship branch with a clean merge is not rebased before review: its
scripts/ may lag dev or be replaced. #749 fixed job integrate; prepare
still ran four control scripts from the material — the issue-body digest
for the anchor, --reuse of a green verdict (#499), validate-gate (#510)
and the spec-change check (#517). The material decided its own admission:
a branch whose review-doc-guard.mjs prints reuse=true merges without the
model. model_review took model-usage.mjs from the material too: a lagging
branch has none, and the publication silently wrote reason=missing.

Now prepare extracts one snapshot right after setup-node, before the
branch switch: `git rev-parse origin/dev` once and `git archive <sha>
scripts .github/workflows/validate.yml`, so the git fetch of the track and
rebase steps cannot mix versions. Every repo script of the job runs from
it via TOOLS — the track step and the rebase guard lose their own
extractions. The SHA goes out as job output tools_sha; the usage step of
model_review archives the same commit inside itself, so a snapshot failure
is a failure of the reporting step (continue-on-error), not of the stage.
The model session runs on that runner, so the usage line stays untrusted
input parsed strictly (#556, #737).

withMaterialAnchors is idempotent: a repeated call drops the separator the
previous call wrote instead of piling up `---` lines.

Tests: test/process-prepare-tools.test.mjs — the job contract (no step
calls scripts/ from the working copy, one pinned archive before the branch
switch, tools_sha reaches model_review) and the steps as they are, on real
bash and git: a branch behind dev without model-usage.mjs and with a
substituted review-doc-guard.mjs; the anchor digest, reuse and the spec
check come from dev, the usage line is data even after dev moved. Red on
the old workflow: all four. Harnesses of process-track, rebase-generated,
review-doc-guard and model-usage take the prepare snapshot. Three registry
mutants (reuse from the material, usage from moving dev, separators).

Canon: PROCESS.md §10.4 «Скрипты конвейера — из dev» covers prepare and
the usage step; «Расход модели» names it a pipeline step, not the reviewer's.

Issue: #765
User-Visible: no
This commit is contained in:
claude[bot]
2026-10-02 10:18:29 +03:00
parent ceee249210
commit 23f822475b
10 changed files with 462 additions and 49 deletions
+19 -6
View File
@@ -199,9 +199,19 @@ test('#737 lastUsageIn: последняя строка формата в тек
// ---------- К3: проводка в обоих workflow ----------
const WORKFLOWS = fileURLToPath(new URL('../.github/workflows/', import.meta.url));
// #765: в _process.yml рабочая копия model_review — материал ревью (ветка задачи
// может отставать от dev или подменять скрипт), поэтому скрипт берётся снимком
// того коммита dev, что закрепила подготовка (`tools_sha`). В _ship-review.yml
// рабочая копия — кандидат линии dev, и скрипт зовётся из неё.
const PIPELINES = [
{ file: '_process.yml', publishJob: 'integrate', publishStep: 'Опубликовать документ ревью', consumes: /^ +--usage="\$USAGE" \\$/m },
{ file: '_ship-review.yml', publishJob: 'publish', publishStep: 'Опубликовать документ', consumes: /^ +usage: process\.env\.USAGE \?\? "",$/m },
{
file: '_process.yml', publishJob: 'integrate', publishStep: 'Опубликовать документ ревью', consumes: /^ +--usage="\$USAGE" \\$/m,
call: /^line=\$\(node "\$tools\/scripts\/model-usage\.mjs" --execution-file="\$EXEC"\)$/m, snapshot: true,
},
{
file: '_ship-review.yml', publishJob: 'publish', publishStep: 'Опубликовать документ', consumes: /^ +usage: process\.env\.USAGE \?\? "",$/m,
call: /^line=\$\(node scripts\/model-usage\.mjs --execution-file="\$EXEC"\)$/m, snapshot: false,
},
];
/** Блок job верхнего уровня `jobs:` — до следующего id на двух пробелах. */
@@ -234,7 +244,7 @@ const named = (steps, name) => {
const hasBash = () => process.platform !== 'win32' && spawnSync('bash', ['--version']).status === 0;
test('#737 AC5: шаг снятия расхода сразу после Review — always, continue-on-error, выход job usage; публикация его берёт', () => {
for (const { file, publishJob, publishStep, consumes } of PIPELINES) {
for (const { file, publishJob, publishStep, consumes, call } of PIPELINES) {
const text = readFileSync(join(WORKFLOWS, file), 'utf8');
const model = jobBlock(text, 'model_review');
const steps = stepsOf(model);
@@ -247,7 +257,7 @@ test('#737 AC5: шаг снятия расхода сразу после Review
assert.match(usage, /^ {8}continue-on-error: true$/m, `${file}: сбой снятия не роняет стадию`);
assert.match(usage, /^ {10}EXEC: \$\{\{ steps\.review\.outputs\.execution_file \}\}$/m, file);
const run = runOf(usage);
assert.match(run, /^line=\$\(node scripts\/model-usage\.mjs --execution-file="\$EXEC"\)$/m, file);
assert.match(run, call, file);
assert.match(run, /^echo "line=\$line" >> "\$GITHUB_OUTPUT"$/m, file);
assert.match(run, /"\$GITHUB_STEP_SUMMARY"$/m, `${file}: строка — в сводку шага`);
const head = model.slice(0, model.indexOf('\n steps:\n'));
@@ -281,7 +291,9 @@ test('#737 AC5: execution_file никуда не выгружается, гра
test('#737 AC5: шаг снятия расхода на настоящем bash — выход line и строка в сводке, секрета нет', (t) => {
if (!hasBash()) { t.skip('bash недоступен'); return; }
for (const { file } of PIPELINES) {
// #765: снимок — коммит HEAD этого дерева (скрипт расхода в нём тот же).
const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' }).stdout.trim();
for (const { file, snapshot } of PIPELINES) {
const text = readFileSync(join(WORKFLOWS, file), 'utf8');
const body = runOf(named(stepsOf(jobBlock(text, 'model_review')), 'Снять расход модели'));
const dir = tempDir(t);
@@ -293,7 +305,8 @@ test('#737 AC5: шаг снятия расхода на настоящем bash
rmSync(output, { force: true });
rmSync(summary, { force: true });
const r = spawnSync('bash', ['--noprofile', '--norc', '-e', '-c', body], {
cwd: ROOT, encoding: 'utf8', env: { ...process.env, EXEC, GITHUB_OUTPUT: output, GITHUB_STEP_SUMMARY: summary },
cwd: ROOT, encoding: 'utf8',
env: { ...process.env, EXEC, GITHUB_OUTPUT: output, GITHUB_STEP_SUMMARY: summary, ...(snapshot ? { TOOLS_SHA: head, RUNNER_TEMP: dir } : {}) },
});
assert.equal(r.status, 0, `${file}: ${r.stderr}`);
assert.equal(readFileSync(output, 'utf8'), `line=${line}\n`, file);