Skip to content

[Good First Issue] 🍡 Add new Grammar Point 21 - Beginner-Friendly Open-source Contribution #30343

[Good First Issue] 🍡 Add new Grammar Point 21 - Beginner-Friendly Open-source Contribution

[Good First Issue] 🍡 Add new Grammar Point 21 - Beginner-Friendly Open-source Contribution #30343

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.`);