I let an agent built a pre-push hook according the parameters @novusnota lined out in #353.
Don't know how well it's suited to replace anything in CI since it doesn't include any error reporting to a centralized API yet. That's why I created this issue to track and share my work.
I will test it over the next week to see if it prevents me sufficiently from forgetting to check everything before pushing 🥲
Like always, feedback welcome! 🫡
It currently consists of two files and requires to run npm run prepare manually to initialize Git hooks with Husky, as prepare scripts are currently disabled.
.husky/pre-push
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run lint checks on changed files
# Usage: node scripts/lint-changed.mjs [baseRef]
# - baseRef: git reference to compare against (default: origin/main)
node scripts/lint-changed.mjs origin/main
scripts/lint-changed.mjs
import { execFileSync, execSync } from "node:child_process";
import { existsSync } from "node:fs";
/**
* @typedef {{ok: true} | {ok: false; error: string; exitCode: number}} LintResult
*/
const MD_FILE_REGEX = /\.(md|mdx)$/;
/**
* Get changed files for linting
* Uses git diff to find files that differ from the default branch
* @param {string} base - The base branch/ref to compare against
* @returns {string[]} Array of file paths
*/
function getChangedFiles(base = "origin/main") {
try {
const output = execSync(`git diff --name-only "${base}"...HEAD`, {
encoding: "utf-8",
}).trim();
if (output) {
return output.split("\n").filter(Boolean);
}
} catch (error) {
throw new Error(
`Failed to get changed files from git using ${base}...HEAD: ${error.message}`,
);
}
return [];
}
/**
* Filter files to only include markdown/mdx files
* @param {string[]} files - Array of file paths
* @param {boolean} checkExists - Whether to verify files exist
* @returns {string[]}
*/
function filterMarkdownFiles(files, checkExists = false) {
return files.filter(
(file) =>
file && MD_FILE_REGEX.test(file) && (!checkExists || existsSync(file)),
);
}
/**
* Run a verification check on files
* @param {string} checkName - Display name of the check
* @param {string[]} npmScriptArgs - npm script name and any script arguments
* @param {string[] | null} files - Array of file paths, or null for no file args
* @param {NodeJS.ProcessEnv} envOverrides - Environment overrides for the command
* @returns {LintResult}
*/
function verifyCheck(
checkName,
npmScriptArgs,
files = null,
envOverrides = {},
) {
if (Array.isArray(files) && files.length === 0) {
console.log(`✓ No files to check (${checkName})`);
return { ok: true };
}
try {
const npmArgs = ["run", ...npmScriptArgs];
if (Array.isArray(files)) {
console.log(`Checking ${checkName} for ${files.length} file(s)...`);
npmArgs.push("--", ...files);
} else {
console.log(`Checking ${checkName}...`);
}
execFileSync("npm", npmArgs, {
stdio: "inherit",
env: { ...process.env, ...envOverrides },
});
console.log(`✓ ${checkName} check passed`);
return { ok: true };
} catch (error) {
return {
ok: false,
error: `${checkName} check failed`,
exitCode: error.status || 1,
};
}
}
/**
* Run all lint checks in sequence
* @param {string[]} changedFiles - Array of file paths
* @returns {void}
*/
function runChecks(changedFiles) {
const markdownFiles = filterMarkdownFiles(changedFiles);
const checks = [
{
name: "formatting",
args: ["check:fmt:some"],
files: markdownFiles,
},
{
name: "spelling",
args: ["spell:some"],
files: changedFiles,
},
{
name: "broken links",
args: ["check:links"],
},
{
name: "navigation",
args: ["check:navigation"],
},
{
name: "redirects",
args: ["check:redirects"],
envOverrides: { ALLOW_NET: "true" },
},
];
for (const check of checks) {
const result = verifyCheck(
check.name,
check.args,
check.files ?? null,
check.envOverrides ?? {},
);
if (!result.ok) {
console.error(`\n✗ ${result.error}`);
process.exit(result.exitCode);
}
}
console.log("\n✓ All lint checks passed!");
process.exit(0);
}
/**
* Main linting function
* @param {string[]} args - Command line arguments
* @returns {void}
*/
function main(args) {
const baseRef = args[0] || "origin/main";
console.log(`Running lint checks (base: ${baseRef})...\n`);
let changedFiles;
try {
changedFiles = getChangedFiles(baseRef);
} catch (error) {
console.error(error.message);
process.exit(1);
}
if (changedFiles.length === 0) {
console.log("No files changed. Skipping lint checks.");
process.exit(0);
}
console.log(`Found ${changedFiles.length} changed file(s):\n`);
changedFiles.forEach((file) => console.log(` - ${file}`));
runChecks(changedFiles);
}
main(process.argv.slice(2));
I let an agent built a pre-push hook according the parameters @novusnota lined out in #353.
Don't know how well it's suited to replace anything in CI since it doesn't include any error reporting to a centralized API yet. That's why I created this issue to track and share my work.
I will test it over the next week to see if it prevents me sufficiently from forgetting to check everything before pushing 🥲
Like always, feedback welcome! 🫡
It currently consists of two files and requires to run
npm run preparemanually to initialize Git hooks with Husky, as prepare scripts are currently disabled..husky/pre-pushscripts/lint-changed.mjs