hotfix: guard staged asset commits
This commit is contained in:
95
scripts/guard-staged-assets.mjs
Executable file
95
scripts/guard-staged-assets.mjs
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const allowAssetCommit = process.env.ALLOW_ASSET_COMMIT === '1';
|
||||
const maxBinarySizeBytes = 5 * 1024 * 1024;
|
||||
const tmpCapturePattern = /^tmp-.*\.(png|jpe?g|webp|gif|mp4|mov|webm)$/i;
|
||||
const binaryAssetPattern = /\.(png|jpe?g|webp|gif|svg|mp4|mov|webm|pdf|zip|7z)$/i;
|
||||
|
||||
const blockedAssetPrefixes = ['public/assets/'];
|
||||
|
||||
function getStagedPaths() {
|
||||
const output = execFileSync(
|
||||
'git',
|
||||
['diff', '--cached', '--name-only', '--diff-filter=ACMR'],
|
||||
{ cwd: repoRoot, encoding: 'utf8' }
|
||||
);
|
||||
|
||||
return output
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function toDisplaySize(bytes) {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function isBlockedAssetPath(filePath) {
|
||||
return blockedAssetPrefixes.some((prefix) => filePath.startsWith(prefix));
|
||||
}
|
||||
|
||||
function main() {
|
||||
const stagedPaths = getStagedPaths();
|
||||
const violations = [];
|
||||
|
||||
for (const relativePath of stagedPaths) {
|
||||
const absolutePath = path.join(repoRoot, relativePath);
|
||||
let stats;
|
||||
|
||||
try {
|
||||
stats = statSync(absolutePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!stats.isFile()) continue;
|
||||
|
||||
const baseName = path.basename(relativePath);
|
||||
const isTmpCapture = tmpCapturePattern.test(baseName);
|
||||
const isBinaryAsset = binaryAssetPattern.test(relativePath);
|
||||
const isOversizedBinary = isBinaryAsset && stats.size > maxBinarySizeBytes;
|
||||
const isBlockedAsset = isBinaryAsset && isBlockedAssetPath(relativePath);
|
||||
|
||||
if (!isTmpCapture && !isOversizedBinary && !isBlockedAsset) continue;
|
||||
|
||||
const reasons = [];
|
||||
if (isTmpCapture) reasons.push('temporary capture file');
|
||||
if (isBlockedAsset) reasons.push('asset path under public/assets');
|
||||
if (isOversizedBinary) {
|
||||
reasons.push(`binary file exceeds ${toDisplaySize(maxBinarySizeBytes)}`);
|
||||
}
|
||||
|
||||
violations.push({
|
||||
relativePath,
|
||||
size: stats.size,
|
||||
reasons,
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length === 0 || allowAssetCommit) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('');
|
||||
console.error('Blocked commit: staged asset files need an explicit override.');
|
||||
console.error('Set ALLOW_ASSET_COMMIT=1 only when the asset commit is intentional.');
|
||||
console.error('');
|
||||
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
`- ${violation.relativePath} (${toDisplaySize(violation.size)}): ${violation.reasons.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user