From 730b23b2d4bf5f62a1e3dd7c727de79684520efc Mon Sep 17 00:00:00 2001 From: csehatt741 Date: Wed, 19 Feb 2025 09:41:38 +0100 Subject: [PATCH] PR Review Reminder created --- .github/workflows/issue-pr-reminder.yaml | 35 ++--- .github/workflows/pr-review-reminder.yaml | 164 ++++++++++++++++++++++ 2 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/pr-review-reminder.yaml diff --git a/.github/workflows/issue-pr-reminder.yaml b/.github/workflows/issue-pr-reminder.yaml index 5204809d..ea3ddfed 100644 --- a/.github/workflows/issue-pr-reminder.yaml +++ b/.github/workflows/issue-pr-reminder.yaml @@ -11,18 +11,18 @@ jobs: steps: - uses: actions/github-script@v7 env: - CREATE_PR_REMINDER_ENABLED: ${{ vars.CREATE_PR_REMINDER_ENABLED }} - CREATE_PR_REMINDER_AFTER_ISSUE_ASSIGNED_DAYS: ${{ vars.CREATE_PR_REMINDER_AFTER_ISSUE_ASSIGNED_DAYS }} - CREATE_PR_REMINDER_BEFORE_ISSUE_UNASSIGNED_DAYS: ${{ vars.CREATE_PR_REMINDER_BEFORE_ISSUE_UNASSIGNED_DAYS }} + CREATE_ISSUE_PR_REMINDER_ENABLED: ${{ vars.CREATE_ISSUE_PR_REMINDER_ENABLED }} + CREATE_ISSUE_PR_REMINDER_AFTER_ISSUE_ASSIGNED_DAYS: ${{ vars.CREATE_ISSUE_PR_REMINDER_AFTER_ISSUE_ASSIGNED_DAYS }} + CREATE_ISSUE_PR_REMINDER_BEFORE_ISSUE_UNASSIGNED_DAYS: ${{ vars.CREATE_ISSUE_PR_REMINDER_BEFORE_ISSUE_UNASSIGNED_DAYS }} UNASSIGN_ISSUE_AFTER_DAYS: ${{ vars.UNASSIGN_ISSUE_AFTER_DAYS }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const owner = 'keyshade-xyz'; const repo = 'keyshade'; - const createPrReminderEnabled = process.env.CREATE_PR_REMINDER_ENABLED || false; - const createPrReminderAfterIssueAssignedDays = process.env.CREATE_PR_REMINDER_AFTER_ISSUE_ASSIGNED_DAYS || 2; - const createPrReminderBeforeIssueUnassignedDays = process.env.CREATE_PR_REMINDER_BEFORE_ISSUE_UNASSIGNED_DAYS || 2; + const createPrReminderEnabled = process.env.CREATE_ISSUE_PR_REMINDER_ENABLED || false; + const createPrReminderAfterIssueAssignedDays = process.env.CREATE_ISSUE_PR_REMINDER_AFTER_ISSUE_ASSIGNED_DAYS || 2; + const createPrReminderBeforeIssueUnassignedDays = process.env.CREATE_ISSUE_PR_REMINDER_BEFORE_ISSUE_UNASSIGNED_DAYS || 2; const unassignIssueAfterDays = process.env.UNASSIGN_ISSUE_AFTER_DAYS || 14; const createPrReminderAfterIssueAssignedMilliseconds = createPrReminderAfterIssueAssignedDays * 24 * 60 * 60 * 1000; const createPrReminderBeforeIssueUnassignedMilliseconds = createPrReminderBeforeIssueUnassignedDays * 24 * 60 * 60 * 1000; @@ -57,12 +57,12 @@ jobs: } async function listIssueEvents(issue) { - const allEvents = []; + const events = []; let page = 1; let hasNextPage = true; while (hasNextPage) { - const issuesResponse = await github.rest.issues.listEventsForTimeline({ + const eventsResponse = await github.rest.issues.listEventsForTimeline({ owner, repo, issue_number: issue.number, @@ -70,22 +70,22 @@ jobs: page }); - allEvents.push(...issuesResponse.data); + events.push(...eventsResponse.data); - hasNextPage = issuesResponse.headers.link && issuesResponse.headers.link.includes('rel="next"'); + hasNextPage = eventsResponse.headers.link && eventsResponse.headers.link.includes('rel="next"'); page++; } - return allEvents.sort((a, b) => a.id > b.id); + return events.sort((a, b) => a.id > b.id); } async function listIssueComments(issue) { - const allComments = []; + const comments = []; let page = 1; let hasNextPage = true; while (hasNextPage) { - const issuesResponse = await github.rest.issues.listComments({ + const commentsResponse = await github.rest.issues.listComments({ owner, repo, issue_number: issue.number, @@ -93,13 +93,13 @@ jobs: page }); - allComments.push(...issuesResponse.data); + comments.push(...commentsResponse.data); - hasNextPage = issuesResponse.headers.link && issuesResponse.headers.link.includes('rel="next"'); + hasNextPage = commentsResponse.headers.link && commentsResponse.headers.link.includes('rel="next"'); page++; } - return allComments; + return comments; } async function createPrReminder(assignee, issueNumber, comments, createPrReminderAfter, prReminder) { @@ -161,8 +161,9 @@ jobs: } // Create final PR reminder + const unassignIssueAfter = new Date(issueAssignedAt.getTime() + unassignIssueAfterMilliseconds); const createFinalPrReminderAfter = new Date(issueAssignedAt.getTime() + unassignIssueAfterMilliseconds - createPrReminderBeforeIssueUnassignedMilliseconds); - const finalPrReminder = `@${assignee.login}, please open a draft PR linking this issue!`; + const finalPrReminder = `@${assignee.login}, please open a draft PR linking this issue; otherwise you will be unassigned from this issue after ${unassignIssueAfter}!`; await createPrReminder(assignee, issue.number, comments, createFinalPrReminderAfter, finalPrReminder); } diff --git a/.github/workflows/pr-review-reminder.yaml b/.github/workflows/pr-review-reminder.yaml new file mode 100644 index 00000000..0cf54557 --- /dev/null +++ b/.github/workflows/pr-review-reminder.yaml @@ -0,0 +1,164 @@ +name: PR Review Reminder + +on: + schedule: + - cron: '0 * * * *' # Runs every hour + workflow_dispatch: + +jobs: + pr-review-reminder: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + env: + CREATE_PR_REVIEW_REMINDER_ENABLED: ${{ vars.CREATE_PR_REVIEW_REMINDER_ENABLED }} + CREATE_PR_REVIEW_REMINDER_AFTER_PR_OPENED_DAYS: ${{ vars.CREATE_PR_REVIEW_REMINDER_AFTER_PR_OPENED_DAYS }} + CREATE_PR_REVIEW_REMINDER_BEFORE_PR_CLOSED_DAYS: ${{ vars.CREATE_PR_REVIEW_REMINDER_BEFORE_PR_CLOSED_DAYS }} + CLOSE_PR_AFTER_DAYS: ${{ vars.CLOSE_PR_AFTER_DAYS }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = 'keyshade-xyz'; + const repo = 'keyshade'; + const createReviewReminderEnabled = process.env.CREATE_PR_REVIEW_REMINDER_ENABLED || false; + const createReviewReminderAfterPrOpenedDays = process.env.CREATE_PR_REVIEW_REMINDER_AFTER_PR_OPENED_DAYS || 2; + const createReviewReminderBeforePrClosedDays = process.env.CREATE_PR_REVIEW_REMINDER_BEFORE_PR_CLOSED_DAYS || 2; + const closePrAfterDays = process.env.CLOSE_PR_AFTER_DAYS || 14; + const createReviewReminderAfterPrOpenedMilliseconds = createReviewReminderAfterPrOpenedDays * 24 * 60 * 60 * 1000; + const createReviewReminderBeforePrClosedMilliseconds = createReviewReminderBeforePrClosedDays * 24 * 60 * 60 * 1000; + const closePrAfterMilliseconds = closePrAfterDays * 24 * 60 * 60 * 1000; + const now = Date.now(); + + if(!createReviewReminderEnabled) { + console.log('!!! Dry run, there are no changes made !!!'); + } + + async function listOpenPrs() { + const pullRequests = []; + let page = 1; + let hasNextPage = true; + + while (hasNextPage) { + const pullRequestsResponse = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + per_page: 100, + page + }); + + pullRequests.push(...pullRequestsResponse.data); + + hasNextPage = pullRequestsResponse.headers.link && pullRequestsResponse.headers.link.includes('rel="next"'); + page++; + } + + return pullRequests; + } + + async function listPrReviews(pullRequest) { + const reviews = []; + let page = 1; + let hasNextPage = true; + + while (hasNextPage) { + const reviewsResponse = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pullRequest.number, + per_page: 100, + page + }); + + reviews.push(...reviewsResponse.data); + + hasNextPage = reviewsResponse.headers.link && reviewsResponse.headers.link.includes('rel="next"'); + page++; + } + + return reviews; + } + + async function listPrComments(pullRequest) { + const comments = []; + let page = 1; + let hasNextPage = true; + + while (hasNextPage) { + const issuesResponse = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pullRequest.number, + per_page: 100, + page + }); + + comments.push(...issuesResponse.data); + + hasNextPage = issuesResponse.headers.link && issuesResponse.headers.link.includes('rel="next"'); + page++; + } + + return comments; + } + + async function createReviewReminder(user, pullRequestNumber, comments, createReviewReminderAfter, reviewReminder) { + // Check if it is time to create review reminder + if (now < createReviewReminderAfter) { + return false; + } + + // Check if review reminder has already been created + if (comments.some(comment => comment.body === reviewReminder)) { + return false; + } + + // Create review reminder + if(createReviewReminderEnabled) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pullRequestNumber, + body: reviewReminder + }); + } + + console.log(`PR '${pullRequestNumber}' review reminder created for: ${user.login}`); + + return true; + } + + async function createReviewReminders() { + const pullRequests = await listOpenPrs(); + + for (const pullRequest of pullRequests) { + const reviews = await listPrReviews(pullRequest); + const unapprovedReviews = reviews.filter(review => review.state !== 'APPROVED'); + const comments = await listPrComments(pullRequest); + const pullRequestCreatedAt = new Date(pullRequest.created_at); + + // Check if PR has unapproved reviews + if(unapprovedReviews.length === 0) { + continue; + } + + // Create first review reminder + const createFirstReviewReminderAfter = new Date(pullRequestCreatedAt.getTime() + createReviewReminderAfterPrOpenedMilliseconds); + const firstReviewReminder = `@${pullRequest.user.login}, please resolve all open reviews!`; + + const isFirstReviewReminderCreated = await createReviewReminder(pullRequest.user, pullRequest.number, comments, createFirstReviewReminderAfter, firstReviewReminder); + + if(isFirstReviewReminderCreated) { + continue; + } + + // Create final review reminder + const closePrAfter = new Date(issueAssignedAt.getTime() + closePrAfterMilliseconds); + const createFinalReviewReminderAfter = new Date(issueAssignedAt.getTime() + closePrAfterMilliseconds - createReviewReminderBeforePrClosedMilliseconds); + const finalReviewReminder = `@${pullRequest.user.login}, please resolve all open reviews; otherwise you will be unassigned from this issue after ${unassignIssueAfter}!`; + + await createReviewReminder(pullRequest.user, pullRequest.number, comments, createFinalReviewReminderAfter, finalReviewReminder); + } + } + + await createReviewReminders(); \ No newline at end of file