Enforce config example drift checks in CI and release

- add `verify:config-example` script with tests to fail on missing/stale generated config artifacts
- run the verification in CI and release workflows, and document it in release/docs guidance
- fix docs-site Cloudflare Pages watch path to `docs-site/*` with regression coverage
This commit is contained in:
2026-03-10 20:06:41 -07:00
parent 5f320edab5
commit 9c7e02cbf0
17 changed files with 346 additions and 44 deletions

View File

@@ -14,3 +14,7 @@ test('ci workflow checks pull requests for required changelog fragments', () =>
assert.match(ciWorkflow, /bun run changelog:pr-check/);
assert.match(ciWorkflow, /skip-changelog/);
});
test('ci workflow verifies generated config examples stay in sync', () => {
assert.match(ciWorkflow, /bun run verify:config-example/);
});

View File

@@ -24,6 +24,10 @@ test('release workflow verifies a committed changelog section before publish', (
assert.match(releaseWorkflow, /bun run changelog:check/);
});
test('release workflow verifies generated config examples before packaging artifacts', () => {
assert.match(releaseWorkflow, /bun run verify:config-example/);
});
test('release workflow generates release notes from committed changelog output', () => {
assert.match(releaseWorkflow, /bun run changelog:release-notes/);
assert.ok(!releaseWorkflow.includes('git log --pretty=format:"- %s"'));
@@ -47,6 +51,10 @@ test('release package scripts disable implicit electron-builder publishing', ()
assert.match(packageJson.scripts['build:win:unsigned'] ?? '', /build-win-unsigned\.mjs/);
});
test('config example generation runs directly from source without unrelated bundle prerequisites', () => {
assert.equal(packageJson.scripts['generate:config-example'], 'bun run src/generate-config-example.ts');
});
test('windows release workflow publishes unsigned artifacts directly without SignPath', () => {
assert.match(releaseWorkflow, /Build unsigned Windows artifacts/);
assert.match(releaseWorkflow, /run: bun run build:win:unsigned/);

View File

@@ -0,0 +1,93 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import {
verifyConfigExampleArtifacts,
type ConfigExampleVerificationResult,
} from './verify-config-example';
function createWorkspace(name: string): string {
const baseDir = path.join(process.cwd(), '.tmp', 'verify-config-example-test');
fs.mkdirSync(baseDir, { recursive: true });
return fs.mkdtempSync(path.join(baseDir, `${name}-`));
}
function assertResult(
result: ConfigExampleVerificationResult,
expected: {
missingPaths?: string[];
stalePaths?: string[];
},
) {
assert.deepEqual(result.missingPaths, expected.missingPaths ?? []);
assert.deepEqual(result.stalePaths, expected.stalePaths ?? []);
}
test('verifyConfigExampleArtifacts reports repo config example when missing', () => {
const workspace = createWorkspace('missing-root');
const projectRoot = path.join(workspace, 'SubMiner');
fs.mkdirSync(projectRoot, { recursive: true });
try {
const result = verifyConfigExampleArtifacts({ cwd: projectRoot });
assertResult(result, {
missingPaths: [path.join(projectRoot, 'config.example.jsonc')],
});
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});
test('verifyConfigExampleArtifacts reports stale docs-site artifact when docs site exists', () => {
const workspace = createWorkspace('stale-docs-site');
const projectRoot = path.join(workspace, 'SubMiner');
const docsSiteRoot = path.join(projectRoot, 'docs-site');
fs.mkdirSync(projectRoot, { recursive: true });
fs.mkdirSync(path.join(docsSiteRoot, 'public'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'config.example.jsonc'), 'fresh\n');
fs.writeFileSync(path.join(docsSiteRoot, 'public', 'config.example.jsonc'), 'stale\n');
try {
const result = verifyConfigExampleArtifacts({
cwd: projectRoot,
template: 'fresh\n',
});
assertResult(result, {
stalePaths: [path.join(docsSiteRoot, 'public', 'config.example.jsonc')],
});
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});
test('verifyConfigExampleArtifacts passes when repo and docs-site artifacts match', () => {
const workspace = createWorkspace('matching-artifacts');
const projectRoot = path.join(workspace, 'SubMiner');
const docsSiteRoot = path.join(projectRoot, 'docs-site');
const template = '{\n "ok": true\n}\n';
fs.mkdirSync(projectRoot, { recursive: true });
fs.mkdirSync(path.join(docsSiteRoot, 'public'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'config.example.jsonc'), template);
fs.writeFileSync(path.join(docsSiteRoot, 'public', 'config.example.jsonc'), template);
try {
const result = verifyConfigExampleArtifacts({
cwd: projectRoot,
template,
});
assertResult(result, {});
assert.deepEqual(result.outputPaths, [
path.join(projectRoot, 'config.example.jsonc'),
path.join(docsSiteRoot, 'public', 'config.example.jsonc'),
]);
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,80 @@
import fs from 'node:fs';
import { DEFAULT_CONFIG, generateConfigTemplate } from './config';
import { resolveConfigExampleOutputPaths } from './generate-config-example';
export type ConfigExampleVerificationResult = {
docsSiteDetected: boolean;
missingPaths: string[];
outputPaths: string[];
stalePaths: string[];
template: string;
};
export function verifyConfigExampleArtifacts(options?: {
cwd?: string;
docsSiteDirName?: string;
template?: string;
deps?: {
existsSync?: (candidate: string) => boolean;
readFileSync?: (candidate: string, encoding: BufferEncoding) => string;
};
}): ConfigExampleVerificationResult {
const existsSync = options?.deps?.existsSync ?? fs.existsSync;
const readFileSync = options?.deps?.readFileSync ?? fs.readFileSync;
const template = options?.template ?? generateConfigTemplate(DEFAULT_CONFIG);
const outputPaths = resolveConfigExampleOutputPaths({
cwd: options?.cwd,
docsSiteDirName: options?.docsSiteDirName,
existsSync,
});
const missingPaths: string[] = [];
const stalePaths: string[] = [];
for (const outputPath of outputPaths) {
if (!existsSync(outputPath)) {
missingPaths.push(outputPath);
continue;
}
if (readFileSync(outputPath, 'utf-8') !== template) {
stalePaths.push(outputPath);
}
}
return {
docsSiteDetected: outputPaths.length > 1,
missingPaths,
outputPaths,
stalePaths,
template,
};
}
function main(): void {
const result = verifyConfigExampleArtifacts();
if (result.missingPaths.length === 0 && result.stalePaths.length === 0) {
console.log('[OK] config example artifacts verified');
for (const outputPath of result.outputPaths) {
console.log(` ${outputPath}`);
}
if (!result.docsSiteDetected) {
console.log(' docs-site not present; skipped docs artifact check');
}
return;
}
console.error('[FAIL] config example artifacts are out of sync');
for (const missingPath of result.missingPaths) {
console.error(` missing: ${missingPath}`);
}
for (const stalePath of result.stalePaths) {
console.error(` stale: ${stalePath}`);
}
console.error(' run: bun run generate:config-example');
process.exitCode = 1;
}
if (require.main === module) {
main();
}