[Good First Issue] 🧧 Add new Etiquette Tip 84 - Beginner-Friendly Open-source Contribution #30285
This file contains hidden or 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: Auto-Reply to Issue Comments | |
| on: | |
| issue_comment: | |
| types: [created] | |
| issues: | |
| types: [assigned] | |
| permissions: | |
| issues: write | |
| contents: read | |
| jobs: | |
| auto-respond: | |
| if: ${{ !github.event.issue.pull_request && (contains(github.event.issue.labels.*.name, 'community') || contains(github.event.issue.labels.*.name, 'good first issue') || contains(github.event.issue.labels.*.name, 'beginner-friendly') || contains(github.event.issue.labels.*.name, 'first-timers-only') || contains(github.event.issue.labels.*.name, 'up-for-grabs') || contains(github.event.issue.labels.*.name, 'help wanted')) && (github.event_name != 'issue_comment' || (github.event.comment.user.login != 'github-actions[bot]' && github.event.comment.user.login != github.repository_owner)) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check and respond to commenter | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.AUTOMATION_PR_TOKEN || secrets.GITHUB_TOKEN }} | |
| script: | | |
| const path = '.github/templates/messages.cjs'; | |
| const ref = context.sha; | |
| const { data: file } = await github.rest.repos.getContent({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| path, | |
| ref | |
| }); | |
| if (!file || !file.content) { | |
| throw new Error('Could not load templates from repository content.'); | |
| } | |
| const content = Buffer.from(file.content, 'base64').toString('utf8'); | |
| const module = { exports: {} }; | |
| const vm = require('vm'); | |
| vm.runInNewContext(content, { module, exports: module.exports }); | |
| const templates = module.exports; | |
| const t = templates.issueAutoRespond; | |
| const issue = context.payload.issue; | |
| const commenter = context.payload.comment?.user?.login; | |
| const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`; | |
| const excludedUsers = new Set(['github-actions[bot]', context.repo.owner, 'tentoumushii']); | |
| function fillTemplate(str, vars) { | |
| return Object.entries(vars).reduce((acc, [key, value]) => { | |
| return acc | |
| .replaceAll(`{${key}}`, String(value)) | |
| .replaceAll(`@{${key}}`, `@${value}`) | |
| .replaceAll(`#{${key}}`, `#${value}`); | |
| }, str); | |
| } | |
| const reactionPool = ['+1', 'laugh', 'hooray', 'heart', 'rocket', 'eyes']; | |
| function pickRandomReactions(count) { | |
| const shuffled = [...reactionPool].sort(() => Math.random() - 0.5); | |
| return shuffled.slice(0, Math.min(count, reactionPool.length)); | |
| } | |
| async function reactToComment(commentId, reactions) { | |
| for (const content of reactions) { | |
| try { | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: commentId, | |
| content | |
| }); | |
| } catch (e) { | |
| console.log(`Reaction ${content} failed: ${e.message}`); | |
| } | |
| } | |
| } | |
| async function hasAlreadyAssignedNoticePostedForUser(username) { | |
| const marker = `<!-- already-assigned-notice:${username} -->`; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| per_page: 100 | |
| }); | |
| return comments.some((comment) => { | |
| return comment.user?.login === 'github-actions[bot]' && typeof comment.body === 'string' && comment.body.includes(marker); | |
| }); | |
| } | |
| if (context.eventName === 'issues') { | |
| const assignee = context.payload.assignee?.login; | |
| if (!assignee) { | |
| console.log('No assignee found in payload; skipping.'); | |
| return; | |
| } | |
| const msg = t.assigned; | |
| const nextSteps = msg.nextSteps.items.map(function(item, i) { | |
| return `${i + 1}. ${fillTemplate(item, { commenter: assignee, issueNumber: issue.number, repoUrl })}`; | |
| }).join('\n'); | |
| const resources = msg.resources.items.map(function(item) { | |
| return '- ' + fillTemplate(item, { commenter: assignee, issueNumber: issue.number, repoUrl }); | |
| }).join('\n'); | |
| const reply = await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: `${fillTemplate(msg.greeting, { commenter: assignee, issueNumber: issue.number, repoUrl })}\n\n${fillTemplate(msg.body, { commenter: assignee, issueNumber: issue.number, repoUrl })}\n\n${fillTemplate(msg.nextSteps.title, { commenter: assignee, issueNumber: issue.number, repoUrl })}\n${nextSteps}\n\n${fillTemplate(msg.resources.title, { commenter: assignee, issueNumber: issue.number, repoUrl })}\n${resources}\n\n${fillTemplate(msg.footer, { commenter: assignee, issueNumber: issue.number, repoUrl })}\n\n${fillTemplate(msg.encouragement, { commenter: assignee, issueNumber: issue.number, repoUrl })}` | |
| }); | |
| await reactToComment(reply.data.id, pickRandomReactions(3)); | |
| console.log(`Posted assignment instructions for issue #${issue.number} to @${assignee}`); | |
| return; | |
| } | |
| if (!commenter) { | |
| console.log('No commenter found in payload; skipping.'); | |
| return; | |
| } | |
| if (excludedUsers.has(commenter)) { | |
| console.log(`Skipping @${commenter} — excluded user.`); | |
| return; | |
| } | |
| // Skip repo collaborators (write/admin) — they should never be auto-assigned | |
| const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: commenter | |
| }).catch(() => ({ data: { permission: 'none' } })); | |
| if (['admin', 'write'].includes(perm.permission)) { | |
| console.log(`Skipping @${commenter} — has ${perm.permission} access.`); | |
| return; | |
| } | |
| if (issue.assignees && issue.assignees.length > 0) { | |
| const assignee = issue.assignees[0].login; | |
| if (assignee !== commenter) { | |
| if (await hasAlreadyAssignedNoticePostedForUser(commenter)) { | |
| console.log(`Skipping duplicate already-assigned reply for @${commenter} on issue #${issue.number}.`); | |
| return; | |
| } | |
| const msg = t.alreadyAssigned; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: `<!-- already-assigned-notice:${commenter} -->\n${fillTemplate(msg.greeting, { commenter, assignee, issueNumber: issue.number, repoUrl })}\n\n${fillTemplate(msg.body, { commenter, assignee, issueNumber: issue.number, repoUrl })}\n\n${fillTemplate(msg.suggestion, { commenter, assignee, issueNumber: issue.number, repoUrl })}\n\n${fillTemplate(msg.encouragement, { commenter, assignee, issueNumber: issue.number, repoUrl })}` | |
| }); | |
| } | |
| return; | |
| } | |
| await github.rest.issues.addAssignees({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| assignees: [commenter] | |
| }); | |
| await reactToComment(context.payload.comment.id, pickRandomReactions(3)); | |
| console.log(`Assigned issue #${issue.number} to @${commenter} — assignment reply will be posted by the issues:assigned event.`); |