Treat generated plugin mirrors and marketplace outputs as managed canonical artifacts so the main-branch sync bot can stage and commit them instead of failing on unmanaged drift. Ignore web-app coverage output during maintainer runs and update the mirrored Office unpack scripts so plugin copies stay aligned with the hardened source implementations.
198 lines
5.6 KiB
JavaScript
198 lines
5.6 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 matchesContractEntry(filePath, entry) {
|
|
const normalizedPath = normalizeRepoPath(filePath);
|
|
const normalizedEntry = normalizeRepoPath(entry);
|
|
|
|
if (!normalizedEntry) {
|
|
return false;
|
|
}
|
|
|
|
if (normalizedEntry.endsWith("/")) {
|
|
return normalizedPath.startsWith(normalizedEntry);
|
|
}
|
|
|
|
return normalizedPath === normalizedEntry;
|
|
}
|
|
|
|
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.some((entry) => matchesContractEntry(filePath, entry));
|
|
}
|
|
|
|
function isMixedFile(filePath, contract) {
|
|
return contract.mixedFiles.some((entry) => matchesContractEntry(filePath, entry));
|
|
}
|
|
|
|
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,
|
|
matchesContractEntry,
|
|
requiresReferencesValidation,
|
|
};
|