Files
houseplan-card/scripts/golden-container.mjs
Claudeandclaude[bot] 88c4e4e9ae 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
2026-09-26 07:28:23 +00:00

88 lines
4.9 KiB
JavaScript
Raw Permalink 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
/**
* Съёмка golden-кандидатов в пиновом образе Playwright (#334).
*
* node scripts/golden-container.mjs # capture
* node scripts/golden-container.mjs --mode=verify
* node scripts/golden-container.mjs --image=mcr.microsoft.com/playwright:v1.62.0-jammy
* node scripts/golden-container.mjs --dry-run # только напечатать команду
*
* Зачем. Прежде эталон принимался только из артефакта CI, и каждый визуальный
* фикс стоил двух полных прогонов: пуш, ожидание, скачивание артефакта,
* приёмка, второй пуш. Здесь картинки снимаются локально в том же образе, что
* стоит на раннере, — одинаковый Chromium и одинаковые шрифты.
*
* Гарантия при этом не на слове. `accept.mjs` принимает съёмку только если
* разошлись РОВНО объявленные сцены (`--expect-change`), а всё остальное
* совпало с принятыми эталонами. Если растеризация в образе всё-таки другая,
* разойдутся посторонние сцены с текстом — и приёмка будет запрещена с их
* перечислением. Поэтому даже неверно угаданный тег образа не может испортить
* эталоны: он может только не сработать.
*
* Почему node_modules прячется анонимным томом: в репозитории владельца он
* собран под Windows, а `npm ci` внутри контейнера поставил бы туда
* linux-бинарники и сломал бы хозяйское дерево. Том перекрывает каталог, и
* установка живёт только внутри контейнера.
*/
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const arg = (name, fallback = '') => {
const found = process.argv.find((item) => item.startsWith(`--${name}=`));
return found ? found.slice(name.length + 3) : fallback;
};
const lock = JSON.parse(readFileSync(resolve(ROOT, 'package-lock.json'), 'utf8'));
const pinned = lock.packages?.['node_modules/playwright']?.version;
if (!pinned) throw new Error('в package-lock.json не найдена залоченная версия playwright');
const image = arg('image', `mcr.microsoft.com/playwright:v${pinned}`);
const mode = arg('mode', 'capture');
if (!['capture', 'verify'].includes(mode)) throw new Error(`неизвестный режим: ${mode}`);
const dryRun = process.argv.includes('--dry-run');
// `CI=` пустой намеренно: отчёт должен честно говорить, что съёмка локальная.
const inner = [
'set -eu',
'npm ci --no-audit --no-fund',
'npm run bundle:sync',
`npm run golden:${mode}`,
// Сборка переписала отслеживаемый dist/ хозяйского дерева: с #657 его
// меняет только кандидат, поэтому после съёмки он возвращается.
'npm run bundle:clean',
].join(' && ');
const args = [
'run', '--rm', '--ipc=host',
'-v', `${ROOT}:/work`,
// Анонимный том перекрывает хозяйский node_modules — см. комментарий выше.
'-v', '/work/node_modules',
'-w', '/work',
'-e', 'CI=',
image, 'bash', '-lc', inner,
];
console.log(`Залоченный playwright: ${pinned}`);
console.log(`Образ: ${image}`);
console.log(`docker ${args.slice(0, -1).join(' ')} '${inner}'`);
if (dryRun) process.exit(0);
const docker = spawnSync('docker', ['--version'], { encoding: 'utf8' });
if (docker.status !== 0) {
console.error('docker недоступен. Съёмка возможна и без контейнера — в любой Linux-среде,'
+ ' включая WSL: правило приёмки одинаково и само отвергнет съёмку, если растеризация'
+ ' разойдётся с раннером. См. demo/golden/README.md.');
process.exit(2);
}
const run = spawnSync('docker', args, { stdio: 'inherit' });
if (run.status !== 0) {
console.error(`\nСъёмка в контейнере не удалась (код ${run.status}).`
+ ' Если docker не смог получить образ — укажите тег дистрибутива:'
+ ` --image=mcr.microsoft.com/playwright:v${pinned}-jammy`);
}
process.exit(run.status ?? 1);