Files
2026-07-13 13:22:34 +08:00

110 lines
3.3 KiB
JavaScript

async function getMaintainers({ github, context }) {
const collaborators = await github.paginate(github.rest.repos.listCollaborators, {
owner: context.repo.owner,
repo: context.repo.repo,
});
return collaborators
.filter(({ role_name }) => ["admin", "maintain"].includes(role_name))
.map(({ login }) => login)
.sort();
}
const EXEMPTION_RULES = [
// Exemption for GenAI evaluation PRs.
{
authors: ["alkispoly-db", "AveshCSingh", "danielseong1", "smoorjani", "SomtochiUmeh", "xsh310"],
allowedPatterns: [
/^mlflow\/genai\//,
/^tests\/genai\//,
/^docs\//,
/^mlflow\/entities\/(assessment|dataset|evaluation|scorer)/,
],
excludedPatterns: [/^mlflow\/genai\/(agent_server|git_versioning|prompts|optimize)\//],
},
// Exemption for UI PRs.
{
authors: ["daniellok-db", "danielseong1", "hubertzub-db"],
allowedPatterns: [/^mlflow\/server\/js\//],
},
];
function matchesAnyPattern(path, patterns) {
if (!patterns) {
return false;
}
return patterns.some((pattern) => pattern.test(path));
}
function isAllowedPath(path, rule) {
return (
matchesAnyPattern(path, rule.allowedPatterns) && !matchesAnyPattern(path, rule.excludedPatterns)
);
}
function isExempted(authorLogin, files) {
let filesToCheck = files;
for (const rule of EXEMPTION_RULES) {
if (rule.authors.includes(authorLogin)) {
filesToCheck = filesToCheck.filter(
({ filename, previous_filename }) =>
// Keep files where NOT all before/after file paths are allowed by the rule.
![filename, previous_filename].filter(Boolean).every((path) => isAllowedPath(path, rule))
);
if (filesToCheck.length === 0) {
return true;
}
}
}
return false;
}
function hasAnyApproval(reviews) {
return reviews.some(({ state }) => state === "APPROVED");
}
module.exports = async ({ github, context, core }) => {
const { pull_request: pr } = context.payload;
const authorLogin = pr?.user?.login;
if (authorLogin === "mlflow-app[bot]") {
return;
}
const maintainers = await getMaintainers({ github, context });
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const APPROVED_BOTS = new Set(["nailaopus[bot]"]);
const maintainerApproved = reviews.some(
({ state, user }) =>
state === "APPROVED" &&
// GitHub returns `user: null` on reviews from accounts that have since
// been deleted; skip them rather than crashing on `user.login`.
user &&
(maintainers.includes(user.login) ||
(user.type.toLowerCase() === "bot" && APPROVED_BOTS.has(user.login)))
);
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
if (isExempted(authorLogin, files)) {
if (!hasAnyApproval(reviews)) {
core.setFailed(
"PR from exempted author needs at least one approval (maintainer approval not required)."
);
}
return;
}
if (!maintainerApproved) {
const maintainerList = maintainers.join(", ");
const message = `This PR requires an approval from at least one of the core maintainers: ${maintainerList}.`;
core.setFailed(message);
}
};