mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Классификатор отдаёт выход mutants по scripts/mutation-gate.mjs, job changed_mutants получает третий дизъюнкт из ТЗ §2; fallback-ветки выставляют mutants=true. Тест AC7 отбирает бэкенд-мутанты по файлу патча. Issue: #475 User-Visible: no
97 lines
5.7 KiB
JavaScript
Executable File
97 lines
5.7 KiB
JavaScript
Executable File
// Классификация изменённых файлов для job `changes` в validate.yml (#473 AC8).
|
||
//
|
||
// Шаблоны жили inline в shell-шаге `classify` как `has('regex')`. Пока выходов
|
||
// было три, это терпимо; с диффозависимыми профилями перф-смока (#473) их
|
||
// пять, и вопрос «запустит ли правка src/iso-x.ts изометрический профиль»
|
||
// стал вопросом к workflow, на который нельзя ответить тестом — shell в YAML
|
||
// не исполняется локально. Теперь ответ даёт функция, а shell только
|
||
// переписывает её вывод в `$GITHUB_OUTPUT`.
|
||
//
|
||
// Контракт неизменен: каждый выход — строка 'true'/'false', как её и читают
|
||
// условия `if: needs.changes.outputs.X == 'true'`. Fallback «полный прогон
|
||
// без классификации» (--all) выставляет все выходы в 'true' — в том числе те,
|
||
// что появятся позже: пропущенный ключ в fallback-е означал бы, что job
|
||
// с этим условием молча не запускается ровно в тех прогонах, где база
|
||
// недоказуема и проверять надо всё.
|
||
|
||
import { readFileSync } from 'node:fs';
|
||
|
||
export const CLASSIFIERS = {
|
||
frontend: /^(src\/|demo\/|test\/|dist\/|custom_components\/houseplan\/frontend\/|package(-lock)?\.json$|rollup\.config\.mjs$|tsconfig)/,
|
||
backend: /^(custom_components\/.*\.py$|tests_backend\/|scripts\/support-relay\/|pytest\.ini$)/,
|
||
integration: /^(custom_components\/houseplan\/manifest\.json$|hacs\.json$|custom_components\/.*\.py$|custom_components\/.*\/translations\/)/,
|
||
// Перф-смок (#473 §5): изометрический профиль — при правке изометрии,
|
||
// профиль взаимодействия — при правке живого пути и оркестраторов кадра.
|
||
// Только `src/**`: тесты и демо кадр не замедляют.
|
||
perf_iso: /^src\/iso-[^/]+\.ts$/,
|
||
perf_interaction: /^src\/(live-[^/]+|render-[^/]+|houseplan-render-lifecycle|houseplan-card)\.ts$/,
|
||
// Реестр мутантов сам по себе — вход гейта по диффу (#475 ревью r1): новый
|
||
// свидетель без правки в src/test иначе не проверялся бы до понедельника.
|
||
mutants: /^scripts\/mutation-gate\.mjs$/,
|
||
};
|
||
|
||
export const OUTPUTS = Object.keys(CLASSIFIERS);
|
||
|
||
/** Список файлов → выходы job `changes` ('true'/'false' по каждому ключу). */
|
||
export function classifyChanges(files) {
|
||
const list = (Array.isArray(files) ? files : String(files).split('\n'))
|
||
.map((file) => file.trim()).filter(Boolean);
|
||
const result = {};
|
||
for (const [name, pattern] of Object.entries(CLASSIFIERS)) {
|
||
result[name] = list.some((file) => pattern.test(file)) ? 'true' : 'false';
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/** Fallback без классификации: всё прогоняется. */
|
||
export function classifyAll() {
|
||
return Object.fromEntries(OUTPUTS.map((name) => [name, 'true']));
|
||
}
|
||
|
||
/**
|
||
* Нужен ли полный набор тяжёлых job — смоки, golden, performance_smoke (#479).
|
||
*
|
||
* На обычном пуше они не идут: за всё время они не ловили дефект в момент
|
||
* ревью, ловили при подготовке беты, а стоили ~6 минут критического пути на
|
||
* каждую итерацию. Полный набор идёт там, где он и нужен:
|
||
* - кандидат беты/релиза — head-коммит несёт трейлер `Release:` (класс D
|
||
* без него и так невалиден, а publish-prerelease требует трейлер явно);
|
||
* - `workflow_dispatch` с `full=true` — ночной прогон (nightly.yml) и ручной;
|
||
* - pull_request — там Validate единственный сигнал.
|
||
*/
|
||
export function heavyGatesRequested({ eventName, headMessage, fullInput } = {}) {
|
||
if (eventName === 'pull_request') return true;
|
||
if (eventName === 'workflow_dispatch') return String(fullInput) === 'true';
|
||
if (eventName === 'schedule') return true;
|
||
return hasReleaseTrailer(headMessage);
|
||
}
|
||
|
||
/** Трейлер `Release: vX.Y.Z` в конце сообщения коммита — признак кандидата. */
|
||
export function hasReleaseTrailer(message) {
|
||
return /^Release:\s*v?\d+\.\d+\.\d+\S*\s*$/m.test(String(message || ''));
|
||
}
|
||
|
||
/** Формат `$GITHUB_OUTPUT`. */
|
||
export function formatOutputs(outputs) {
|
||
return OUTPUTS.map((name) => `${name}=${outputs[name]}`).join('\n') + '\n';
|
||
}
|
||
|
||
const invokedDirectly = process.argv[1]
|
||
&& import.meta.url === new URL(`file://${process.argv[1]}`).href;
|
||
if (invokedDirectly) {
|
||
if (process.argv.includes('--heavy')) {
|
||
// Отдельный вызов: у `heavy` другие входы (событие, сообщение head-коммита),
|
||
// и на dev он нужен даже там, где классификация путей выключена.
|
||
const heavy = heavyGatesRequested({
|
||
eventName: process.env.EVENT_NAME,
|
||
headMessage: process.env.HEAD_MESSAGE,
|
||
fullInput: process.env.FULL_INPUT,
|
||
});
|
||
process.stdout.write(`heavy=${heavy ? 'true' : 'false'}\n`);
|
||
} else {
|
||
const all = process.argv.includes('--all');
|
||
const outputs = all ? classifyAll() : classifyChanges(readFileSync(0, 'utf8'));
|
||
process.stdout.write(formatOutputs(outputs));
|
||
}
|
||
}
|