Сторонние Actions закреплены SHA, права выданы по job, граница проверяется фикстурами

Три вещи, которые аудит 12.09 назвал в §10.

**Перемещаемые ссылки.** `home-assistant/actions/hassfest@master` и
`hacs/action@main` — это произвольный будущий коммит чужой ветки, а ревьюера с
Read/Write/Bash запускал перемещаемый major `anthropics/claude-code-action@v1`.
Все 116 `uses:` в девяти воркфлоу закреплены полным SHA с комментарием-версией;
`scripts/action-pins.mjs` это проверяет, а предполётный вердикт Validate —
исполняет. Локальная переиспользуемая workflow пина не требует и исключена
явно.

**Права.** Один блок `permissions` на весь конвейер выдавал `issues: write` и
OIDC каждой стадии, включая единственную недоверенную — работу модели. Теперь
права выдаются по job: модели только чтение и OIDC для самой
`claude-code-action`, писать в issue умеют детерминированные стадии.

**Граница.** Разбор запечатанного результата переехал из inline-shell в
`scripts/review-result-gate.mjs` — не ради красоты, а потому что в YAML его
нельзя прогнать ни одним отрицательным случаем. Проверяются те же вещи, что и
раньше, и в том же объёме: точный набор файлов, контрольные суммы, схема
паспорта и совпадение КАЖДОГО из семнадцати полей с тем, что посчитала
детерминированная стадия. Сверху — пятнадцать враждебных фикстур: неполный
набор, лишний файл, подменённое содержимое, чужой run и попытка, устаревший
material_sha и tree, чужие задача, этап, раунд и ветка, вердикт вне словаря,
пустой документ, manifest не о тех файлах, неразбираемый JSON.

Настоящих секретов и привилегированных операций фикстуры не трогают.

Issue: #556
User-Visible: no
This commit is contained in:
Codex
2026-09-13 16:24:10 +00:00
committed by claude[bot]
parent ad886b7f3c
commit b35551884f
17 changed files with 680 additions and 162 deletions
+45 -38
View File
@@ -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,13 @@ jobs:
model_review:
name: "Ревью: работа модели"
# Единственная недоверенная стадия: модель читает материал и кладёт
# запечатанный artifact. Писать в issue и в репозиторий ей незачем — это
# делает `integrate`, проверив происхождение. `id-token` нужен самой
# claude-code-action для авторизации GitHub App.
permissions:
contents: read
id-token: write
needs: [guard, prepare]
if: needs.prepare.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true'
runs-on: ubuntu-latest
@@ -748,19 +763,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 +823,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 +861,7 @@ jobs:
- name: Review
id: review
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@9cdae7f0d995e3ba7c33f226087fdf82a59cd520 # v1
env:
# Вне рабочей копии: восстановление дерева ревьюером не должно
# уничтожать его собственный артефакт (#220).
@@ -1097,7 +1112,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 +1131,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 +1172,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 +1194,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 +1217,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<<EOF_RESULT\n' >> "$GITHUB_OUTPUT"
cat "$dir/verdict.json" >> "$GITHUB_OUTPUT"
# structured_output не обязан оканчиваться LF: delimiter команды