cache-apt-pkgs-action/dev_scripts/verify_configs.mjs

268 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
import {
ROOT_DIR,
fail,
logError,
logInfo,
logSuccess,
runCaptureOutput,
REPO_SLUG,
} from "./lib.mjs";
import packageJson from "../package.json" with { type: "json" };
import vscodeSettingsJson from "../.vscode/settings.json" with { type: "json" };
import tsconfigJson from "../tsconfig.json" with { type: "json" };
import typedocJson from "../typedoc.json" with { type: "json" };
function tokenizePath(pathExpression) {
const tokens = [];
let index = 0;
while (index < pathExpression.length) {
const ch = pathExpression[index];
if (ch === ".") {
index += 1;
continue;
}
if (ch === "[") {
const endIndex = pathExpression.indexOf("]", index);
if (endIndex < 0) {
throw new Error(`Invalid path expression '${pathExpression}'`);
}
const indexText = pathExpression.slice(index + 1, endIndex).trim();
if (!/^\d+$/.test(indexText)) {
throw new Error(`Invalid array index in path '${pathExpression}'`);
}
tokens.push(Number(indexText));
index = endIndex + 1;
continue;
}
let endIndex = index;
while (
endIndex < pathExpression.length &&
pathExpression[endIndex] !== "." &&
pathExpression[endIndex] !== "["
) {
endIndex += 1;
}
tokens.push(pathExpression.slice(index, endIndex));
index = endIndex;
}
return tokens;
}
function getByPath(root, pathExpression) {
const tokens = tokenizePath(pathExpression);
let cursor = root;
for (const token of tokens) {
if (typeof token === "number") {
if (!Array.isArray(cursor) || token < 0 || token >= cursor.length) {
return { found: false, value: undefined };
}
cursor = cursor[token];
continue;
}
if (cursor === null || typeof cursor !== "object") {
return { found: false, value: undefined };
}
if (!Object.prototype.hasOwnProperty.call(cursor, token)) {
return { found: false, value: undefined };
}
cursor = cursor[token];
}
return { found: true, value: cursor };
}
function createErrorCollector(fileLabel) {
const errors = [];
return {
errors,
add(message) {
errors.push(`${fileLabel}: ${message}`);
},
};
}
function valueType(value) {
if (Array.isArray(value)) {
return "array";
}
if (value === null) {
return "null";
}
return typeof value;
}
function expectFieldType(doc, fieldPath, expectedType, addError) {
const result = getByPath(doc, fieldPath);
if (!result.found) {
addError(`missing required field '${fieldPath}'`);
return;
}
const actualType = valueType(result.value);
if (actualType !== expectedType) {
addError(
`field '${fieldPath}' type '${actualType}' does not match expected '${expectedType}'`,
);
}
}
function expectFieldValue(
doc,
fieldPath,
expectedValue,
addError,
errorMessage,
) {
const result = getByPath(doc, fieldPath);
if (!result.found) {
addError(`missing required field '${fieldPath}'`);
return;
}
if (result.value !== expectedValue) {
addError(
`field '${fieldPath}' value '${result.value}' does not match expected '${expectedValue}'${errorMessage ? `: ${errorMessage}` : ""}`,
);
}
}
function validatePackageJson(json) {
const { errors, add } = createErrorCollector("package.json");
logInfo("Validating package.json...");
expectFieldValue(json, "name", "cache-apt-pkgs-action", add);
expectFieldValue(
json,
"version",
"0.0.0-semantically-released",
add,
"version is updated automatically by semantic-release and should not be changed manually",
);
expectFieldValue(json, "license", "Apache-2.0", add);
expectFieldValue(json, "repository.type", "git", add);
expectFieldValue(
json,
"repository.url",
`https://github.com/${REPO_SLUG}.git`,
add,
);
return errors;
}
function validateTsconfigJson(json) {
logInfo("Validating tsconfig.json...");
const { errors, add } = createErrorCollector("tsconfig.json");
expectFieldType(json, "extends", "string", add);
expectFieldType(json, "compilerOptions", "object", add);
return errors;
}
function validateTypedocJson(json) {
logInfo("Validating typedoc.json...");
const { errors, add } = createErrorCollector("typedoc.json");
expectFieldType(json, "entryPoints", "array", add);
expectFieldType(json, "out", "string", add);
return errors;
}
function validateVscodeSettingsJson(json) {
logInfo("Validating .vscode/settings.json...");
const { errors, add } = createErrorCollector(".vscode/settings.json");
expectFieldType(json, "chat.tools.terminal.autoApprove", "object", add);
expectFieldValue(json, "chat.useAgentsMdFile", true, add);
return errors;
}
function runGitNameOnly(args, cwd) {
const output = runCaptureOutput("git", args, {
cwd,
stdout: "pipe",
stderr: "pipe",
});
if (!output) {
return [];
}
return output
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);
}
function getProhibitedPathsChanges() {
const prohibitedPaths = ["docs/api"];
const staged = runGitNameOnly(
["diff", "--name-only", "--cached", "--", ...prohibitedPaths],
ROOT_DIR,
);
const unstaged = runGitNameOnly(
["diff", "--name-only", "--", ...prohibitedPaths],
ROOT_DIR,
);
const untracked = runGitNameOnly(
["ls-files", "--others", "--exclude-standard", "--", ...prohibitedPaths],
ROOT_DIR,
);
return [...new Set([...staged, ...unstaged, ...untracked])].sort(
(left, right) => left.localeCompare(right),
);
}
function main() {
const errors = [
...validatePackageJson(packageJson),
...validateTsconfigJson(tsconfigJson),
...validateTypedocJson(typedocJson),
...validateVscodeSettingsJson(vscodeSettingsJson),
];
const changedPaths = getProhibitedPathsChanges();
if (changedPaths.length > 0) {
errors.push(
`git-change-check: detected changes in prohibited paths (docs/api): ${changedPaths.join(", ")}`,
);
}
if (errors.length > 0) {
logError(`Found ${errors.length} validation issue(s):`);
for (const errorText of errors) {
logError(`- ${errorText}`);
}
fail("Validation failed.");
}
logSuccess("Validation passed.");
}
try {
main();
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}