191 lines
6.1 KiB
JavaScript
191 lines
6.1 KiB
JavaScript
const ACTIVITY_WINDOW_MS = 14 * 24 * 60 * 60 * 1000;
|
|
const MAX_REPOS_TO_DISPLAY = 10;
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function getRecentActivity(github, username) {
|
|
const windowStart = new Date(Date.now() - ACTIVITY_WINDOW_MS);
|
|
const dateString = windowStart.toISOString().slice(0, 10);
|
|
const query = `type:pr author:${username} created:>${dateString}`;
|
|
const items = await github.paginate(github.rest.search.issuesAndPullRequests, {
|
|
q: query,
|
|
per_page: 100,
|
|
});
|
|
const repoCounts = new Map();
|
|
for (const item of items) {
|
|
const repoFullName = item.repository_url.replace("https://api.github.com/repos/", "");
|
|
if (!repoCounts.has(repoFullName)) {
|
|
repoCounts.set(repoFullName, { open: 0, closed: 0, merged: 0 });
|
|
}
|
|
const counts = repoCounts.get(repoFullName);
|
|
if (item.pull_request?.merged_at) {
|
|
counts.merged++;
|
|
} else if (item.state === "closed") {
|
|
counts.closed++;
|
|
} else {
|
|
counts.open++;
|
|
}
|
|
}
|
|
return { totalPRs: items.length, repoCount: repoCounts.size, repoBreakdown: repoCounts };
|
|
}
|
|
|
|
async function getRecentActivitySection(github, username) {
|
|
const { totalPRs, repoCount, repoBreakdown } = await getRecentActivity(github, username);
|
|
if (totalPRs === 0) {
|
|
return "";
|
|
}
|
|
const prLabel = totalPRs === 1 ? "PR" : "PRs";
|
|
const repoLabel = repoCount === 1 ? "repo" : "repos";
|
|
const total = ({ open, closed, merged }) => open + closed + merged;
|
|
const sortedRepos = [...repoBreakdown.entries()]
|
|
.sort((a, b) => total(b[1]) - total(a[1]))
|
|
.slice(0, MAX_REPOS_TO_DISPLAY);
|
|
const tableRows = sortedRepos
|
|
.map(
|
|
([repo, counts]) =>
|
|
`| [${repo}](https://github.com/${repo}/pulls/${username}) | ${counts.open} | ${
|
|
counts.closed
|
|
} | ${counts.merged} | ${total(counts)} |`
|
|
)
|
|
.join("\n");
|
|
const topNote = repoCount > MAX_REPOS_TO_DISPLAY ? ` (showing top ${MAX_REPOS_TO_DISPLAY})` : "";
|
|
return `
|
|
<details><summary>PR author's recent activity</summary>
|
|
|
|
In the last 14 days, @${username} opened **${totalPRs} ${prLabel}** across **${repoCount} ${repoLabel}**${topNote}:
|
|
|
|
| Repository | Open | Closed | Merged | Total |
|
|
| ---------- | ---- | ------ | ------ | ----- |
|
|
${tableRows}
|
|
|
|
</details>`;
|
|
}
|
|
|
|
async function getDcoCheck(github, owner, repo, sha) {
|
|
const backoffs = [0, 2, 4, 6, 8];
|
|
const numAttempts = backoffs.length;
|
|
for (const [index, backoff] of backoffs.entries()) {
|
|
await sleep(backoff * 1000);
|
|
const resp = await github.rest.checks.listForRef({
|
|
owner,
|
|
repo,
|
|
ref: sha,
|
|
app_id: 1861, // ID of the DCO check app
|
|
});
|
|
|
|
const { check_runs } = resp.data;
|
|
if (check_runs.length > 0 && check_runs[0].status === "completed") {
|
|
return check_runs[0];
|
|
}
|
|
console.log(`[Attempt ${index + 1}/${numAttempts}]`, "The DCO check hasn't completed yet.");
|
|
}
|
|
}
|
|
|
|
module.exports = async ({ context, github }) => {
|
|
const { owner, repo } = context.repo;
|
|
const { number: issue_number } = context.issue;
|
|
const { sha, label } = context.payload.pull_request.head;
|
|
const { user, body } = context.payload.pull_request;
|
|
const messages = [];
|
|
|
|
const title = "Install mlflow from this PR";
|
|
// Check if an install comment already exists
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner,
|
|
repo,
|
|
issue_number,
|
|
});
|
|
const installCommentExists = comments.some((comment) => comment.body.includes(title));
|
|
|
|
if (!installCommentExists) {
|
|
let activitySection = "";
|
|
const memberAssociations = ["MEMBER", "OWNER", "COLLABORATOR"];
|
|
if (
|
|
user.type !== "Bot" &&
|
|
!memberAssociations.includes(context.payload.pull_request.author_association)
|
|
) {
|
|
try {
|
|
activitySection = await getRecentActivitySection(github, user.login);
|
|
} catch (e) {
|
|
console.log("Failed to fetch recent activity:", e);
|
|
}
|
|
}
|
|
const devToolsComment = `
|
|
<details><summary>${title}</summary>
|
|
<p>
|
|
|
|
#### Install mlflow from this PR
|
|
|
|
\`\`\`bash
|
|
# mlflow
|
|
pip install git+https://github.com/mlflow/mlflow.git@refs/pull/${issue_number}/merge
|
|
# mlflow-skinny
|
|
pip install git+https://github.com/mlflow/mlflow.git@refs/pull/${issue_number}/merge#subdirectory=libs/skinny
|
|
\`\`\`
|
|
|
|
For Databricks, use the following command:
|
|
|
|
\`\`\`bash
|
|
%sh curl -LsSf https://raw.githubusercontent.com/mlflow/mlflow/HEAD/dev/install-skinny.sh | sh -s pull/${issue_number}/merge
|
|
\`\`\`
|
|
|
|
</p>
|
|
</details>
|
|
${activitySection}
|
|
`.trim();
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number,
|
|
body: devToolsComment,
|
|
});
|
|
}
|
|
|
|
// Exit early if the PR author is a bot
|
|
if (user.type === "Bot") {
|
|
return;
|
|
}
|
|
|
|
const dcoCheck = await getDcoCheck(github, owner, repo, sha);
|
|
if (dcoCheck && dcoCheck.conclusion !== "success") {
|
|
messages.push(
|
|
"#### ❌ DCO check\n\n" +
|
|
"The DCO check failed. " +
|
|
`Please sign off your commit(s) by following the instructions [here](${dcoCheck.html_url}). ` +
|
|
"See https://github.com/mlflow/mlflow/blob/master/CONTRIBUTING.md#sign-your-work for more " +
|
|
"details."
|
|
);
|
|
}
|
|
|
|
if (label.endsWith(":master")) {
|
|
messages.push(
|
|
"#### ❌ PR branch check\n\n" +
|
|
"This PR was filed from the master branch in your fork, which is not recommended " +
|
|
"and may cause our CI checks to fail. Please close this PR and file a new PR from " +
|
|
"a non-master branch."
|
|
);
|
|
}
|
|
|
|
if (!(body || "").includes("How should the PR be classified in the release notes?")) {
|
|
messages.push(
|
|
"#### ❌ Invalid PR template\n\n" +
|
|
"The PR description is missing required sections. " +
|
|
"Please use the [PR template](https://raw.githubusercontent.com/mlflow/mlflow/master/.github/pull_request_template.md)."
|
|
);
|
|
}
|
|
|
|
if (messages.length > 0) {
|
|
const body =
|
|
`@${user.login} Thank you for the contribution! Could you fix the following issue(s)? Otherwise, this PR may be automatically closed.\n\n` +
|
|
messages.join("\n\n");
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number,
|
|
body,
|
|
});
|
|
}
|
|
};
|