182 lines
5.2 KiB
JavaScript
182 lines
5.2 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const { findProjectRoot } = require("./project-root");
|
|
|
|
const DOC_PREFIXES = ["docs/"];
|
|
const DOC_FILES = new Set(["README.md", "CONTRIBUTING.md", "CHANGELOG.md", "walkthrough.md"]);
|
|
const INFRA_PREFIXES = [".github/", "tools/", "apps/"];
|
|
const INFRA_FILES = new Set(["package.json", "package-lock.json"]);
|
|
const REFERENCES_PREFIXES = ["docs/", ".github/", "tools/", "apps/", "data/"];
|
|
const REFERENCES_FILES = new Set([
|
|
"README.md",
|
|
"CONTRIBUTING.md",
|
|
"CHANGELOG.md",
|
|
"walkthrough.md",
|
|
"package.json",
|
|
"package-lock.json",
|
|
]);
|
|
|
|
function normalizeRepoPath(filePath) {
|
|
return String(filePath || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function loadWorkflowContract(startDir = __dirname) {
|
|
const projectRoot = findProjectRoot(startDir);
|
|
const configPath = path.join(projectRoot, "tools", "config", "generated-files.json");
|
|
const rawConfig = fs.readFileSync(configPath, "utf8");
|
|
const config = JSON.parse(rawConfig);
|
|
|
|
return {
|
|
projectRoot,
|
|
configPath,
|
|
derivedFiles: config.derivedFiles.map(normalizeRepoPath),
|
|
mixedFiles: config.mixedFiles.map(normalizeRepoPath),
|
|
releaseManagedFiles: config.releaseManagedFiles.map(normalizeRepoPath),
|
|
};
|
|
}
|
|
|
|
function getManagedFiles(contract, options = {}) {
|
|
const includeMixed = Boolean(options.includeMixed);
|
|
const includeReleaseManaged = Boolean(options.includeReleaseManaged);
|
|
const managedFiles = [...contract.derivedFiles];
|
|
|
|
if (includeMixed) {
|
|
managedFiles.push(...contract.mixedFiles);
|
|
}
|
|
|
|
if (includeReleaseManaged) {
|
|
managedFiles.push(...contract.releaseManagedFiles);
|
|
}
|
|
|
|
return [...new Set(managedFiles.map(normalizeRepoPath))];
|
|
}
|
|
|
|
function isDerivedFile(filePath, contract) {
|
|
return contract.derivedFiles.includes(normalizeRepoPath(filePath));
|
|
}
|
|
|
|
function isMixedFile(filePath, contract) {
|
|
return contract.mixedFiles.includes(normalizeRepoPath(filePath));
|
|
}
|
|
|
|
function isDocLikeFile(filePath) {
|
|
const normalized = normalizeRepoPath(filePath);
|
|
return normalized.endsWith(".md") || DOC_FILES.has(normalized) || DOC_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
}
|
|
|
|
function isInfraLikeFile(filePath) {
|
|
const normalized = normalizeRepoPath(filePath);
|
|
return (
|
|
INFRA_FILES.has(normalized) ||
|
|
INFRA_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
|
);
|
|
}
|
|
|
|
function classifyChangedFiles(changedFiles, contract) {
|
|
const categories = new Set();
|
|
const normalizedFiles = changedFiles.map(normalizeRepoPath).filter(Boolean);
|
|
|
|
for (const filePath of normalizedFiles) {
|
|
if (isDerivedFile(filePath, contract)) {
|
|
continue;
|
|
}
|
|
|
|
const isSkillPath = filePath.startsWith("skills/");
|
|
|
|
if (isSkillPath) {
|
|
categories.add("skill");
|
|
}
|
|
|
|
if (!isSkillPath && (isDocLikeFile(filePath) || isMixedFile(filePath, contract))) {
|
|
categories.add("docs");
|
|
}
|
|
|
|
if (isInfraLikeFile(filePath)) {
|
|
categories.add("infra");
|
|
}
|
|
}
|
|
|
|
const orderedCategories = ["skill", "docs", "infra"].filter((category) => categories.has(category));
|
|
let primaryCategory = "none";
|
|
if (orderedCategories.includes("infra")) {
|
|
primaryCategory = "infra";
|
|
} else if (orderedCategories.includes("skill")) {
|
|
primaryCategory = "skill";
|
|
} else if (orderedCategories.includes("docs")) {
|
|
primaryCategory = "docs";
|
|
}
|
|
|
|
return {
|
|
categories: orderedCategories,
|
|
primaryCategory,
|
|
};
|
|
}
|
|
|
|
function getDirectDerivedChanges(changedFiles, contract) {
|
|
return changedFiles
|
|
.map(normalizeRepoPath)
|
|
.filter(Boolean)
|
|
.filter((filePath) => isDerivedFile(filePath, contract));
|
|
}
|
|
|
|
function requiresReferencesValidation(changedFiles, contract) {
|
|
return changedFiles
|
|
.map(normalizeRepoPath)
|
|
.filter(Boolean)
|
|
.some((filePath) => {
|
|
if (isDerivedFile(filePath, contract) || isMixedFile(filePath, contract)) {
|
|
return true;
|
|
}
|
|
|
|
return (
|
|
REFERENCES_FILES.has(filePath) ||
|
|
REFERENCES_PREFIXES.some((prefix) => filePath.startsWith(prefix))
|
|
);
|
|
});
|
|
}
|
|
|
|
function extractChangelogSection(content, version) {
|
|
const headingExpression = new RegExp(`^## \\[${escapeRegExp(version)}\\].*$`, "m");
|
|
const headingMatch = headingExpression.exec(content);
|
|
if (!headingMatch) {
|
|
throw new Error(`CHANGELOG.md does not contain a section for version ${version}.`);
|
|
}
|
|
|
|
const startIndex = headingMatch.index;
|
|
const remainder = content.slice(startIndex + headingMatch[0].length);
|
|
const nextSectionRelativeIndex = remainder.search(/^## \[/m);
|
|
const endIndex =
|
|
nextSectionRelativeIndex === -1
|
|
? content.length
|
|
: startIndex + headingMatch[0].length + nextSectionRelativeIndex;
|
|
|
|
return `${content.slice(startIndex, endIndex).trim()}\n`;
|
|
}
|
|
|
|
function hasQualityChecklist(body) {
|
|
return /quality bar checklist/i.test(String(body || ""));
|
|
}
|
|
|
|
function hasIssueLink(body) {
|
|
return /(?:closes|fixes)\s+#\d+/i.test(String(body || ""));
|
|
}
|
|
|
|
module.exports = {
|
|
classifyChangedFiles,
|
|
extractChangelogSection,
|
|
getDirectDerivedChanges,
|
|
getManagedFiles,
|
|
hasIssueLink,
|
|
hasQualityChecklist,
|
|
isDerivedFile,
|
|
isMixedFile,
|
|
loadWorkflowContract,
|
|
normalizeRepoPath,
|
|
requiresReferencesValidation,
|
|
};
|