From 5a1cddeaf0fbac3e96bdc7ace20091a4549691c1 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 10 Sep 2026 00:04:41 +0300 Subject: [PATCH] ci: review anchors are taken after the pipeline's rebase, not before MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The material anchors (commit, tree, spec blobs) written into every review document came from the checkout step, before "Привести ветку к dev". Whenever dev had moved — since 09.09 every review-document publish moves it — the pipeline rebased and force-pushed the branch, orphaning the pre-rebase commit and its tree. A fresh clone in the next run could not resolve that tree: reuse (#499) always reported false and the model reviewed the same code again, and the #413 post-step refused the green round because neither the cited SHA nor the tree anchor was reachable — #508 took three identical green rounds this way. The `material` step, which already fixes the reviewed SHA after the rebase, now also records the tree and spec blobs, and the publish step reads all three from it. The contract test pins the order and forbids reading anchors from the checkout step. Issue: #515 User-Visible: no --- .github/workflows/process.yml | 44 ++++++++++++++++++++-------------- test/review-doc-guard.test.mjs | 19 +++++++++++++++ 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 1a8e7927..6ef9056e 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -321,20 +321,10 @@ jobs: git checkout -q "origin/$branch" echo "материал ревью: ветка $branch, $(git rev-parse --short HEAD)" echo "name=$branch" >> "$GITHUB_OUTPUT" - # Якоря материала, устойчивые к ребейзу (#413, #414). SHA коммита - # ребейз меняет — содержимое нет: git адресует деревья и блобы их - # хешем. Снимаются здесь, где рабочая копия ЕЩЁ равна тому, что - # ревьюер прочтёт; в шаге публикации дерево уже сброшено на целевую - # ветку, и спрашивать его поздно. - echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - echo "tree=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT" - # ТЗ задачи: блоб переживает и ребейз, и удаление ветки, пока текст - # где-нибудь достижим. Файлов может не быть (инфраструктурная - # задача) или быть несколько (разбитое ТЗ) — тогда список пуст либо - # длиннее одного. - specs=$(git ls-files -s -- "docs/specs/${NUM}-*.md" \ - | awk '{print $2" "$4}' | tr '\n' ';') - echo "specs=$specs" >> "$GITHUB_OUTPUT" + # Якоря материала (sha, tree, specs) снимает шаг `material` — ПОСЛЕ + # ребейза (#515): снятые здесь, они после force-push приведённой + # ветки указывали на осиротевший коммит, и ни reuse (#499), ни + # страховка #414 не находили дерева в свежем клоне. else echo "::warning::ветка issue/${NUM}-* не найдена на origin — ревью пойдёт по dev" echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY" @@ -413,9 +403,26 @@ jobs: - name: Зафиксировать SHA материала ревью id: material if: steps.rebase.outputs.conflict != 'true' + env: + NUM: ${{ github.event.issue.number }} run: | echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - echo "материал ревью: $(git rev-parse --short HEAD)" + # Якоря материала, устойчивые к ребейзу (#413, #414). SHA коммита + # ребейз меняет — содержимое нет: git адресует деревья и блобы их + # хешем. Снимаются здесь, ПОСЛЕ приведения к dev (#515): рабочая + # копия равна тому, что ревьюер прочтёт, и коммит с этим деревом + # уже запушен в ветку — следующий прогон найдёт его в свежем клоне. + # В шаге публикации дерево уже сброшено на целевую ветку, и + # спрашивать его поздно. + echo "tree=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT" + # ТЗ задачи: блоб переживает и ребейз, и удаление ветки, пока текст + # где-нибудь достижим. Файлов может не быть (инфраструктурная + # задача) или быть несколько (разбитое ТЗ) — тогда список пуст либо + # длиннее одного. + specs=$(git ls-files -s -- "docs/specs/${NUM}-*.md" \ + | awk '{print $2" "$4}' | tr '\n' ';') + echo "specs=$specs" >> "$GITHUB_OUTPUT" + echo "материал ревью: $(git rev-parse --short HEAD), дерево $(git rev-parse --short 'HEAD^{tree}')" # Повторное применение зелёного вердикта без вызова модели (#499). Сценарий # #437 r4: зелёный r3 не слился (страж #312), задача вернулась в S6 и тут же @@ -849,9 +856,10 @@ jobs: STAGE: ${{ needs.guard.outputs.stage }} CYCLE: ${{ needs.guard.outputs.cycle }} SOURCE: ${{ runner.temp }}/review-document.md - MATERIAL_SHA: ${{ steps.branch.outputs.sha }} - MATERIAL_TREE: ${{ steps.branch.outputs.tree }} - MATERIAL_SPECS: ${{ steps.branch.outputs.specs }} + # После ребейза конвейером — якоря приведённого материала (#515). + MATERIAL_SHA: ${{ steps.material.outputs.sha }} + MATERIAL_TREE: ${{ steps.material.outputs.tree }} + MATERIAL_SPECS: ${{ steps.material.outputs.specs }} # Вердикт из structured_output попадает в блок якорей (#499): по нему # следующий заход решает, можно ли применить зелёный вердикт повторно. OUT: ${{ steps.review.outputs.structured_output }} diff --git a/test/review-doc-guard.test.mjs b/test/review-doc-guard.test.mjs index 7ff0a10d..8a0d6100 100644 --- a/test/review-doc-guard.test.mjs +++ b/test/review-doc-guard.test.mjs @@ -633,3 +633,22 @@ test('#510 AC2: конвейер запускает Validate с мутантам assert.match(chunk, /if: (needs\.guard\.outputs\.stage == 'code' && )?steps\.(gate\.outputs\.proceed == 'true'|decide\.outputs\.green == 'true')/, `${name}: условие по proceed/зелёному`); } }); + +test('#515: якоря материала снимаются ПОСЛЕ ребейза конвейером и публикуются из шага material', () => { + const workflow = readFileSync(new URL('../.github/workflows/process.yml', import.meta.url), 'utf8'); + const at = (marker) => { const i = workflow.indexOf(marker); assert.ok(i > 0, `нет «${marker}»`); return i; }; + const rebase = at(' - name: Привести ветку к dev\n'); + const material = at(' - name: Зафиксировать SHA материала ревью\n'); + const reuse = at(' - name: "Зелёный вердикт прошлого захода применим без ревью (#499)"\n'); + assert.ok(rebase < material && material < reuse, 'material стоит после ребейза и до reuse'); + const materialStep = workflow.slice(material, reuse); + assert.match(materialStep, /echo "tree=\$\(git rev-parse 'HEAD\^\{tree\}'\)" >> "\$GITHUB_OUTPUT"/, 'дерево — из шага material'); + assert.match(materialStep, /echo "specs=\$specs" >> "\$GITHUB_OUTPUT"/, 'ТЗ — из шага material'); + const publish = at(' - name: Опубликовать документ ревью\n'); + const publishStep = workflow.slice(publish, at(' - name: "Материал раунда воспроизводим (#413)"\n')); + for (const name of ['SHA', 'TREE', 'SPECS']) { + assert.match(publishStep, new RegExp(`MATERIAL_${name}: \\$\\{\\{ steps\\.material\\.outputs\\.${name.toLowerCase()} \\}\\}`), `MATERIAL_${name} из material`); + } + // до-ребейзные якоря из шага branch никем не читаются: после force-push они мертвы (#508 r1–r3) + assert.doesNotMatch(workflow, /steps\.branch\.outputs\.(sha|tree|specs)/, 'якоря из шага branch — осиротевшие после ребейза'); +});