mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix: keep the docs fingerprint blind to the product version
Issue: #245 User-Visible: no
This commit is contained in:
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync, statSync } from 'node:fs';
|
||||
import { dirname, extname, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { sourceFingerprint } from './source-fingerprint.mjs';
|
||||
import { visualFingerprint } from './source-fingerprint.mjs';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const EXTERNAL = process.argv.includes('--external');
|
||||
@@ -129,7 +129,10 @@ if (!existsSync(manifestPath)) {
|
||||
} else {
|
||||
const manifest = JSON.parse(canonicalText(manifestPath));
|
||||
if (manifest.fixture !== 'synthetic-only') errors.push('screenshot manifest must declare synthetic-only fixture');
|
||||
if (manifest.sourceFingerprint !== sourceFingerprint(ROOT))
|
||||
// Версионно-нечувствительный отпечаток (#245): бамп версии не меняет ни одного
|
||||
// пикселя, поэтому не обязан требовать пересъёмки — иначе каждый релизный
|
||||
// коммит оставляет этот гейт красным.
|
||||
if (manifest.sourceFingerprint !== visualFingerprint(ROOT))
|
||||
errors.push('screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs');
|
||||
const scriptPath = resolve(ROOT, 'demo/docs/capture.mjs');
|
||||
if (manifest.captureScriptSha256 !== sha256(readFileSync(scriptPath)))
|
||||
|
||||
@@ -122,6 +122,19 @@ export const MUTANTS = [
|
||||
replace: ' const span = centre;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'docs-fingerprint-sees-product-version',
|
||||
guard: 'node --test --test-name-pattern="не трогает отпечаток скриншотов" '
|
||||
+ 'test/source-fingerprint.test.mjs',
|
||||
because: 'номер версии продукта на скриншотах не нарисован, и если отпечаток документации '
|
||||
+ 'снова начнёт его видеть, каждый релизный коммит будет оставлять job docs красным — '
|
||||
+ 'красный гейт, который «всегда такой», перестают читать (#245)',
|
||||
patches: [{
|
||||
file: 'scripts/source-fingerprint.mjs',
|
||||
find: ' ? (text) => text.split(version).join(\'0.0.0-product-version\')',
|
||||
replace: ' ? (text) => text',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'smoke-select-drops-registered-link',
|
||||
guard: 'node --test --test-name-pattern="держится на зарегистрированной связи" '
|
||||
|
||||
@@ -20,27 +20,69 @@ const BUILD_INPUTS = [
|
||||
'scripts/source-fingerprint.mjs',
|
||||
];
|
||||
|
||||
/** Stable digest of frontend sources plus the files that control their build. */
|
||||
export const sourceFingerprint = (root = process.cwd()) => {
|
||||
const sourceRoot = resolve(root, 'src');
|
||||
const hash = createHash('sha256');
|
||||
const fingerprintFiles = (root) => {
|
||||
const deterministicFixtureInputs = ['demo/fixtures', 'demo/golden']
|
||||
.map((name) => resolve(root, name))
|
||||
.filter(existsSync)
|
||||
.flatMap(sourceFiles)
|
||||
.filter((file) => file.endsWith('.mjs'));
|
||||
const files = [
|
||||
...sourceFiles(sourceRoot),
|
||||
return [
|
||||
...sourceFiles(resolve(root, 'src')),
|
||||
...deterministicFixtureInputs,
|
||||
...BUILD_INPUTS.map((name) => resolve(root, name)).filter(existsSync),
|
||||
].sort((a, b) => relative(root, a).localeCompare(relative(root, b)));
|
||||
};
|
||||
|
||||
const digest = (root, files, normalize) => {
|
||||
const hash = createHash('sha256');
|
||||
for (const file of files) {
|
||||
hash.update(relative(root, file).replaceAll('\\', '/'));
|
||||
hash.update('\0');
|
||||
// Git-canonical text, independent of core.autocrlf. Otherwise the injected
|
||||
// hash would make an otherwise identical Windows/Linux bundle differ.
|
||||
hash.update(readFileSync(file, 'utf8').replace(/\r\n?/g, '\n'));
|
||||
hash.update(normalize(readFileSync(file, 'utf8').replace(/\r\n?/g, '\n')));
|
||||
hash.update('\0');
|
||||
}
|
||||
return hash.digest('hex');
|
||||
};
|
||||
|
||||
/** Stable digest of frontend sources plus the files that control their build. */
|
||||
export const sourceFingerprint = (root = process.cwd()) =>
|
||||
digest(root, fingerprintFiles(root), (text) => text);
|
||||
|
||||
/** Номер версии продукта, как его знает package.json. */
|
||||
const productVersion = (root) => {
|
||||
const path = resolve(root, 'package.json');
|
||||
if (!existsSync(path)) return '';
|
||||
try {
|
||||
const version = JSON.parse(readFileSync(path, 'utf8')).version;
|
||||
return typeof version === 'string' ? version : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Тот же корпус, но без номера версии продукта (#245).
|
||||
*
|
||||
* Зачем понадобился второй отпечаток. Релизный коммит бампает версию в трёх
|
||||
* местах разом: `package.json`, `package-lock.json` и `CARD_VERSION` в
|
||||
* `src/houseplan-card.ts`. Для бандла и для переиспользования гейтов это
|
||||
* настоящее изменение — бандл действительно другой, и его нельзя считать
|
||||
* свежим; отпечаток обязан ехать. А для скриншотов документации номер версии
|
||||
* не значит ничего: он на них не нарисован. Общий отпечаток на два разных
|
||||
* вопроса давал дефект, из-за которого КАЖДЫЙ релизный коммит оставлял job
|
||||
* `docs` красным: пересъёмка шла до бампа, и записанное значение не совпадало
|
||||
* с закоммиченным деревом.
|
||||
*
|
||||
* Нормализуется ровно строка версии продукта — не любое похожее число:
|
||||
* версия зависимости в `package-lock.json` остаётся частью отпечатка, иначе
|
||||
* обновление зависимости перестало бы требовать пересъёмки.
|
||||
*/
|
||||
export const visualFingerprint = (root = process.cwd()) => {
|
||||
const version = productVersion(root);
|
||||
const normalize = version
|
||||
? (text) => text.split(version).join('0.0.0-product-version')
|
||||
: (text) => text;
|
||||
return digest(root, fingerprintFiles(root), normalize);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user