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

185 lines
4.9 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, login }) =>
["admin", "maintain"].includes(role_name) ||
[
"alkispoly-db",
"AveshCSingh",
"danielseong1",
"smoorjani",
"SomtochiUmeh",
"xsh310",
].includes(login)
)
.map(({ login }) => login)
.sort();
}
async function getLinkedIssues({ github, owner, repo, prNumber }) {
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
createdAt
closingIssuesReferences(first: 10) {
nodes {
number
createdAt
}
}
}
}
}
`;
const result = await github.graphql(query, {
owner,
repo,
number: prNumber,
});
return {
prCreatedAt: result.repository.pullRequest.createdAt,
issues: result.repository.pullRequest.closingIssuesReferences.nodes,
};
}
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
async function findFirstMaintainerInIssueComments({
github,
owner,
repo,
issueNumber,
maintainers,
}) {
for await (const response of github.paginate.iterator(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
})) {
for (const comment of response.data) {
if (maintainers.has(comment.user.login)) {
return comment.user.login;
}
}
}
return null;
}
async function findMaintainerFromLinkedIssue({ github, owner, repo, prNumber, maintainers }) {
const { prCreatedAt, issues: linkedIssues } = await getLinkedIssues({
github,
owner,
repo,
prNumber,
});
if (linkedIssues.length !== 1 || !prCreatedAt) {
return null;
}
const linkedIssue = linkedIssues[0];
const prCreatedDate = new Date(prCreatedAt);
const issueCreatedDate = new Date(linkedIssue.createdAt);
if (prCreatedDate - issueCreatedDate > SEVEN_DAYS_MS) {
return null;
}
return findFirstMaintainerInIssueComments({
github,
owner,
repo,
issueNumber: linkedIssue.number,
maintainers,
});
}
module.exports = async ({ github, context, skipAssignment = false }) => {
const { owner, repo } = context.repo;
const maintainers = new Set(await getMaintainers({ github, context }));
// Get current time minus 200 minutes to look for recent comments and PRs
const lookbackTime = new Date(Date.now() - 200 * 60 * 1000);
// Use search API to find recently updated open PRs
const searchQuery = `repo:${owner}/${repo} is:pr is:open updated:>=${lookbackTime.toISOString()}`;
const searchResults = await github.paginate(github.rest.search.issuesAndPullRequests, {
q: searchQuery,
sort: "updated",
order: "desc",
});
console.log(`Scanning ${searchResults.length} recently updated PRs`);
for (const pr of searchResults) {
// Get recent comments and reviews
const issueComments = await github.rest.issues.listComments({
owner,
repo,
issue_number: pr.number,
since: lookbackTime.toISOString(),
});
const reviews = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr.number,
});
// Filter reviews by lookback time and extract authors
const recentReviews = reviews.data.filter((r) => new Date(r.submitted_at) > lookbackTime);
const commentAuthors = new Set([
...issueComments.data.map((c) => c.user.login),
...recentReviews.map((r) => r.user.login),
]);
// Use Set operations to find maintainers to assign
const prAuthor = pr.user.login;
const currentAssignees = new Set(pr.assignees.map((a) => a.login));
const excludeSet = new Set([prAuthor, ...currentAssignees]);
let maintainersToAssign = [...commentAuthors.intersection(maintainers).difference(excludeSet)];
// Fall back to linked issue comments if no maintainers found from recent PR activity
if (maintainersToAssign.length === 0) {
const maintainer = await findMaintainerFromLinkedIssue({
github,
owner,
repo,
prNumber: pr.number,
maintainers,
});
if (maintainer && !excludeSet.has(maintainer)) {
maintainersToAssign = [maintainer];
}
}
if (maintainersToAssign.length === 0) {
continue;
}
// Assign maintainers
if (!skipAssignment) {
await github.rest.issues.addAssignees({
owner,
repo,
issue_number: pr.number,
assignees: maintainersToAssign,
});
}
console.log(
`${skipAssignment ? "[DRY RUN] Would assign" : "Assigned"} [${maintainersToAssign.join(
", "
)}] to PR #${pr.number}`
);
}
console.log("Scan completed");
};