mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
Six workflows run from the default branch (issues, schedule, workflow_run): process, process-resume, process-reconcile, mutation-gate, nightly, process-metrics. Their bodies move to _<name>.yml (on: workflow_call); the original files keep only triggers, run-name, permissions, concurrency and one job `uses: Matysh/houseplan-card/.github/workflows/_<name>.yml@dev` with `secrets: inherit`. A pipeline change becomes one commit to dev. - caller job permissions = union of body job permissions (#556 minimum kept per job inside the body); caller `if` repeats the body guard for process and process-resume so unrelated events stay skipped; - dispatch inputs forwarded via workflow_call inputs of the same names; - _mutation-gate.yml keys evidence/marker on job.workflow_sha (the body SHA): in a called workflow github.workflow_sha belongs to the caller in main; - action-pins: narrow exception for this repo's _*.yml at @dev with a reason; - preflight workflow_sync compares all six thin callers (was 3 of 6); performance.yml excluded: its schedule judges main with main's own body; - tests read bodies from _*.yml; new test/default-branch-workflows.test.mjs; six mutants; PROCESS.md §10.4, AGENTS.md, REVIEWER.md updated. Issue: #623 User-Visible: no
97 lines
5.5 KiB
JavaScript
97 lines
5.5 KiB
JavaScript
// #556: перемещаемая ссылка в `uses:` — это доверие чужому владельцу тега здесь
|
||
// и сейчас, а не коду, который читали. До правки конвейер брал
|
||
// `home-assistant/actions/hassfest@master` и `hacs/action@main`, то есть
|
||
// произвольный будущий коммит чужой ветки, а ревьюера с Read/Write/Bash
|
||
// запускал перемещаемый major `anthropics/claude-code-action@v1`.
|
||
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import { readFileSync } from 'node:fs';
|
||
|
||
import {
|
||
auditRepository, auditWorkflowSource, isLocal, isOwnDevReusable, isPinned, hasVersionNote, listWorkflows,
|
||
} from '../scripts/action-pins.mjs';
|
||
|
||
test('#556: в репозитории не осталось незакреплённых Actions', () => {
|
||
assert.deepEqual(auditRepository(), []);
|
||
});
|
||
|
||
test('#556: проверка смотрит на все воркфлоу, а не на один', () => {
|
||
const files = listWorkflows();
|
||
assert.ok(files.length >= 9, `воркфлоу найдено ${files.length}`);
|
||
assert.ok(files.includes('process.yml') && files.includes('validate.yml'));
|
||
});
|
||
|
||
test('#556: локальная переиспользуемая workflow пина не требует', () => {
|
||
assert.equal(isLocal('./.github/workflows/announce.yml'), true);
|
||
assert.deepEqual(auditWorkflowSource('x.yml', ' uses: ./.github/workflows/announce.yml\n'), []);
|
||
});
|
||
|
||
test('#556: перемещаемая ссылка — находка, в какой бы форме ни пришла', () => {
|
||
for (const spec of [
|
||
'actions/checkout@v7', 'hacs/action@main', 'home-assistant/actions/hassfest@master',
|
||
'anthropics/claude-code-action@v1', 'owner/repo@abc1234',
|
||
]) {
|
||
assert.equal(isPinned(spec), false, spec);
|
||
const found = auditWorkflowSource('x.yml', ` - uses: ${spec}\n`);
|
||
assert.equal(found.length, 1, spec);
|
||
assert.match(found[0], /не закреплён полным SHA/);
|
||
}
|
||
});
|
||
|
||
test('#556: SHA без комментария с версией — тоже находка', () => {
|
||
const sha = 'a'.repeat(40);
|
||
assert.equal(isPinned(`owner/repo@${sha}`), true);
|
||
assert.equal(hasVersionNote(' # v7'), true);
|
||
assert.equal(hasVersionNote(''), false);
|
||
const bare = auditWorkflowSource('x.yml', ` - uses: owner/repo@${sha}\n`);
|
||
assert.equal(bare.length, 1);
|
||
assert.match(bare[0], /без комментария с версией/);
|
||
assert.deepEqual(auditWorkflowSource('x.yml', ` - uses: owner/repo@${sha} # v7\n`), []);
|
||
});
|
||
|
||
test('#556: находка называет файл и строку', () => {
|
||
const source = ['jobs:', ' a:', ' steps:', ' - uses: hacs/action@main'].join('\n');
|
||
assert.match(auditWorkflowSource('.github/workflows/x.yml', source)[0],
|
||
/^\.github\/workflows\/x\.yml:4: /);
|
||
});
|
||
|
||
// Проверка обязана стоять в предполётном вердикте Validate, иначе она есть, но
|
||
// не сработает ни на одном пуше.
|
||
test('#556: preflight Validate считает пины частью вердикта', () => {
|
||
const workflow = readFileSync(new URL('../.github/workflows/validate.yml', import.meta.url), 'utf8');
|
||
const preflight = workflow.slice(0, workflow.indexOf('\n changes:'));
|
||
assert.match(preflight, /^\s+run: node scripts\/action-pins\.mjs$/m, 'шаг запускает проверку');
|
||
assert.match(preflight, /ACTION_PINS: \$\{\{ steps\.action_pins\.outcome \}\}/);
|
||
assert.match(preflight, /^\s+check "пины сторонних Actions" "\$ACTION_PINS"$/m,
|
||
'исход попадает в вердикт, а не теряется в continue-on-error');
|
||
});
|
||
|
||
// #623: тонкие файлы в main вызывают тело из dev этого же репозитория. Исключение
|
||
// обязано быть узким: другой ref, другой репозиторий или файл вне `_*.yml`
|
||
// остаются находкой, иначе «@dev» стал бы дверью для любой перемещаемой ссылки.
|
||
test('#623: тело этого репозитория по ссылке @dev пина не требует, но требует причины', () => {
|
||
const own = 'Matysh/houseplan-card/.github/workflows/_process.yml@dev';
|
||
assert.equal(isOwnDevReusable(own), true);
|
||
assert.deepEqual(auditWorkflowSource('x.yml', ` uses: ${own} # #623: тело из dev\n`), []);
|
||
const bare = auditWorkflowSource('x.yml', ` uses: ${own}\n`);
|
||
assert.equal(bare.length, 1);
|
||
assert.match(bare[0], /без комментария с причиной/);
|
||
});
|
||
|
||
test('#623: исключение @dev не распространяется на другие ref, репозитории и пути', () => {
|
||
for (const spec of [
|
||
'Matysh/houseplan-card/.github/workflows/_process.yml@main',
|
||
'Matysh/houseplan-card/.github/workflows/_process.yml@issue/623-x',
|
||
'Matysh/houseplan-card/.github/workflows/process.yml@dev',
|
||
'Matysh/houseplan-card/scripts/_x.yml@dev',
|
||
'Matysh/houseplan-card-fork/.github/workflows/_process.yml@dev',
|
||
'Other/houseplan-card/.github/workflows/_process.yml@dev',
|
||
'actions/checkout@dev',
|
||
]) {
|
||
assert.equal(isOwnDevReusable(spec), false, spec);
|
||
const found = auditWorkflowSource('x.yml', ` uses: ${spec} # comment\n`);
|
||
assert.equal(found.length, 1, spec);
|
||
assert.match(found[0], /не закреплён полным SHA/, spec);
|
||
}
|
||
});
|