This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
/** Audit deterministic mini-tool package and text-file size limits. */
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const MIB = 1024 * 1024;
|
||||
const ZIP_LIMIT = 10 * MIB;
|
||||
const ZIP_RECOMMENDED = 2 * MIB;
|
||||
const TEXT_FILE_WARNING = 2 * MIB;
|
||||
const TEXT_TOTAL_WARNING = 5 * MIB;
|
||||
const TEXT_SUFFIXES = new Set([".html", ".css", ".js", ".json"]);
|
||||
|
||||
function formatSize(size) {
|
||||
return `${(size / MIB).toFixed(2)} MiB`;
|
||||
}
|
||||
|
||||
function walkDirectory(root) {
|
||||
const files = [];
|
||||
function walk(current) {
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
if (entry.name === ".git") continue;
|
||||
const target = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) walk(target);
|
||||
else if (entry.isFile()) files.push(target);
|
||||
}
|
||||
}
|
||||
walk(root);
|
||||
return files;
|
||||
}
|
||||
|
||||
function auditDirectory(root) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const files = walkDirectory(root);
|
||||
let textTotal = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (!TEXT_SUFFIXES.has(path.extname(file).toLowerCase())) continue;
|
||||
const size = fs.statSync(file).size;
|
||||
const name = path.relative(root, file).split(path.sep).join("/");
|
||||
textTotal += size;
|
||||
if (size > TEXT_FILE_WARNING) {
|
||||
warnings.push(`${name}: text file is ${formatSize(size)}; review parse and memory cost`);
|
||||
}
|
||||
}
|
||||
if (textTotal > TEXT_TOTAL_WARNING) {
|
||||
warnings.push(`uncompressed HTML/CSS/JS/JSON total is ${formatSize(textTotal)}; review for embedded databases or generated content`);
|
||||
}
|
||||
return { errors, warnings, count: files.length };
|
||||
}
|
||||
|
||||
function auditZipSize(file) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const size = fs.statSync(file).size;
|
||||
if (size > ZIP_LIMIT) errors.push(`${path.basename(file)}: zip is ${formatSize(size)}; hard limit is 10 MiB`);
|
||||
else if (size > ZIP_RECOMMENDED) warnings.push(`${path.basename(file)}: zip is ${formatSize(size)}; recommended target is 2 MiB`);
|
||||
return { errors, warnings, count: 1, note: "Node audit checks zip size only; artifact contents must be audited before packaging" };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const target = process.argv[2];
|
||||
if (!target) {
|
||||
console.error("Usage: node audit_artifact.mjs <artifact-directory-or-zip>");
|
||||
return 2;
|
||||
}
|
||||
const resolved = path.resolve(target);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
console.error(`ERROR: path not found: ${resolved}`);
|
||||
return 2;
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = fs.statSync(resolved).isDirectory()
|
||||
? auditDirectory(resolved)
|
||||
: auditZipSize(resolved);
|
||||
} catch (error) {
|
||||
console.error(`ERROR: cannot inspect ${resolved}: ${error.message}`);
|
||||
return 2;
|
||||
}
|
||||
|
||||
for (const message of result.errors) console.log(`ERROR: ${message}`);
|
||||
for (const message of result.warnings) console.log(`WARN: ${message}`);
|
||||
if (result.note) console.log(`NOTE: ${result.note}`);
|
||||
if (result.errors.length) {
|
||||
console.log(`FAILED: ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
|
||||
return 1;
|
||||
}
|
||||
console.log(`PASS: ${result.count} file(s), ${result.warnings.length} warning(s)`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
process.exitCode = main();
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit deterministic mini-tool package and text-file size limits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
MIB = 1024 * 1024
|
||||
ZIP_LIMIT = 10 * MIB
|
||||
ZIP_RECOMMENDED = 2 * MIB
|
||||
TEXT_FILE_WARNING = 2 * MIB
|
||||
TEXT_TOTAL_WARNING = 5 * MIB
|
||||
TEXT_SUFFIXES = {".html", ".css", ".js", ".json"}
|
||||
|
||||
|
||||
def format_size(size: int) -> str:
|
||||
return f"{size / MIB:.2f} MiB"
|
||||
|
||||
|
||||
def audit_entries(entries: list[tuple[str, int]]) -> list[str]:
|
||||
warnings: list[str] = []
|
||||
text_total = 0
|
||||
for name, size in entries:
|
||||
if PurePosixPath(name).suffix.lower() not in TEXT_SUFFIXES:
|
||||
continue
|
||||
text_total += size
|
||||
if size > TEXT_FILE_WARNING:
|
||||
warnings.append(
|
||||
f"{name}: text file is {format_size(size)}; review parse and memory cost"
|
||||
)
|
||||
|
||||
if text_total > TEXT_TOTAL_WARNING:
|
||||
warnings.append(
|
||||
f"uncompressed HTML/CSS/JS/JSON total is {format_size(text_total)}; "
|
||||
"review for embedded databases or generated content"
|
||||
)
|
||||
return warnings
|
||||
|
||||
|
||||
def read_directory(path: Path) -> list[tuple[str, int]]:
|
||||
return [
|
||||
(file.relative_to(path).as_posix(), file.stat().st_size)
|
||||
for file in sorted(path.rglob("*"))
|
||||
if file.is_file() and ".git" not in file.relative_to(path).parts
|
||||
]
|
||||
|
||||
|
||||
def read_zip(path: Path) -> tuple[list[tuple[str, int]], list[str], list[str]]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
if path.stat().st_size > ZIP_LIMIT:
|
||||
errors.append(
|
||||
f"{path.name}: zip is {format_size(path.stat().st_size)}; hard limit is 10 MiB"
|
||||
)
|
||||
elif path.stat().st_size > ZIP_RECOMMENDED:
|
||||
warnings.append(
|
||||
f"{path.name}: zip is {format_size(path.stat().st_size)}; recommended target is 2 MiB"
|
||||
)
|
||||
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
infos = [info for info in archive.infolist() if not info.is_dir()]
|
||||
entries = [(info.filename, info.file_size) for info in infos]
|
||||
return entries, errors, warnings
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python3 audit_artifact.py <artifact-directory-or-zip>")
|
||||
return 2
|
||||
|
||||
path = Path(sys.argv[1]).resolve()
|
||||
if not path.exists():
|
||||
print(f"ERROR: path not found: {path}")
|
||||
return 2
|
||||
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
try:
|
||||
if path.is_dir():
|
||||
entries = read_directory(path)
|
||||
elif zipfile.is_zipfile(path):
|
||||
entries, zip_errors, zip_warnings = read_zip(path)
|
||||
errors.extend(zip_errors)
|
||||
warnings.extend(zip_warnings)
|
||||
else:
|
||||
print(f"ERROR: expected a directory or zip file: {path}")
|
||||
return 2
|
||||
except (OSError, zipfile.BadZipFile) as exc:
|
||||
print(f"ERROR: cannot inspect {path}: {exc}")
|
||||
return 2
|
||||
|
||||
warnings.extend(audit_entries(entries))
|
||||
|
||||
for message in errors:
|
||||
print(f"ERROR: {message}")
|
||||
for message in warnings:
|
||||
print(f"WARN: {message}")
|
||||
|
||||
if errors:
|
||||
print(f"FAILED: {len(errors)} error(s), {len(warnings)} warning(s)")
|
||||
return 1
|
||||
print(f"PASS: {len(entries)} file(s), {len(warnings)} warning(s)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user