diff --git a/.github/workflows/mutation-gate.yml b/.github/workflows/mutation-gate.yml index ce66d331..6408f1da 100644 --- a/.github/workflows/mutation-gate.yml +++ b/.github/workflows/mutation-gate.yml @@ -53,7 +53,7 @@ jobs: tree: ${{ steps.identity.outputs.tree }} ref: ${{ steps.identity.outputs.ref }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }} fetch-depth: 0 @@ -76,19 +76,19 @@ jobs: # Час — потолок против зависшего Chromium. timeout-minutes: 60 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ needs.material.outputs.sha }} fetch-depth: 0 - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 22 cache: npm - run: npm ci - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 with: python-version: '3.14' @@ -102,7 +102,7 @@ jobs: - name: Кэш браузеров Playwright id: pw - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} @@ -138,7 +138,7 @@ jobs: --shard=${{ matrix.shard }} --shards=4 - name: Сохранить лог и identity шарда if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: mutation-shard-${{ matrix.shard }}-attempt-${{ github.run_attempt }} path: artifacts/mutation-shard-${{ matrix.shard }} @@ -153,16 +153,16 @@ jobs: if: always() runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # CLI отчётчика принадлежит зафиксированному material. В main лежит # только workflow и может не быть dev-версии scripts/**. ref: ${{ needs.material.outputs.sha }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 22 - name: Забрать evidence всех попыток - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: pattern: mutation-shard-* path: artifacts/mutation-logs @@ -191,17 +191,17 @@ jobs: actions: read issues: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # Не перечитываем moving dev: код отчётчика берётся из уже # зафиксированного material. В стабильном main dev-скриптов нет. ref: ${{ needs.material.outputs.sha }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 22 - name: Забрать логи шардов continue-on-error: true - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: pattern: mutation-shard-* path: artifacts/mutation-logs diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index 6ca0440d..d1655b00 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -29,16 +29,20 @@ on: # отменялся первой же посторонней меткой. Теперь посторонняя метка не запускает # ни одной job (`if` на guard) и в группу не входит. +# #556: права выдаются по job, а не одной строкой на весь workflow. Общий блок +# давал `issues: write` и OIDC даже той стадии, которая ничего не пишет, — +# модели. Ниже минимум на каждую: писать в issue умеют только детерминированные +# стадии, OIDC нужен исключительно `claude-code-action`. permissions: contents: read - issues: write - # Обязательно: claude-code-action получает OIDC-токен для авторизации - # GitHub App. Без этого прогон падает с «Could not fetch an OIDC token». - id-token: write jobs: guard: name: "Страж: ребейз на dev и предпосылки ревью" + # Читает и переставляет метки, комментирует отказ. + permissions: + contents: read + issues: write # Только статусные метки этапов ревью запускают конвейер (#499). Остальные # события помечаются skipped и не занимают место в группе concurrency. if: github.event.label.name == 'S4-spec-review' || github.event.label.name == 'S7-code-review' @@ -56,7 +60,7 @@ jobs: # scripts/review-doc-guard.mjs — счёт раундов вынесен туда, потому что # inline-shell не покрывается тестами (#454). Node на раннере # предустановлен, setup-node не нужен. - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 1 ref: dev @@ -257,6 +261,10 @@ jobs: prepare: name: "Ревью: материал и deterministic gates" + # Ребейзит ветку задачи и комментирует возврат автору. + permissions: + contents: read + issues: write needs: guard if: needs.guard.outputs.stage != '' runs-on: ubuntu-latest @@ -290,7 +298,7 @@ jobs: id: clock run: echo "started=$(date +%s)" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 ref: dev @@ -320,7 +328,7 @@ jobs: # кэша, платились из бюджета 45 минут и из лимитов подписки, а ходы модели # тратились на работу инфраструктуры. В validate.yml кэш стоит на всех # тяжёлых job, здесь его не было. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 22 cache: npm @@ -713,7 +721,7 @@ jobs: - name: Передать подтверждённый материал модели if: steps.gate.outputs.proceed == 'true' && steps.reuse.outputs.reuse != 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: ${{ steps.prepared.outputs.artifact }} path: ${{ runner.temp }}/review-prepared @@ -732,6 +740,28 @@ jobs: model_review: name: "Ревью: работа модели" + # Единственная недоверенная стадия: модель читает материал и кладёт + # запечатанный artifact. Писать в issue и в репозиторий ей незачем — это + # делает `integrate`, проверив происхождение: в репозиторий модель не пишет + # вообще, а в issue — только своим комментарием и отдельным issue по §12. + # + # Права здесь реальны только вместе с `github_token` у шага Review. Без него + # claude-code-action меняет OIDC на собственный App-токен с дефолтом + # `contents/issues/pull_requests: write` (`src/github/token.ts`), и блок ниже + # не ограничивает ничего: токен `ghs_…` от claude[bot] лежит прямо в + # окружении Bash-инструмента модели — это поймало ревью r1 по #556 в + # собственной же сессии. С переданным `secrets.GITHUB_TOKEN` обмена не + # происходит, `id-token` больше не нужен, и этот список становится потолком. + # + # `issues: write` остаётся: процесс требует от ревьюера комментарий с + # вердиктом (§7.2) и отдельный issue на Medium вне скоупа (§12). Снять его + # можно только перенеся и то и другое в `integrate` — это отдельная правка + # конвейера, не эта задача. Что остаётся модели этим правом: комментарий, + # метки, правка тела issue. Чего не остаётся: запись в репозиторий, слияние + # (его решает запечатанный verdict.json в `integrate`), релиз. + permissions: + contents: read + issues: write needs: [guard, prepare] if: needs.prepare.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' runs-on: ubuntu-latest @@ -748,19 +778,19 @@ jobs: id: clock run: echo "started=$(date +%s)" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 ref: ${{ needs.prepare.outputs.material_sha }} persist-credentials: false - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 22 cache: npm - name: Получить контракт подготовленного материала - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: review-prepared-${{ github.event.issue.number }}-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/review-prepared @@ -808,7 +838,7 @@ jobs: # но когда нужен — качать его заново дороже, чем держать в кэше. - name: Кэш браузеров Playwright id: pw - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} @@ -846,7 +876,7 @@ jobs: - name: Review id: review - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@9cdae7f0d995e3ba7c33f226087fdf82a59cd520 # v1 env: # Вне рабочей копии: восстановление дерева ревьюером не должно # уничтожать его собственный артефакт (#220). @@ -855,6 +885,11 @@ jobs: # Подписка, а не отдельный счёт API: токен выпускается через # `claude setup-token` (Pro/Max). Действуют лимиты подписки. claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Ambient job-scoped токен вместо App-обмена (#556). Со ним + # `permissions:` этой job — настоящий потолок прав модели: ни записи в + # репозиторий, ни постановки метки, ни комментария. Строку нельзя + # снять, не вернув модели право двигать процесс. + github_token: ${{ secrets.GITHUB_TOKEN }} path_to_claude_code_executable: ${{ steps.claude_bin.outputs.path }} prompt: | Ты ревьюер проекта House Plan. Язык ответа — русский. @@ -1097,7 +1132,7 @@ jobs: echo "artifact=review-result-${NUM}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" - name: Передать результат интеграции - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: ${{ steps.result.outputs.artifact }} path: ${{ runner.temp }}/review-result @@ -1116,6 +1151,10 @@ jobs: integrate: name: "Ревью: публикация и интеграция" + # Публикует разбор, переставляет метку, сливает проверенный материал. + permissions: + contents: read + issues: write needs: [guard, prepare, model_review] if: always() && needs.guard.outputs.stage != '' runs-on: ubuntu-latest @@ -1153,21 +1192,21 @@ jobs: fi echo "proceed=true" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 if: steps.ready.outputs.proceed == 'true' with: fetch-depth: 0 ref: dev persist-credentials: false - - uses: actions/setup-node@v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 if: steps.ready.outputs.proceed == 'true' with: node-version: 22 - name: Получить результат модели if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: review-result-${{ github.event.issue.number }}-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/review-result @@ -1175,8 +1214,14 @@ jobs: - name: Проверить полноту и происхождение результата id: result if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' + # #556: единственное, что недоверенная стадия модели может передать + # дальше, — этот artifact, и принимается он как ввод противника: полный + # набор файлов, сходящиеся суммы, совпадение КАЖДОГО поля паспорта с + # тем, что посчитала детерминированная `prepare`. Проверка вынесена из + # inline-shell в `scripts/review-result-gate.mjs` ради враждебных + # фикстур — в YAML её нельзя прогнать ни одним отрицательным случаем. env: - NUM: ${{ github.event.issue.number }} + ISSUE: ${{ github.event.issue.number }} STAGE: ${{ needs.guard.outputs.stage }} CYCLE: ${{ needs.guard.outputs.cycle }} BRANCH: ${{ needs.prepare.outputs.branch }} @@ -1192,27 +1237,9 @@ jobs: SPEC_BODY_DOC: ${{ needs.prepare.outputs.spec_body_doc }} SPEC_BODY_RECORDED: ${{ needs.prepare.outputs.spec_body_recorded }} run: | + set -euo pipefail dir="$RUNNER_TEMP/review-result" - expected=$(printf '%s\n' manifest.sha256 prepared.json review-document.md verdict.json | sort) - actual=$(find "$dir" -maxdepth 1 -type f -printf '%f\n' | sort) - test "$actual" = "$expected" || { echo "::error::неполный или неожиданный набор evidence"; printf 'ожидалось:\n%s\nполучено:\n%s\n' "$expected" "$actual"; exit 1; } - (cd "$dir" && sha256sum -c manifest.sha256) - test -s "$dir/review-document.md" - jq -e \ - --arg run_id "$GITHUB_RUN_ID" --arg run_attempt "$GITHUB_RUN_ATTEMPT" \ - --arg issue "$NUM" --arg stage "$STAGE" --arg cycle "$CYCLE" \ - --arg branch "$BRANCH" --arg sha "$MATERIAL_SHA" --arg tree "$MATERIAL_TREE" \ - --arg specs "$MATERIAL_SPECS" --arg body "$MATERIAL_ISSUE_BODY" \ - --arg validate "$VALIDATE_RESULT" --arg validate_url "$VALIDATE_URL" \ - --arg rebase_note "$REBASE_NOTE" --arg validated_note "$VALIDATED_NOTE" \ - --arg spec_changed "$SPEC_BODY_CHANGED" --arg spec_doc "$SPEC_BODY_DOC" \ - --arg spec_recorded "$SPEC_BODY_RECORDED" \ - '.schema == 1 and .run_id == $run_id and .run_attempt == $run_attempt and .issue == $issue and .stage == $stage and .cycle == $cycle and .branch == $branch and .material_sha == $sha and .material_tree == $tree and .material_specs == $specs and .material_issue_body == $body and .validate_result == $validate and .validate_url == $validate_url and .rebase_note == $rebase_note and .validated_note == $validated_note and .spec_body_changed == $spec_changed and .spec_body_doc == $spec_doc and .spec_body_recorded == $spec_recorded' \ - "$dir/prepared.json" - jq -e ' - (.verdict == "green" or .verdict == "yellow" or .verdict == "red") - and (.high | type == "number") and (.medium | type == "number") - and (.summary | type == "string")' "$dir/verdict.json" >/dev/null + node scripts/review-result-gate.mjs --dir="$dir" printf 'structured_output<> "$GITHUB_OUTPUT" cat "$dir/verdict.json" >> "$GITHUB_OUTPUT" # structured_output не обязан оканчиваться LF: delimiter команды