diff --git a/.github/workflows/process-resume.yml b/.github/workflows/process-resume.yml new file mode 100644 index 00000000..e26cf38a --- /dev/null +++ b/.github/workflows/process-resume.yml @@ -0,0 +1,60 @@ +name: Продолжение ревью после Validate +run-name: "resume · ${{ github.event.workflow_run.head_branch }} · ${{ github.event.workflow_run.conclusion }}" + +# #636. Стадия `prepare` конвейера (process.yml) больше не ждёт Validate с +# мутантами на материале внутри job — раннер спал ≈ 28 минут на раунд при +# 10–12 минутах работы модели. Она диспатчит прогон, кладёт запечатанный +# маркер `review-pending-…` и выходит. Этот workflow просыпается на завершение +# любого Validate и, если раунд ждал именно этот прогон (маркер на материале, +# метка S7 стоит, активного прогона конвейера нет), переставляет метку S7 — +# новый прогон `prepare` находит завершённый dispatch и продолжает раунд. +# Ничего не оценивает: зелёный/красный разбирает сам конвейер. Страховка на +# потерянное событие — process-reconcile.yml с тем же маркером. +# +# Как и process.yml, файл исполняется из ветки по умолчанию (main): для +# события `workflow_run` GitHub берёт workflow только оттуда. Сверка копий — +# в preflight validate.yml. + +on: + workflow_run: + workflows: ["Проверка (CI)"] + types: [completed] + +permissions: + contents: read + actions: read + +jobs: + resume: + name: "Разбудить раунд, ждавший этот Validate" + if: github.event.workflow_run.event == 'workflow_dispatch' && startsWith(github.event.workflow_run.head_branch, 'issue/') + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: process-resume-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + steps: + # Код берётся из dev, как у reconcile: после штатного слияния действует + # версия, которую проверил CI, а не копия из main. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: dev + fetch-depth: 1 + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 22 + # Метка переставляется HP_PROCESS_TOKEN: событие от GITHUB_TOKEN не + # запустило бы process.yml (см. шапку process.yml, п. 1). + - name: Решить по маркеру ожидания и переставить S7 + env: + GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.event.workflow_run.head_branch }} + SHA: ${{ github.event.workflow_run.head_sha }} + EVENT: ${{ github.event.workflow_run.event }} + STATUS: ${{ github.event.workflow_run.status }} + run: | + node scripts/process-resume.mjs \ + --repo="$REPO" --branch="$BRANCH" --sha="$SHA" \ + --event="$EVENT" --status="$STATUS" --apply=true | tee -a "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index d1655b00..1813ba7c 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -582,14 +582,64 @@ jobs: { echo 'proceed=true'; echo 'result=skipped'; } >> "$GITHUB_OUTPUT" exit 0 fi - if node scripts/validate-gate.mjs --repo="${{ github.repository }}" --ref="$BRANCH" --sha="$SHA"; then - echo 'proceed=true' >> "$GITHUB_OUTPUT" - else - echo 'proceed=false' >> "$GITHUB_OUTPUT" - fi + # #636: раннер не спит, пока идёт Validate (28 минут на раунд при + # 10–12 минутах работы модели). Гейт диспатчит прогон, убеждается, что + # тот встал на материал, и выходит с кодом 2 — «идёт». Раунд продолжит + # событие завершения Validate (process-resume.yml переставит метку + # S7), страховка — process-reconcile. Зелёный или красный завершённый + # прогон гейт и без ожидания возвращает сразу. + set +e + node scripts/validate-gate.mjs --repo="${{ github.repository }}" --ref="$BRANCH" --sha="$SHA" --no-wait + code=$? + set -e + case "$code" in + 0) echo 'proceed=true' >> "$GITHUB_OUTPUT" ;; + 2) echo 'proceed=pending' >> "$GITHUB_OUTPUT" ;; + *) echo 'proceed=false' >> "$GITHUB_OUTPUT" ;; + esac + + # #636: прогон на материале идёт — записать маркер ожидания и освободить + # раннер. Метка S7 остаётся; событие `workflow_run` по завершении Validate + # переставит её, и новый прогон конвейера найдёт завершённый dispatch + # сразу. Маркер читают process-resume.mjs и process-reconcile.mjs: без + # него ни один из них не имеет права будить раунд — иначе «успешный + # прогон без вердикта» неотличим от потерянного запроса. + - name: Validate идёт — раунд продолжит событие + id: pending + if: steps.rebase.outputs.conflict != 'true' && steps.gate.outputs.proceed == 'pending' + env: + NUM: ${{ github.event.issue.number }} + STAGE: ${{ needs.guard.outputs.stage }} + BRANCH: ${{ steps.branch.outputs.name }} + SHA: ${{ steps.material.outputs.sha }} + VALIDATE_RUN_ID: ${{ steps.gate.outputs.run_id }} + VALIDATE_URL: ${{ steps.gate.outputs.url }} + run: | + dir="$RUNNER_TEMP/review-pending" + mkdir -p "$dir" + jq -n -S \ + --arg run_id "$GITHUB_RUN_ID" --arg run_attempt "$GITHUB_RUN_ATTEMPT" \ + --arg issue "$NUM" --arg stage "$STAGE" --arg branch "$BRANCH" \ + --arg material_sha "$SHA" --arg validate_run_id "$VALIDATE_RUN_ID" \ + --arg validate_url "$VALIDATE_URL" \ + '{schema:1,run_id:$run_id,run_attempt:$run_attempt,issue:$issue,stage:$stage,branch:$branch,material_sha:$material_sha,validate_run_id:$validate_run_id,validate_url:$validate_url}' \ + > "$dir/pending.json" + (cd "$dir" && sha256sum pending.json > manifest.sha256) + echo "artifact=review-pending-${NUM}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" + short=$(git rev-parse --short "$SHA") + echo "Validate с мутантами на \`$short\` идёт — раннер освобождён, раунд продолжится по завершении прогона${VALIDATE_URL:+ ($VALIDATE_URL)}." >> "$GITHUB_STEP_SUMMARY" + + - name: Сохранить маркер ожидания + if: steps.gate.outputs.proceed == 'pending' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: ${{ steps.pending.outputs.artifact }} + path: ${{ runner.temp }}/review-pending + if-no-files-found: error + retention-days: 1 - name: Validate красный — вернуть автору без ревью - if: steps.rebase.outputs.conflict != 'true' && steps.gate.outputs.proceed != 'true' + if: steps.rebase.outputs.conflict != 'true' && steps.gate.outputs.proceed == 'false' env: GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }} NUM: ${{ github.event.issue.number }} @@ -1181,6 +1231,11 @@ jobs: echo "::error::стадия deterministic prerequisites завершилась: $PREPARE_RESULT" exit 1 fi + if [ "$PROCEED" = "pending" ]; then + echo "Validate на материале ещё идёт — раунд продолжит событие завершения (#636); интегрировать нечего" + echo "proceed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi if [ "$PROCEED" != "true" ]; then echo "подготовка уже вернула задачу автору; интегрировать нечего" echo "proceed=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1965d75c..a5b80ccb 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -101,9 +101,10 @@ jobs: run: | git fetch --quiet origin main dev # #472: расписание mutation-gate.yml тоже исполняется из ветки по - # умолчанию — та же ловушка, что у process.yml. Сверяются оба. + # умолчанию — та же ловушка, что у process.yml; #636 добавил + # process-resume.yml (событие workflow_run). Сверяются все три. status=0 - for file in process.yml mutation-gate.yml; do + for file in process.yml mutation-gate.yml process-resume.yml; do if diff <(git show "origin/main:.github/workflows/$file") \ <(git show "origin/dev:.github/workflows/$file"); then echo "$file: main и dev идентичны" @@ -243,7 +244,7 @@ jobs: mutants_requested: ${{ steps.heavy.outputs.mutants_requested }} # #510 base: ${{ steps.base.outputs.base }} # `base` — до какого коммита классифицировать файлы ветки (#387), - # `range_base` — от какого судить диапазон на dev (#388). + # `range_base` — от какого судить диапазон на dev/main (#388, #619). range_base: ${{ steps.base.outputs.range_base }} steps: # `git diff --name-only` содержимого файлов не читает вовсе, поэтому @@ -283,9 +284,10 @@ jobs: gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \ -f branch="$BRANCH" -f status=completed -F per_page=100 \ > /tmp/validate-runs.json || echo '{}' > /tmp/validate-runs.json - if [ "$REF" = "refs/heads/dev" ]; then - # На dev классифицировать нечего (всё true), но база диапазона - # нужна гейту «новый код не добавляет any» в job frontend (#388). + if [ "$REF" = "refs/heads/dev" ] || [ "$REF" = "refs/heads/main" ]; then + # На интеграционных ветках классифицировать нечего (всё true), но + # база диапазона нужна гейту «новый код не добавляет any» в job + # frontend (#388, #619). node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \ --name=range_base --fallback="$FALLBACK" --runs=/tmp/validate-runs.json exit 0 @@ -302,8 +304,8 @@ jobs: HEAD_SHA: ${{ github.sha }} REF: ${{ github.ref }} run: | - if [ "$REF" = "refs/heads/dev" ]; then - echo "dev: без фильтров, всё true" + if [ "$REF" = "refs/heads/dev" ] || [ "$REF" = "refs/heads/main" ]; then + echo "dev/main: без фильтров, всё true" node scripts/classify-changes.mjs --all >> "$GITHUB_OUTPUT" exit 0 fi @@ -713,11 +715,12 @@ jobs: git fetch -q origin dev zero=$(printf '%040d' 0) base="" - # Пуш прямо в dev: диапазон — то, что добавлено с последнего - # ДОКАЗАННО зелёного предка (#388), а не с головы предыдущего пуша: - # его прогон штатно отменяется следующим, и добавленные им строки не - # судил бы никто. Фолбэк — прежний `before`. - if [ "$EVENT_NAME" != "pull_request" ] && [ "$REF" = "refs/heads/dev" ]; then + # Пуш прямо в dev/main: диапазон — то, что добавлено с последнего + # ДОКАЗАННО зелёного предка (#388, #619), а не с головы предыдущего + # пуша: его прогон штатно отменяется следующим, и добавленные им + # строки не судил бы никто. Фолбэк — прежний `before`. + if [ "$EVENT_NAME" != "pull_request" ] \ + && { [ "$REF" = "refs/heads/dev" ] || [ "$REF" = "refs/heads/main" ]; }; then base="${PROVEN_BASE:-$BEFORE_SHA}" fi # Ветка, PR, новая история: точки отсчёта нет, берём merge-base с dev.