All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m10s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 11s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
188 lines
5.2 KiB
JavaScript
188 lines
5.2 KiB
JavaScript
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const crypto = require("crypto");
|
|
|
|
const repoRoot = path.resolve(__dirname, "..");
|
|
const canonicalDir = path.resolve(repoRoot, "packages", "db", "migrations");
|
|
const legacyDir = path.resolve(repoRoot, "backend", "migrations");
|
|
const defaultReportPath = path.resolve(legacyDir, "stale-sql-report.json");
|
|
|
|
function parseArgs(argv) {
|
|
const args = new Set(argv);
|
|
return {
|
|
write: args.has("--write"),
|
|
failOnStale: args.has("--fail-on-stale"),
|
|
help: args.has("--help"),
|
|
};
|
|
}
|
|
|
|
function ensureDirectoryExists(dirPath, label) {
|
|
if (!fs.existsSync(dirPath)) {
|
|
throw new Error(`${label} directory not found: ${dirPath}`);
|
|
}
|
|
}
|
|
|
|
function sha256File(filePath) {
|
|
const hash = crypto.createHash("sha256");
|
|
hash.update(fs.readFileSync(filePath));
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
function listFiles(dirPath) {
|
|
return fs
|
|
.readdirSync(dirPath)
|
|
.filter((name) => fs.statSync(path.join(dirPath, name)).isFile())
|
|
.sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
function listSqlFiles(dirPath) {
|
|
return listFiles(dirPath).filter((name) => name.toLowerCase().endsWith(".sql"));
|
|
}
|
|
|
|
function mapByNameWithHash(dirPath, names) {
|
|
const map = new Map();
|
|
for (const name of names) {
|
|
map.set(name, {
|
|
name,
|
|
path: path.join(dirPath, name),
|
|
sha256: sha256File(path.join(dirPath, name)),
|
|
});
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function buildReport() {
|
|
ensureDirectoryExists(canonicalDir, "Canonical migrations");
|
|
ensureDirectoryExists(legacyDir, "Legacy migrations");
|
|
|
|
const canonicalSql = listSqlFiles(canonicalDir);
|
|
const legacySql = listSqlFiles(legacyDir);
|
|
const legacyNonSql = listFiles(legacyDir).filter(
|
|
(name) => !name.toLowerCase().endsWith(".sql")
|
|
);
|
|
|
|
const canonicalMap = mapByNameWithHash(canonicalDir, canonicalSql);
|
|
const legacyMap = mapByNameWithHash(legacyDir, legacySql);
|
|
|
|
const staleFiles = [];
|
|
for (const legacyName of legacySql) {
|
|
const legacyFile = legacyMap.get(legacyName);
|
|
const canonicalFile = canonicalMap.get(legacyName);
|
|
|
|
if (!canonicalFile) {
|
|
staleFiles.push({
|
|
filename: legacyName,
|
|
status: "STALE_ONLY_IN_BACKEND",
|
|
backend_sha256: legacyFile.sha256,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (legacyFile.sha256 === canonicalFile.sha256) {
|
|
staleFiles.push({
|
|
filename: legacyName,
|
|
status: "STALE_DUPLICATE_OF_CANONICAL",
|
|
backend_sha256: legacyFile.sha256,
|
|
canonical_sha256: canonicalFile.sha256,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
staleFiles.push({
|
|
filename: legacyName,
|
|
status: "STALE_DIVERGED_FROM_CANONICAL",
|
|
backend_sha256: legacyFile.sha256,
|
|
canonical_sha256: canonicalFile.sha256,
|
|
});
|
|
}
|
|
|
|
const canonicalOnly = canonicalSql
|
|
.filter((name) => !legacyMap.has(name))
|
|
.map((name) => ({
|
|
filename: name,
|
|
status: "CANONICAL_ONLY",
|
|
canonical_sha256: canonicalMap.get(name).sha256,
|
|
}));
|
|
|
|
return {
|
|
generated_at: new Date().toISOString(),
|
|
canonical_dir: path.relative(repoRoot, canonicalDir),
|
|
legacy_dir: path.relative(repoRoot, legacyDir),
|
|
stale_sql_files: staleFiles,
|
|
canonical_only_sql_files: canonicalOnly,
|
|
legacy_non_sql_files: legacyNonSql,
|
|
summary: {
|
|
stale_total: staleFiles.length,
|
|
stale_only_in_backend_total: staleFiles.filter(
|
|
(f) => f.status === "STALE_ONLY_IN_BACKEND"
|
|
).length,
|
|
stale_duplicate_total: staleFiles.filter(
|
|
(f) => f.status === "STALE_DUPLICATE_OF_CANONICAL"
|
|
).length,
|
|
stale_diverged_total: staleFiles.filter(
|
|
(f) => f.status === "STALE_DIVERGED_FROM_CANONICAL"
|
|
).length,
|
|
canonical_only_total: canonicalOnly.length,
|
|
},
|
|
};
|
|
}
|
|
|
|
function printReport(report) {
|
|
console.log("Stale SQL Tracker");
|
|
console.log(`- Canonical: ${report.canonical_dir}`);
|
|
console.log(`- Legacy: ${report.legacy_dir}`);
|
|
console.log(`- Generated: ${report.generated_at}`);
|
|
console.log("");
|
|
|
|
console.log(`Stale SQL files in legacy dir: ${report.summary.stale_total}`);
|
|
for (const stale of report.stale_sql_files) {
|
|
console.log(` - ${stale.filename} :: ${stale.status}`);
|
|
}
|
|
|
|
console.log("");
|
|
console.log(`Canonical-only SQL files: ${report.summary.canonical_only_total}`);
|
|
for (const canonicalOnly of report.canonical_only_sql_files) {
|
|
console.log(` - ${canonicalOnly.filename}`);
|
|
}
|
|
|
|
console.log("");
|
|
console.log(`Legacy non-SQL files: ${report.legacy_non_sql_files.length}`);
|
|
for (const nonSql of report.legacy_non_sql_files) {
|
|
console.log(` - ${nonSql}`);
|
|
}
|
|
}
|
|
|
|
function writeReport(report) {
|
|
fs.writeFileSync(defaultReportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
console.log("");
|
|
console.log(`Wrote stale SQL report: ${path.relative(repoRoot, defaultReportPath)}`);
|
|
}
|
|
|
|
function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
if (options.help) {
|
|
console.log("Usage: node scripts/db-stale-sql-tracker.js [--write] [--fail-on-stale]");
|
|
process.exit(0);
|
|
}
|
|
|
|
const report = buildReport();
|
|
printReport(report);
|
|
|
|
if (options.write) {
|
|
writeReport(report);
|
|
}
|
|
|
|
if (options.failOnStale && report.summary.stale_total > 0) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exit(1);
|
|
}
|