Issue PR Reminder #36
Workflow file for this run
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Issue PR Reminder | |
on: | |
schedule: | |
- cron: '0 * * * *' # Runs every hour | |
workflow_dispatch: | |
jobs: | |
issue-pr-reminder: | |
runs-on: ubuntu-latest | |
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 }} | |
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 unassignIssueAfterDays = process.env.UNASSIGN_ISSUE_AFTER_DAYS || 14; | |
const createPrReminderAfterIssueAssignedMilliseconds = createPrReminderAfterIssueAssignedDays * 24 * 60 * 60 * 1000; | |
const createPrReminderBeforeIssueUnassignedMilliseconds = createPrReminderBeforeIssueUnassignedDays * 24 * 60 * 60 * 1000; | |
const unassignIssueAfterMilliseconds = unassignIssueAfterDays * 24 * 60 * 60 * 1000; | |
const now = Date.now(); | |
if(!createPrReminderEnabled) { | |
console.log('!!! Dry run, there are no changes made !!!'); | |
} | |
async function listOpenIssues() { | |
const issues = []; | |
let page = 1; | |
let hasNextPage = true; | |
while (hasNextPage) { | |
const issuesResponse = await github.rest.issues.listForRepo({ | |
owner, | |
repo, | |
state: 'open', | |
per_page: 100, | |
page | |
}); | |
issues.push(...issuesResponse.data); | |
hasNextPage = issuesResponse.headers.link && issuesResponse.headers.link.includes('rel="next"'); | |
page++; | |
} | |
return issues; | |
} | |
async function listIssueEvents(issue) { | |
const allEvents = []; | |
let page = 1; | |
let hasNextPage = true; | |
while (hasNextPage) { | |
const issuesResponse = await github.rest.issues.listEventsForTimeline({ | |
owner, | |
repo, | |
issue_number: issue.number, | |
per_page: 100, | |
page | |
}); | |
allEvents.push(...issuesResponse.data); | |
hasNextPage = issuesResponse.headers.link && issuesResponse.headers.link.includes('rel="next"'); | |
page++; | |
} | |
return allEvents.sort((a, b) => a.id > b.id); | |
} | |
async function listIssueComments(issue) { | |
const allComments = []; | |
let page = 1; | |
let hasNextPage = true; | |
while (hasNextPage) { | |
const issuesResponse = await github.rest.issues.listComments({ | |
owner, | |
repo, | |
issue_number: issue.number, | |
per_page: 100, | |
page | |
}); | |
allComments.push(...issuesResponse.data); | |
hasNextPage = issuesResponse.headers.link && issuesResponse.headers.link.includes('rel="next"'); | |
page++; | |
} | |
return allComments; | |
} | |
async function createPrReminder(assignee, issueNumber, comments, createPrReminderAfter, prReminder) { | |
// Check if it is time to create PR reminder | |
if (now < createPrReminderAfter) { | |
return false; | |
} | |
// Check if PR reminder has already been created | |
if (comments.some(comment => comment.body === prReminder)) { | |
return false; | |
} | |
// Create PR reminder | |
if(createPrReminderEnabled) { | |
await github.rest.issues.createComment({ | |
owner, | |
repo, | |
issue_number: issueNumber, | |
body: prReminder | |
}); | |
} | |
console.log(`Issue '${issueNumber}' PR reminder created for: ${assignee.login}`); | |
return true; | |
} | |
async function createPrReminders() { | |
const issues = await listOpenIssues(); | |
for (const issue of issues) { | |
const events = await listIssueEvents(issue); | |
const pullRequests = events | |
.filter(event => event.event === 'cross-referenced') | |
.map(event => event.source.issue) | |
.filter(issue => issue && issue.pull_request); | |
const comments = await listIssueComments(issue); | |
for (const assignee of issue.assignees) { | |
const assignedEvent = events.find(event => event.event === 'assigned' && event.assignee.login === assignee.login); | |
const issueAssignedAt = new Date(assignedEvent.created_at); | |
// Check if assignee has already opened a PR | |
const assigneePullRequest = pullRequests.find(pullRequest => pullRequest.user.login === assignee.login); | |
if(assigneePullRequest) { | |
continue; | |
} | |
// Create first PR reminder | |
const createFirstPrReminderAfter = new Date(issueAssignedAt.getTime() + createPrReminderAfterIssueAssignedMilliseconds); | |
const firstPrReminder = `@${assignee.login}, please open a draft PR linking this issue!`; | |
const isFirstPrReminderCreated = await createPrReminder(assignee, issue.number, comments, createFirstPrReminderAfter, firstPrReminder); | |
if(isFirstPrReminderCreated) { | |
continue; | |
} | |
// Create final PR reminder | |
const createFinalPrReminderAfter = new Date(issueAssignedAt.getTime() + unassignIssueAfterMilliseconds - createPrReminderBeforeIssueUnassignedMilliseconds); | |
const finalPrReminder = `@${assignee.login}, please open a draft PR linking this issue!`; | |
await createPrReminder(assignee, issue.number, comments, createFinalPrReminderAfter, finalPrReminder); | |
} | |
} | |
} | |
await createPrReminders(); |