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 — осиротевшие после ребейза'); +});