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
+13 -6
View File
@@ -137,10 +137,12 @@ test('конвейер: трек снимается до ребейза, мут
assert.ok(branch < track && track < rebase && rebase < gate, 'трек — после выбора ветки и до ребейза');
const trackStep = workflow.slice(track, rebase);
assert.match(trackStep, /LABELS: \$\{\{ needs\.guard\.outputs\.labels \}\}/, 'метки — текущие, из guard');
assert.match(trackStep, /git archive origin\/dev scripts \| tar -x -C "\$tools"/, 'скрипт — из dev: ветка show/ship не ребейзится');
// #765: скрипт — из снимка dev подготовки: ветка show/ship не ребейзится.
assert.match(trackStep, /TOOLS: \$\{\{ steps\.tools\.outputs\.dir \}\}/, 'скрипт — из снимка dev: ветка show/ship не ребейзится');
assert.doesNotMatch(trackStep, /git archive/, 'своего извлечения у шага нет — один снимок на заход');
// #707: один вызов скрипта решает трек, рамки ship и риск; bash только исполняет.
assert.equal((trackStep.match(/process-track\.mjs/g) || []).length, 1, 'скрипт трека вызывается один раз');
assert.match(trackStep, /node "\$tools\/scripts\/process-track\.mjs" stage --stage="\$STAGE" --labels="\$LABELS" \\\n\s+--branch="\$BRANCH" --base=origin\/dev --head=HEAD --comments="\$comments" --owner="\$OWNER"/);
assert.match(trackStep, /node "\$TOOLS\/scripts\/process-track\.mjs" stage --stage="\$STAGE" --labels="\$LABELS" \\\n\s+--branch="\$BRANCH" --base=origin\/dev --head=HEAD --comments="\$comments" --owner="\$OWNER"/);
assert.match(trackStep, /if printf '%s\\n' "\$out" \| grep -qx 'raise=true'; then\n\s+gh issue comment "\$NUM" --repo "\$\{\{ github\.repository \}\}" --body-file "\$RUNNER_TEMP\/track\/raise\.md"/,
'комментарий повышения — из файла скрипта, только по его флагу');
assert.match(trackStep, /--add-label track:show --remove-label track:ship/, 'выход за рамки повышает трек');
@@ -652,6 +654,8 @@ const TRACK_STEP = ' - name: "Трек задачи и рамки ship (#69
const GUARD_STEP = ' - id: decide\n';
const DECIDE_STEP = ' - name: Решение по вердикту\n';
const TOOLS_STEP = ' - name: Скрипты конвейера — из dev (#749)\n';
// #765: снимок подготовки — шаги prepare (трек, ребейз) зовут скрипты из него.
const PREPARE_TOOLS_STEP = ' - name: Скрипты конвейера — из dev (#765)\n';
const PUBLISH_STEP = ' - name: Опубликовать документ ревью\n';
test('#707 AC4: изменённые run шага трека, guard и решения по вердикту проходят bash -n', async (t) => {
@@ -785,7 +789,7 @@ function trackSandbox(t, { change, base = () => {} }) {
].join('\n'), { mode: 0o755 });
const read = (path) => (existsSync(path) ? readFileSync(path, 'utf8') : '');
let tools = '';
return {
const box = {
work, fake,
run(script, env) {
for (const name of ['gh-calls', 'comment.md']) rmSync(join(fake, name), { force: true });
@@ -811,15 +815,18 @@ function trackSandbox(t, { change, base = () => {} }) {
if (list === null) rmSync(join(fake, 'labels'), { force: true });
else writeFileSync(join(fake, 'labels'), `${list.join(',')}\n`);
},
/** #749: шаг снимка job integrate как есть; его каталог дальше идёт шагам как TOOLS. */
snapshot() {
const r = this.run(stepRun(readFileSync(WORKFLOW, 'utf8'), TOOLS_STEP), {});
/** #749/#765: шаг снимка job как есть; его каталог дальше идёт шагам как TOOLS. */
snapshot(step = TOOLS_STEP) {
const r = this.run(stepRun(readFileSync(WORKFLOW, 'utf8'), step), {});
assert.equal(r.status, 0, `снимок скриптов dev: ${r.stderr}`);
assert.ok(r.output.dir && existsSync(join(r.output.dir, 'scripts', 'process-track.mjs')), 'снимок несёт скрипт трека');
tools = r.output.dir;
return tools;
},
};
// #765: в prepare снимок dev идёт до перехода на материал и до трека.
box.snapshot(PREPARE_TOOLS_STEP);
return box;
}
const touchChange = (work) => writeFileSync(join(work, 'src', 'pointer-modality.ts'),