Collect customer issues #9
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: Collect customer issues | |
on: | |
schedule: | |
- cron: "0 15 * * *" | |
workflow_dispatch: | |
jobs: | |
collect-issues-pending-triage: | |
name: Pending Triage | |
runs-on: ubuntu-latest | |
env: | |
DAYS: 30 | |
steps: | |
- name: Retrieve and format issues | |
id: retrieve-issues | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
// List of labels to filter by | |
const requiredLabels = [ | |
"triage", | |
]; | |
// List of core developers to exclude | |
const coreDevelopers = [ | |
"mikeldking", | |
"axiomofjoy", | |
"anticorrelator", | |
"cephalization", | |
"Parker-Stafford", | |
"Jgilhuly", | |
"RogerHYang", | |
]; | |
// Access the DAYS environment variable | |
const days = parseInt(process.env.DAYS || '7', 10); | |
// Calculate the cutoff date | |
const cutoffDate = new Date(); | |
cutoffDate.setDate(cutoffDate.getDate() - days); | |
// Fetch issues created since DAYS ago | |
const issues = await github.paginate( | |
github.rest.issues.listForRepo, | |
{ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
state: "open", | |
since: cutoffDate.toISOString(), | |
per_page: 100, | |
} | |
); | |
// Check if issue has any of the required labels | |
function hasRequiredLabel(issueLabels, requiredLabels) { | |
return issueLabels.some(label => requiredLabels.includes(label.name)); | |
} | |
// Filter issues | |
const filteredIssues = issues.filter(issue => | |
!coreDevelopers.includes(issue.user.login) && | |
hasRequiredLabel(issue.labels, requiredLabels) && | |
new Date(issue.created_at) > cutoffDate && | |
!issue.pull_request | |
).sort((a, b) => b.number - a.number); | |
// Function to calculate "X days ago" from created_at date | |
function timeAgo(createdAt) { | |
const createdDate = new Date(createdAt); | |
const now = new Date(); | |
const diffInMs = now - createdDate; | |
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); | |
if (diffInDays === 0) { | |
return "Today"; | |
} else if (diffInDays === 1) { | |
return "Yesterday"; | |
} else { | |
return `${diffInDays} days ago`; | |
} | |
} | |
// Function to get unique participants from comments on an issue, excluding the author | |
async function getParticipants(issueNumber, author) { | |
try { | |
const comments = await github.paginate(github.rest.issues.listComments, { | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: issueNumber, | |
per_page: 100, | |
}); | |
// Extract unique usernames of commenters, excluding the author | |
const uniqueParticipants = [ | |
...new Set(comments.map(comment => comment.user.login).filter(username => username !== author)) | |
].sort(); | |
return uniqueParticipants; | |
} catch (error) { | |
console.error(`Error fetching comments for issue #${issueNumber}: ${error}`); | |
return []; | |
} | |
} | |
// Format the issues as a Markdown message for Slack | |
if (filteredIssues.length === 0) { | |
core.setOutput("has_issues", "false"); | |
} else { | |
core.setOutput("has_issues", "true"); | |
let message = `*🛠️ Phoenix Customer Issues Opened in the Last ${days} Day(s)`; | |
message += ` Pending <https://github.com/Arize-ai/phoenix/issues?q=is%3Aissue+is%3Aopen+label%3Atriage|Triage>`; | |
message += `*\n\n`; | |
// Separate issues into two lists: those with the "bug" label and those without | |
const bugIssues = filteredIssues.filter(issue => | |
issue.labels.some(label => label.name === 'bug') | |
); | |
const enhancementIssues = filteredIssues.filter(issue => | |
!issue.labels.some(label => label.name === 'bug') | |
); | |
const issueGroups = [ | |
[bugIssues, "*🐛 Bugs*"], | |
[enhancementIssues, "*💡 Enhancements or Inquiries*"] | |
]; | |
// Use `for...of` loop to allow async/await inside the loop | |
for (const [issues, header] of issueGroups) { | |
if (issues.length > 0) { | |
message += `${header}\n`; | |
for (const [i, issue] of issues.entries()) { | |
message += `${i + 1}. *<${issue.html_url}|#${issue.number}>:* ${issue.title}`; | |
message += ` (by <https://github.com/${issue.user.login}|${issue.user.login}>`; | |
message += `; ${timeAgo(issue.created_at)}`; | |
if (issue.comments > 0) { | |
message += `; ${issue.comments} comments`; | |
const participants = await getParticipants(issue.number, issue.user.login); | |
if (participants.length > 0) { | |
message += `; participants: `; | |
message += participants.map(participant => `<https://github.com/${participant}|${participant}>`).join(", "); | |
} | |
} | |
if (issue.assignees.length > 0) { | |
message += `; assigned to: ` | |
message += issue.assignees.map(assignee => `<https://github.com/${assignee.login}|${assignee.login}>`).join(", "); | |
} | |
message += `)\n`; | |
} | |
} | |
} | |
core.setOutput("slack_message", message); | |
} | |
- name: Send message to Slack | |
uses: slackapi/slack-github-action@v1 | |
if: steps.retrieve-issues.outputs.has_issues == 'true' | |
with: | |
payload: | | |
{ | |
"type": "mrkdwn", | |
"text": ${{ toJSON(steps.retrieve-issues.outputs.slack_message) }} | |
} | |
env: | |
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} |