storebound/tools/cli-key.ts

48 lines
2.9 KiB
TypeScript

#!/usr/bin/env bun
// The CLI key: one hash of everything that decides how the packed npm CLI behaves in the acceptance matrix
// (tools/acceptance.ts), with the version copies normalized. Two commits with the same key run the same CLI code; a
// version bump (tools/desktop-version.ts --set) does not change it. Acceptance results carry the key, so the results
// of a commit count for a version-only child (tools/release-candidate.ts) and the Mac reuses its cached macOS results
// (tools/release.ts) until the CLI code really changes.
// bun tools/cli-key.ts print the key of the working tree (tracked files only)
// In the key: the npm package's files (package.json "files") without the prose that ships with it (CHANGELOG.md,
// README.md, docs/*.md: the CLI never reads storebound's own docs), package.json, bun.lock and what the acceptance
// run itself uses (tools/acceptance.ts, tools/build-node.ts, tests/helpers, tests/build-fixtures.ts, tests/fixtures).
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { COPIES } from './desktop-version';
const ROOT = resolve(import.meta.dir, '..');
const PROSE = /^(CHANGELOG\.md|README\.md|docs\/[^/]+\.md)$/;
const EXTRA = ['package.json', 'bun.lock', 'tools/acceptance.ts', 'tools/build-node.ts', 'tools/cli-key.ts', 'tools/desktop-version.ts', 'tests/helpers', 'tests/build-fixtures.ts', 'tests/fixtures'];
const NORMAL = '0.0.0-cli-key';
/** The paths of the key (git pathspecs). Pure. */
export function keyPaths(pkgFiles: string[]): string[] {
return [...new Set([...pkgFiles.filter(f => f !== 'dist/storebound.mjs'), ...EXTRA])].sort();
}
/** One file's content in the key: the version copies with the version normalized. Pure. */
export function keyContent(file: string, text: Buffer): Buffer {
const copy = COPIES.find(c => c.file === file);
return copy ? Buffer.from(copy.write(text.toString('utf8'), NORMAL)) : text;
}
/** sha256 over (path, content) of every tracked file of the key, sorted. Pure given the reader. */
export function cliKeyOf(files: string[], read: (f: string) => Buffer): string {
const h = createHash('sha256');
for (const f of [...files].filter(f => !PROSE.test(f)).sort()) { h.update(`${f}\0`); h.update(keyContent(f, read(f))); h.update('\0'); }
return h.digest('hex').slice(0, 32);
}
export function cliKey(root = ROOT): string {
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { files: string[] };
const ls = spawnSync('git', ['ls-files', '-z', '--', ...keyPaths(pkg.files)], { cwd: root, encoding: 'utf8', maxBuffer: 64 << 20 });
if (ls.status !== 0) throw new Error(`git ls-files: ${ls.stderr}`);
const files = ls.stdout.split('\0').filter(f => f && existsSync(join(root, f)));
return cliKeyOf(files, f => readFileSync(join(root, f)));
}
if (import.meta.main) console.log(cliKey());