mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Promote the reproduced release lessons into harness enforcement: keep tracked native regeneration separate from artifact builds, separate Android build and upload authorization, use stable login readiness in Maestro, and retain the iOS 43 and Android 46 version increments.
123 lines
5.5 KiB
JavaScript
123 lines
5.5 KiB
JavaScript
import { access, readFile, readdir } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
const root = path.resolve(new URL('..', import.meta.url).pathname);
|
|
const errors = [];
|
|
const scoped = process.argv.includes('--scope');
|
|
|
|
async function exists(file) {
|
|
try { await access(file); return true; } catch { return false; }
|
|
}
|
|
|
|
async function walk(directory, files = []) {
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
if (['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', 'evidence', '.ralph'].includes(entry.name)) continue;
|
|
const target = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) await walk(target, files);
|
|
else files.push(target);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function fail(file, message, fix) {
|
|
errors.push(`${path.relative(root, file)}: ${message}\n Fix: ${fix}`);
|
|
}
|
|
|
|
const allFiles = await walk(root);
|
|
for (const file of allFiles) {
|
|
if (path.basename(file) === 'agents.md') {
|
|
fail(file, 'lowercase agent context is not discoverable consistently', 'rename it to AGENTS.md or move maintained detail under docs/');
|
|
}
|
|
if (!/\.(md|sql)$/.test(file)) continue;
|
|
const source = await readFile(file, 'utf8');
|
|
if (/^\s*[-*]\s*Use\s+.*apply_migration/im.test(source)) {
|
|
fail(file, 'instruction authorizes untracked remote migrations', 'create a committed apps/api/drizzle migration instead');
|
|
}
|
|
}
|
|
|
|
if (!scoped) {
|
|
const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
const mobilePackageFile = path.join(root, 'apps', 'mobile', 'package.json');
|
|
const mobilePackageJson = JSON.parse(await readFile(mobilePackageFile, 'utf8'));
|
|
const releaseBuildScripts = Object.entries(mobilePackageJson.scripts ?? {})
|
|
.filter(([name]) => /^release:(ios|android)(:preview)?$/.test(name));
|
|
for (const [name, command] of releaseBuildScripts) {
|
|
if (/\bexpo\s+prebuild\b/.test(command)) {
|
|
fail(
|
|
mobilePackageFile,
|
|
`${name} regenerates tracked native projects before packaging`,
|
|
'keep native regeneration as an explicit reviewed change and let the release script build the checked-in native sources',
|
|
);
|
|
}
|
|
if (/\b(?:fastlane|eas)\b[^\n]*(?:upload|submit)|\bupload_to_/.test(command)) {
|
|
fail(
|
|
mobilePackageFile,
|
|
`${name} combines build creation with store mutation`,
|
|
'keep build scripts artifact-only and expose upload or submission as a separately authorized command',
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const app of (await readdir(path.join(root, 'apps'), { withFileTypes: true })).filter((entry) => entry.isDirectory())) {
|
|
const packageFile = path.join(root, 'apps', app.name, 'package.json');
|
|
if (!(await exists(packageFile))) continue;
|
|
if (!packageJson.workspaces.includes(`apps/${app.name}`)) {
|
|
fail(packageFile, 'application is outside the root workspace', `add apps/${app.name} to package.json workspaces`);
|
|
}
|
|
}
|
|
|
|
const claude = await readFile(path.join(root, 'CLAUDE.md'), 'utf8');
|
|
if (claude.split('\n').length > 20 || !claude.includes('AGENTS.md')) {
|
|
fail(path.join(root, 'CLAUDE.md'), 'runtime-specific context has drifted from the universal map', 'keep CLAUDE.md as a short pointer to AGENTS.md');
|
|
}
|
|
|
|
const duplicateMigrations = path.join(root, 'supabase', 'migrations');
|
|
if (await exists(duplicateMigrations)) {
|
|
const entries = await readdir(duplicateMigrations);
|
|
if (entries.length) fail(duplicateMigrations, 'second migration tree can bypass deployment', 'move active migrations to apps/api/drizzle and remove the duplicate');
|
|
}
|
|
|
|
const cutoff = '20260201100806';
|
|
const migrationDir = path.join(root, 'apps', 'supabase', 'migrations');
|
|
for (const name of await readdir(migrationDir)) {
|
|
if (name.slice(0, 14) <= cutoff) continue;
|
|
const file = path.join(migrationDir, name);
|
|
const source = await readFile(file, 'utf8');
|
|
if (/https:\/\/[a-z0-9-]+\.supabase\.co/i.test(source)) {
|
|
fail(file, 'new migration hardcodes a remote Supabase project', 'resolve the function URL from environment-specific app_config');
|
|
}
|
|
}
|
|
|
|
const docsIndex = await readFile(path.join(root, 'docs', 'index.md'), 'utf8');
|
|
for (const match of docsIndex.matchAll(/\[[^\]]+\]\(([^)]+\.md)\)/g)) {
|
|
const target = path.resolve(root, 'docs', match[1]);
|
|
if (!(await exists(target))) fail(path.join(root, 'docs', 'index.md'), `broken documentation link ${match[1]}`, 'correct the link or restore the maintained document');
|
|
}
|
|
|
|
const mcpAuth = await readFile(path.join(root, 'apps', 'mcp', 'src', 'lib', 'auth.ts'), 'utf8');
|
|
if (/allowing all requests|query\(['"]apiKey/i.test(mcpAuth)) {
|
|
fail(path.join(root, 'apps', 'mcp', 'src', 'lib', 'auth.ts'), 'MCP authentication can leak or fail open', 'require a configured Bearer token and fail closed');
|
|
}
|
|
|
|
for (const directory of [
|
|
path.join(root, 'apps', 'mobile'),
|
|
path.join(root, 'apps', 'admin', 'src'),
|
|
path.join(root, 'apps', 'web'),
|
|
path.join(root, 'apps', 'mcp', 'src'),
|
|
]) {
|
|
for (const file of await walk(directory, [])) {
|
|
if (!/\.(?:ts|tsx|js|jsx)$/.test(file) || /(?:\.test\.|__tests__)/.test(file)) continue;
|
|
const source = await readFile(file, 'utf8');
|
|
if (/\/functions\/v1\/|\.functions\.invoke\s*\(/.test(source)) {
|
|
fail(file, 'runtime code calls the retired Supabase Edge Function data plane', 'call the standalone versioned API instead');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errors.length) {
|
|
console.error(`Harness invariant failures (${errors.length}):\n\n${errors.join('\n\n')}`);
|
|
process.exit(1);
|
|
}
|
|
console.log('Harness invariants passed');
|