Files
houseplan-card/scripts/action-pins.mjs
T
Codexandclaude[bot] b35551884f Сторонние 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
2026-09-13 16:24:10 +00:00

91 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Все сторонние Actions закреплены полным immutable SHA (#556).
*
* Перемещаемая ссылка в `uses:` — это доверие чужому владельцу тега здесь и
* сейчас, а не коду, который читали. До этой правки конвейер брал
* `home-assistant/actions/hassfest@master` и `hacs/action@main` — то есть
* произвольный будущий коммит чужой ветки, — а ревьюера с Read/Write/Bash
* запускал `anthropics/claude-code-action@v1`, перемещаемый major.
*
* Проверка механическая и потому не врёт: `uses:` обязан быть либо локальным
* (`./.github/…`), либо `<owner>/<repo>[/<path>]@<40 hex>` с комментарием, где
* записана человекочитаемая версия — то, что при обновлении сверяет человек.
*
* node scripts/action-pins.mjs # проверить
* node scripts/action-pins.mjs --list # что и к чему закреплено
*
* Обновление пина: посмотреть, что сейчас стоит за тегом
* `gh api repos/<owner>/<repo>/commits/<tag> -q .sha`, прочитать дельту от
* закреплённого SHA и заменить обе части — SHA и комментарий — одним коммитом.
*/
import { readFileSync, readdirSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export const WORKFLOW_DIR = '.github/workflows';
const USES = /^\s*(?:-\s*)?uses:\s*(\S+)(.*)$/;
/** Локальная переиспользуемая workflow — не сторонний код, пина не требует. */
export const isLocal = (spec) => spec.startsWith('./');
export const isPinned = (spec) => /^[\w.-]+\/[\w./-]+@[0-9a-f]{40}$/.test(spec);
/** Комментарий обязателен: без него человек не знает, какую версию он закрепил. */
export const hasVersionNote = (tail) => /#\s*\S/.test(tail);
export function auditWorkflowSource(file, source) {
const problems = [];
source.split('\n').forEach((line, index) => {
const match = USES.exec(line);
if (!match) return;
const [, spec, tail] = match;
const at = `${file}:${index + 1}`;
if (isLocal(spec)) return;
if (!isPinned(spec)) {
problems.push(`${at}: «${spec}» не закреплён полным SHA`);
return;
}
if (!hasVersionNote(tail)) {
problems.push(`${at}: «${spec}» без комментария с версией`);
}
});
return problems;
}
export function listWorkflows(root = ROOT) {
return readdirSync(resolve(root, WORKFLOW_DIR))
.filter((name) => name.endsWith('.yml') || name.endsWith('.yaml'))
.sort();
}
export function auditRepository(root = ROOT) {
const problems = [];
for (const name of listWorkflows(root)) {
const file = `${WORKFLOW_DIR}/${name}`;
problems.push(...auditWorkflowSource(file, readFileSync(resolve(root, file), 'utf8')));
}
return problems;
}
const invokedDirectly = process.argv[1]
&& resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
if (invokedDirectly) {
if (process.argv.includes('--list')) {
for (const name of listWorkflows()) {
const file = `${WORKFLOW_DIR}/${name}`;
for (const line of readFileSync(resolve(ROOT, file), 'utf8').split('\n')) {
const match = USES.exec(line);
if (match && !isLocal(match[1])) console.log(`${file}: ${match[1]}${match[2]}`);
}
}
process.exit(0);
}
const problems = auditRepository();
if (problems.length) {
for (const problem of problems) console.error(`::error::${problem}`);
console.error(`не закреплено: ${problems.length}`);
process.exit(1);
}
console.log('все сторонние Actions закреплены полным SHA');
}