mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 11:18:48 +00:00
Решения владельца: 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
103 lines
4.9 KiB
JavaScript
103 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Разложить собранный бандл по местам, которым он нужен (#255).
|
|
*
|
|
* Копий две с половиной. `demo/srv/assets` — рабочая копия стенда: её читают
|
|
* браузерные смоки, golden и съёмка скриншотов, в репозитории её нет (#255).
|
|
* `custom_components/houseplan/frontend` — та, что ставит HACS; с #657 она
|
|
* обновляется только для релизного кандидата (`--release`, `npm run
|
|
* bundle:release`): в остальных коммитах закоммиченный бандл законно отстаёт
|
|
* от исходников, и обычная задача его не трогает. Раньше «скопировать туда»
|
|
* жило шестью разными `cp` в воркфлоу и трижды в документации; когда копию
|
|
* забывали, смок врал согласованно (#236).
|
|
*
|
|
* node scripts/bundle-sync.mjs # dist → demo/srv/assets
|
|
* node scripts/bundle-sync.mjs --release # и в custom_components/houseplan/frontend
|
|
*/
|
|
import { createHash } from 'node:crypto';
|
|
import {
|
|
copyFileSync, existsSync, mkdirSync, readFileSync, rmSync,
|
|
} from 'node:fs';
|
|
import { dirname, relative, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
assertBundleManifest, assertOwnBundleTopology, orderedBundlePayload, verifyBundleTree,
|
|
} from './bundle-tree.mjs';
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const SOURCE_ROOT = resolve(ROOT, 'dist');
|
|
const MANIFEST_NAME = 'houseplan-assets.json';
|
|
const DEMO_TARGET = 'demo/srv/assets';
|
|
const RELEASE_TARGET = 'custom_components/houseplan/frontend';
|
|
const TARGETS = process.argv.includes('--release') ? [RELEASE_TARGET, DEMO_TARGET] : [DEMO_TARGET];
|
|
|
|
const manifestPath = resolve(SOURCE_ROOT, MANIFEST_NAME);
|
|
if (!existsSync(manifestPath)) {
|
|
console.error(`dist/${MANIFEST_NAME} не найден: сначала \`npm run build\``);
|
|
process.exit(1);
|
|
}
|
|
|
|
const parseManifest = (path) => {
|
|
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
return parsed;
|
|
};
|
|
const sourceManifest = assertBundleManifest(parseManifest(manifestPath), manifestPath);
|
|
// #537: this script materializes OUR dist, so the topology of the current
|
|
// build is judged here and nowhere in the shared loader-side validator.
|
|
assertOwnBundleTopology(sourceManifest, manifestPath);
|
|
const managedFiles = [MANIFEST_NAME, ...sourceManifest.files.map((file) => file.path)];
|
|
const sha256 = (path) => createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
const contained = (root, name) => {
|
|
const path = resolve(root, name);
|
|
const rel = relative(root, path);
|
|
if (!rel || rel.startsWith('..') || rel.includes(':')) {
|
|
throw new Error(`bundle path escapes target root: ${name}`);
|
|
}
|
|
return path;
|
|
};
|
|
|
|
for (const file of sourceManifest.files) {
|
|
const path = contained(SOURCE_ROOT, file.path);
|
|
if (!existsSync(path)) throw new Error(`manifest asset is missing: ${file.path}`);
|
|
if (sha256(path) !== file.sha256) throw new Error(`manifest hash mismatch: ${file.path}`);
|
|
}
|
|
verifyBundleTree(SOURCE_ROOT);
|
|
|
|
for (const target of TARGETS) {
|
|
const targetRoot = resolve(ROOT, target);
|
|
const oldManifestPath = resolve(targetRoot, MANIFEST_NAME);
|
|
let old = null;
|
|
if (existsSync(oldManifestPath)) {
|
|
// The first two-entry sync necessarily replaces a valid legacy one-entry
|
|
// manifest. Read only its old inventory here; the copied result below is
|
|
// validated against the complete current contract.
|
|
old = parseManifest(oldManifestPath);
|
|
if (old?.schema !== 1 || !Array.isArray(old.files)) {
|
|
throw new Error(`${oldManifestPath}: invalid previous House Plan bundle manifest`);
|
|
}
|
|
}
|
|
// Content-hashed dependencies first, both stable entries second, manifest
|
|
// last. This is offline materialization, not a promise of live atomicity;
|
|
// either stale entry has its own visible reload fallback for the copy window.
|
|
// The new manifest is never published before its complete payload, while an
|
|
// interrupted pre-manifest copy can still expose a cross-generation entry.
|
|
const payload = orderedBundlePayload(sourceManifest);
|
|
for (const name of payload) {
|
|
const source = contained(SOURCE_ROOT, name);
|
|
const destination = contained(targetRoot, name);
|
|
mkdirSync(dirname(destination), { recursive: true });
|
|
copyFileSync(source, destination);
|
|
}
|
|
copyFileSync(manifestPath, oldManifestPath);
|
|
for (const file of old?.files || []) {
|
|
if (!managedFiles.includes(file.path)) rmSync(contained(targetRoot, file.path), { force: true });
|
|
}
|
|
const copied = verifyBundleTree(targetRoot);
|
|
for (const file of copied.files) {
|
|
if (sha256(contained(targetRoot, file.path)) !== file.sha256) {
|
|
throw new Error(`${target}/${file.path}: copied hash mismatch`);
|
|
}
|
|
}
|
|
console.log(`бандл-дерево → ${target}`);
|
|
}
|