mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +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
127 lines
7.1 KiB
JavaScript
127 lines
7.1 KiB
JavaScript
// #657 (2б): стенд dev берёт бандл из артефакта Validate через ветку dev-build,
|
||
// а не из дерева dev, где бандл меняет только кандидат.
|
||
import assert from 'node:assert/strict';
|
||
import { execFileSync, spawnSync } from 'node:child_process';
|
||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { tmpdir } from 'node:os';
|
||
import test from 'node:test';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { createHash } from 'node:crypto';
|
||
|
||
import {
|
||
DEV_BUILD_MARKER, DEV_BUILD_TARGET, parseArgs, publishDevBuild,
|
||
} from '../scripts/dev-build.mjs';
|
||
|
||
const STAND = fileURLToPath(new URL('../demo/stand/update-dev-bundle.sh', import.meta.url));
|
||
const ENV = { GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' };
|
||
const git = (cwd, ...args) => execFileSync('git', args, {
|
||
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...ENV },
|
||
}).trim();
|
||
const sha256 = (text) => createHash('sha256').update(text).digest('hex');
|
||
|
||
function writeDist(root, card) {
|
||
const dir = join(root, 'dist');
|
||
mkdirSync(join(dir, 'houseplan-assets'), { recursive: true });
|
||
const entries = [['houseplan-card.js', card], ['houseplan-panel.js', 'panel'], ['houseplan-assets/c-HASH.js', `chunk ${card}`]];
|
||
for (const [name, value] of entries) writeFileSync(join(dir, name), value);
|
||
const files = entries.map(([path, value]) => ({
|
||
path, sha256: sha256(value), rawBytes: value.length, gzipBytes: 1,
|
||
isEntry: !path.includes('/'), imports: [], dynamicImports: [],
|
||
}));
|
||
writeFileSync(join(dir, 'houseplan-assets.json'), `${JSON.stringify({
|
||
schema: 1, fingerprint: 'a'.repeat(64), entry: 'houseplan-card.js', panelEntry: 'houseplan-panel.js',
|
||
initialViewFiles: ['houseplan-card.js'], initialViewGzipBytes: 1,
|
||
initialPanelFiles: ['houseplan-panel.js'], initialPanelGzipBytes: 1,
|
||
initialPanelOnlyFiles: ['houseplan-panel.js'], initialPanelOnlyGzipBytes: 1, files,
|
||
})}\n`);
|
||
}
|
||
|
||
function fixture() {
|
||
const root = mkdtempSync(join(tmpdir(), 'hp-dev-build-test-'));
|
||
const origin = join(root, 'origin.git');
|
||
const work = join(root, 'work');
|
||
git(root, 'init', '--bare', '-q', '-b', 'dev', origin);
|
||
git(root, 'clone', '-q', origin, work);
|
||
git(work, 'checkout', '-q', '-b', 'dev');
|
||
mkdirSync(join(work, DEV_BUILD_TARGET), { recursive: true });
|
||
writeFileSync(join(work, DEV_BUILD_TARGET, 'houseplan-card.js'), 'card of the last beta');
|
||
writeFileSync(join(work, DEV_BUILD_TARGET, 'houseplan-assets.json'), '{}\n');
|
||
writeFileSync(join(work, 'src.ts'), 'v1');
|
||
git(work, 'add', '-A');
|
||
git(work, 'commit', '-q', '-m', 'base');
|
||
git(work, 'push', '-q', '-u', 'origin', 'dev');
|
||
return { root, origin, work, head: git(work, 'rev-parse', 'HEAD') };
|
||
}
|
||
|
||
test('#657 parseArgs: значения по умолчанию и обе формы флагов', () => {
|
||
assert.deepEqual(parseArgs(['--sha', 'x']), {
|
||
sha: 'x', dist: 'dist', remote: 'origin', branch: 'dev-build', expectRef: 'refs/heads/dev', dryRun: false,
|
||
});
|
||
assert.equal(parseArgs(['--sha=y', '--dry-run']).dryRun, true);
|
||
assert.equal(parseArgs(['--sha=y']).sha, 'y');
|
||
});
|
||
|
||
test('#657 dev-build: одна ветка без истории с бандлом головы dev и SHA источника', () => {
|
||
const { root, origin, work, head } = fixture();
|
||
try {
|
||
writeDist(work, 'fresh card');
|
||
const first = publishDevBuild({ cwd: work, sha: head, log: () => {} });
|
||
assert.equal(first.published, true);
|
||
const branchTip = git(origin, 'rev-parse', 'refs/heads/dev-build');
|
||
assert.equal(git(origin, 'rev-list', '--count', branchTip), '1', 'коммит без родителей');
|
||
assert.equal(git(origin, 'show', `${branchTip}:${DEV_BUILD_TARGET}/houseplan-card.js`), 'fresh card');
|
||
const marker = JSON.parse(git(origin, 'show', `${branchTip}:${DEV_BUILD_MARKER}`));
|
||
assert.equal(marker.source, head);
|
||
assert.equal(marker.files, 3);
|
||
assert.equal(git(work, 'status', '--porcelain', '--', DEV_BUILD_TARGET), '',
|
||
'рабочее дерево и индекс источника не тронуты');
|
||
assert.equal(git(origin, 'rev-parse', 'refs/heads/dev'), head, 'dev не тронут');
|
||
|
||
// Второй прогон заменяет ветку целиком — истории не копится.
|
||
writeDist(work, 'fresher card');
|
||
publishDevBuild({ cwd: work, sha: head, log: () => {} });
|
||
const second = git(origin, 'rev-parse', 'refs/heads/dev-build');
|
||
assert.notEqual(second, branchTip);
|
||
assert.equal(git(origin, 'rev-list', '--count', second), '1');
|
||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||
});
|
||
|
||
test('#657 dev-build: dev ушёл вперёд — прогон не пушит устаревшую сборку', () => {
|
||
const { root, origin, work, head } = fixture();
|
||
try {
|
||
writeFileSync(join(work, 'src.ts'), 'v2');
|
||
git(work, 'commit', '-qam', 'next');
|
||
git(work, 'push', '-q', 'origin', 'dev');
|
||
writeDist(work, 'card of the older head');
|
||
const lines = [];
|
||
const result = publishDevBuild({ cwd: work, sha: head, log: (l) => lines.push(l) });
|
||
assert.deepEqual({ published: result.published, reason: result.reason }, { published: false, reason: 'stale' });
|
||
assert.equal(git(origin, 'for-each-ref', 'refs/heads/dev-build'), '', 'ветка не создана');
|
||
assert.match(lines.join('\n'), /публикует следующий прогон/);
|
||
assert.throws(() => publishDevBuild({ cwd: work, sha: 'HEAD', log: () => {} }), /full commit SHA/);
|
||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||
});
|
||
|
||
test('#657 стенд: update-dev-bundle.sh ставит бандл dev-build поверх рабочей копии и умеет вернуть её до pull', () => {
|
||
const { root, origin, work, head } = fixture();
|
||
try {
|
||
writeDist(work, 'fresh card');
|
||
publishDevBuild({ cwd: work, sha: head, log: () => {} });
|
||
const stand = join(root, 'stand');
|
||
git(root, 'clone', '-q', '-b', 'dev', origin, stand);
|
||
writeFileSync(join(stand, DEV_BUILD_TARGET, 'stale-orphan.js'), 'left over');
|
||
const run = spawnSync('bash', [STAND, stand], { encoding: 'utf8', env: { ...process.env, ...ENV } });
|
||
assert.equal(run.status, 0, run.stderr);
|
||
assert.equal(readFileSync(join(stand, DEV_BUILD_TARGET, 'houseplan-card.js'), 'utf8'), 'fresh card');
|
||
assert.equal(existsSync(join(stand, DEV_BUILD_TARGET, 'houseplan-assets/c-HASH.js')), true);
|
||
assert.equal(existsSync(join(stand, DEV_BUILD_TARGET, 'stale-orphan.js')), false, 'каталог заменён целиком');
|
||
assert.match(run.stdout, new RegExp(head.slice(0, 8)));
|
||
|
||
const reset = spawnSync('bash', [STAND, '--reset', stand], { encoding: 'utf8', env: { ...process.env, ...ENV } });
|
||
assert.equal(reset.status, 0, reset.stderr);
|
||
assert.equal(git(stand, 'status', '--porcelain'), '', 'до pull рабочая копия чистая');
|
||
assert.equal(readFileSync(join(stand, DEV_BUILD_TARGET, 'houseplan-card.js'), 'utf8'), 'card of the last beta');
|
||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||
});
|