mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
infra(process): bundle and review index change only on the way to dev (#657)
Решения владельца: 1б — индекс ревью не пересобирается в ветке задачи, только коммитами, идущими в dev; 2б — бандл меняет только кандидат беты/релиза, стенд dev берёт его из артефакта Validate. - scripts/bundle-policy.mjs: коммит, трогающий dist/** или custom_components/houseplan/frontend/**, обязан нести Release: (хук commit-msg и история в CI через validate-commit-provenance; коммиты с датой автора до 2026-09-27 не судятся); --verify судит целостность свежей сборки всегда, побайтовую сверку с закоммиченной копией — только на коммите, меняющем бандл, или кандидате; --clean. - release-prerelease: публикация отказывает, если отпечаток исходников в закоммиченном манифесте не равен отпечатку дерева (хотфикс поверх кандидата без пересборки). - bundle-sync: по умолчанию только demo/srv/assets; --release (npm run bundle:release) — ещё и custom_components. - rebase-on-dev: конфликт в бандле берёт копию dev, без пересборки и amend. - validate.yml: job dev_build публикует card-bundle головы dev в сиротскую ветку dev-build (scripts/dev-build.mjs); стенд накладывает её demo/stand/update-dev-bundle.sh. - _process.yml: индекс ревью больше не пересобирается при приведении к dev и при публикации документа в ветку задачи. - golden-wsl-artifact/golden-container: сборка перед съёмкой не считается правкой источника, после — bundle:clean. - test/bundle-tree-committed: судит закоммиченный снимок, не диск. - 11 мутантов в реестре; PROCESS/AGENTS/DEVELOPMENT/AUTHOR/REVIEWER. Issue: #657 User-Visible: no
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
import { compareBundleTrees, readBundleManifest, verifyBundleTree } from '../scripts/bundle-tree.mjs';
|
||||
|
||||
@@ -17,16 +19,42 @@ import { compareBundleTrees, readBundleManifest, verifyBundleTree } from '../scr
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const COPIES = ['dist', 'custom_components/houseplan/frontend'];
|
||||
|
||||
test('манифест бандла не ссылается в никуда, обе копии целы и равны (#349)', () => {
|
||||
for (const copy of COPIES) {
|
||||
// verifyBundleTree отвечает сразу на три вопроса: файл существует, его
|
||||
// sha256 совпадает с манифестом, путь не выходит за корень копии.
|
||||
assert.doesNotThrow(() => verifyBundleTree(resolve(ROOT, copy)), `${copy}: дерево бандла битое`);
|
||||
// #657: судится ЗАКОММИЧЕННЫЙ снимок, а не диск. С #657 бандл в дереве меняет
|
||||
// только кандидат, и после обычного `npm run build` отслеживаемый `dist/` на
|
||||
// диске законно новее `custom_components/houseplan/frontend` — сравнение диска
|
||||
// красило бы любую сборку. Снимок HEAD обязан быть цел и равен себе в обеих
|
||||
// копиях всегда: его ставит HACS и его публикует бета.
|
||||
function committedCopies() {
|
||||
const listed = spawnSync('git', ['-C', ROOT, 'ls-tree', '-r', '-z', '--name-only', 'HEAD', '--', ...COPIES], { encoding: 'utf8' });
|
||||
if (listed.status !== 0) return null;
|
||||
const root = mkdtempSync(resolve(tmpdir(), 'hp-committed-bundle-'));
|
||||
for (const path of listed.stdout.split('\0').filter(Boolean)) {
|
||||
const blob = spawnSync('git', ['-C', ROOT, 'show', `HEAD:${path}`], { maxBuffer: 64 * 1024 * 1024 });
|
||||
assert.equal(blob.status, 0, `git show HEAD:${path}`);
|
||||
mkdirSync(dirname(resolve(root, path)), { recursive: true });
|
||||
writeFileSync(resolve(root, path), blob.stdout);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
test('манифест бандла не ссылается в никуда, обе копии целы и равны (#349)', () => {
|
||||
const committed = committedCopies();
|
||||
// Без git (распакованный архив) — проверяется то, что лежит на диске, громко.
|
||||
if (!committed) console.log('ПРОПУЩЕНО: git недоступен, судится диск вместо закоммиченного снимка');
|
||||
const base = committed ?? ROOT;
|
||||
try {
|
||||
for (const copy of COPIES) {
|
||||
// verifyBundleTree отвечает сразу на три вопроса: файл существует, его
|
||||
// sha256 совпадает с манифестом, путь не выходит за корень копии.
|
||||
assert.doesNotThrow(() => verifyBundleTree(resolve(base, copy)), `${copy}: дерево бандла битое`);
|
||||
}
|
||||
assert.doesNotThrow(
|
||||
() => compareBundleTrees(resolve(base, COPIES[0]), resolve(base, COPIES[1])),
|
||||
'копии бандла разошлись: HACS ставит вторую, а сверяется первая',
|
||||
);
|
||||
} finally {
|
||||
if (committed) rmSync(committed, { recursive: true, force: true });
|
||||
}
|
||||
assert.doesNotThrow(
|
||||
() => compareBundleTrees(resolve(ROOT, COPIES[0]), resolve(ROOT, COPIES[1])),
|
||||
'копии бандла разошлись: HACS ставит вторую, а сверяется первая',
|
||||
);
|
||||
});
|
||||
|
||||
test('каждый файл манифеста отслеживается git, а не только лежит на диске (#349)', () => {
|
||||
@@ -46,13 +74,20 @@ test('каждый файл манифеста отслеживается git,
|
||||
const tracked = new Set(listed.stdout.split('\0').filter(Boolean));
|
||||
assert.ok(tracked.size, 'git ls-files не вернул ни одного файла — проверьте вызов');
|
||||
const missing = [];
|
||||
for (const copy of COPIES) {
|
||||
const manifest = readBundleManifest(resolve(ROOT, copy));
|
||||
assert.ok(manifest.files.length, `${copy}: манифест без файлов`);
|
||||
for (const file of manifest.files) {
|
||||
const relative = `${copy}/${file.path}`;
|
||||
if (!tracked.has(relative)) missing.push(relative);
|
||||
// Манифест — закоммиченный (#657): на диске после сборки лежит новый, с
|
||||
// хешами, которых в индексе законно нет.
|
||||
const committed = committedCopies();
|
||||
try {
|
||||
for (const copy of COPIES) {
|
||||
const manifest = readBundleManifest(resolve(committed ?? ROOT, copy));
|
||||
assert.ok(manifest.files.length, `${copy}: манифест без файлов`);
|
||||
for (const file of manifest.files) {
|
||||
const relative = `${copy}/${file.path}`;
|
||||
if (!tracked.has(relative)) missing.push(relative);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (committed) rmSync(committed, { recursive: true, force: true });
|
||||
}
|
||||
assert.deepEqual(missing, [],
|
||||
'манифест ссылается на файлы, которых нет в индексе git: собрано, но не закоммичено');
|
||||
|
||||
Reference in New Issue
Block a user