diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ca121e11..f52efc46 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -13,6 +13,11 @@ # Красный ночной прогон — сигнал автору последних коммитов на dev, не гейт: # гейт беты по-прежнему требует зелёный Validate на точном SHA кандидата, и # там полный набор идёт заново. +# +# Сигнал обязан быть настоящим (#492 §7): до этой задачи job завершалась +# успехом в момент постановки Validate в очередь, и красный полный прогон не +# делал ночной workflow красным. Теперь job находит запущенный прогон и ждёт +# его: успешный dispatch — не успешная проверка. name: Ночной полный прогон dev on: @@ -26,13 +31,34 @@ permissions: jobs: dispatch: - name: "Запустить Validate на dev с полным набором" + name: "Запустить Validate на dev с полным набором и дождаться результата" runs-on: ubuntu-latest + timeout-minutes: 90 steps: - env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} run: | set -euo pipefail + since=$(date -u +%FT%TZ) gh workflow run validate.yml --repo "$REPO" --ref dev -f full=true - echo "Validate(dev, full=true) поставлен в очередь: $(date -u +%FT%TZ)" + echo "Validate(dev, full=true) поставлен в очередь: $since" + # Найти именно этот прогон: workflow_dispatch на dev, созданный не + # раньше момента запуска. До трёх минут на появление в списке. + run_id="" + for _ in $(seq 1 18); do + sleep 10 + run_id=$(gh run list --repo "$REPO" --workflow validate.yml --branch dev \ + --event workflow_dispatch --json databaseId,createdAt --limit 5 \ + --jq "[.[] | select(.createdAt >= \"$since\")] | sort_by(.createdAt) | last | .databaseId // empty") + [ -n "$run_id" ] && break + done + if [ -z "$run_id" ]; then + echo "::error::прогон Validate не появился за 3 минуты — dispatch не равен проверке" + exit 1 + fi + url="${{ github.server_url }}/$REPO/actions/runs/$run_id" + echo "дочерний прогон: $url" + echo "- Validate(dev, full=true): $url" >> "$GITHUB_STEP_SUMMARY" + # Ждём завершения; красный дочерний прогон — красный ночной. + gh run watch "$run_id" --repo "$REPO" --exit-status --interval 30 diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 499b2898..19fee6a4 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -1012,16 +1012,19 @@ jobs: # S8-merged утверждает, что код в dev. Значит слияние обязано произойти # ДО метки, иначе она врёт в промежутке. # - # При конфликте шаг НЕ падает и метку не оставляет на месте. Первая - # редакция делала именно так, и это оказалось тупиком: автор ждёт смену - # метки, метка не менялась, и он тридцать раз опрашивал впустую, чтобы - # затем отчитаться «лимит исчерпан» — при зелёном вердикте. Инвариант - # теперь жёстче: ПОСЛЕ ПРОГОНА РЕВЬЮ МЕТКА МЕНЯЕТСЯ ВСЕГДА. + # Слияние — точный кандидат (#492 §4, scripts/merge-candidate.mjs): + # ветка сверяется с материалом (#312); если dev не двигался — push с + # lease на текущую вершину; если двигался — ребейз, сравнение patch-id + # с проверенным диффом, публикация кандидата в ветку, ожидание + # Validate на этом SHA и только потом push в dev с lease. Повторное + # движение dev — новая попытка, не более трёх. Каждый исход, кроме + # успеха, ведёт в S6-in-progress/S7-code-review с комментарием, ПОСЛЕ + # ПРОГОНА МЕТКА МЕНЯЕТСЯ ВСЕГДА — инвариант тот же, что и раньше. - name: Слить ветку в dev id: merge if: needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' env: - TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + HP_PROCESS_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} BRANCH: ${{ steps.branch.outputs.name }} NUM: ${{ github.event.issue.number }} @@ -1032,58 +1035,8 @@ jobs: echo "merged=false" >> "$GITHUB_OUTPUT" exit 0 fi - git fetch -q origin dev "$BRANCH" - # #312: сливается только проверенный код. Допустимые вершины ветки: - # сам SHA материала либо он же плюс ровно один коммит публикации - # документа ревью (дифф только docs/reviews/). Любой другой коммит — - # ветка уехала после ревью, вердикт к ней не применим: возврат в - # S6-in-progress через merged=false, как при конфликте. - actual=$(git rev-parse "origin/$BRANCH") - reviewed="$MATERIAL_SHA" - fresh=false - if [ "$actual" = "$reviewed" ]; then - fresh=true - elif [ "$(git rev-parse "$actual^" 2>/dev/null)" = "$reviewed" ] \ - && [ -z "$(git diff --name-only "$reviewed" "$actual" -- . ':!docs/reviews')" ]; then - fresh=true - fi - if [ "$fresh" != true ]; then - echo "merged=false" >> "$GITHUB_OUTPUT" - echo "::warning::ветка $BRANCH уехала после проверенного SHA $reviewed (сейчас $actual) — слияние отменено (#312)" - cat > /tmp/stale-verdict.md <> "$GITHUB_OUTPUT" - echo "::warning::ветка $BRANCH не сливается в dev без конфликта" - cat > /tmp/conflict.md <> "$GITHUB_OUTPUT" - echo "слито в dev: $(git rev-parse --short HEAD)" + node scripts/merge-candidate.mjs --branch="$BRANCH" --material="$MATERIAL_SHA" \ + --issue="$NUM" --repo="${{ github.repository }}" - name: Переставить метку if: steps.rebase.outputs.conflict != 'true' @@ -1094,7 +1047,10 @@ jobs: FROM: ${{ steps.decide.outputs.from }} # Зелёное код-ревью без слияния ведёт не в S8-merged, а обратно к # автору: метка утверждала бы, что код в dev, а его там нет. - TO: ${{ (needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' && steps.merge.outputs.merged != 'true') && 'S6-in-progress' || steps.decide.outputs.to }} + # Точный кандидат (#492) сам называет исход: S8 после push, S6/S7 — + # когда кандидат не слит (конфликт, красный Validate, изменившийся + # patch-id, ушедший dev). Без исхода от скрипта — как раньше: S6. + TO: ${{ (needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' && steps.merge.outputs.to != '') && steps.merge.outputs.to || ((needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' && steps.merge.outputs.merged != 'true') && 'S6-in-progress' || steps.decide.outputs.to) }} run: | gh issue edit "$NUM" --repo "${{ github.repository }}" \ --add-label "$TO" --remove-label "$FROM"