Skip to content

Commit e4eac14

Browse files
authored
Merge pull request #29 from iamshobhraj/notifySlack
Enhance Slack notifications and automate GitHub assignment replies based on issue labels
2 parents e44fde5 + 8aceb70 commit e4eac14

File tree

5 files changed

+254
-37
lines changed

5 files changed

+254
-37
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Handle contributor comment on GitHub issue
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
7+
jobs:
8+
call-workflow:
9+
uses: learningequality/.github/.github/workflows/contributor-issue-comment.yml@main
10+
secrets:
11+
LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }}
12+
LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }}
13+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
14+
SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: Handle contributor comment on GitHub issue
2+
3+
on:
4+
workflow_call:
5+
secrets:
6+
LE_BOT_APP_ID:
7+
description: "GitHub App ID for authentication"
8+
required: true
9+
LE_BOT_PRIVATE_KEY:
10+
description: "GitHub App Private Key for authentication"
11+
required: true
12+
SLACK_WEBHOOK_URL:
13+
required: true
14+
description: "Webhook URL for Slack #support-dev channel"
15+
SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL:
16+
required: true
17+
description: "Webhook URL for Slack #support-dev-notifications channel"
18+
19+
20+
jobs:
21+
process-issue-comment:
22+
name: Process issue comment
23+
24+
if: >-
25+
${{
26+
!github.event.issue.pull_request &&
27+
github.event.comment.author_association != 'MEMBER' &&
28+
github.event.comment.author_association != 'OWNER' &&
29+
github.event.comment.user.login != 'sentry-io[bot]' &&
30+
github.event.comment.user.login != 'learning-equality-bot[bot]'
31+
}}
32+
33+
runs-on: ubuntu-latest
34+
steps:
35+
- name: Generate App Token
36+
id: generate-token
37+
uses: tibdex/github-app-token@v2
38+
with:
39+
app_id: ${{ secrets.LE_BOT_APP_ID }}
40+
private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }}
41+
42+
- name: Checkout .github repository
43+
uses: actions/checkout@v4
44+
with:
45+
repository: learningequality/.github
46+
ref: main
47+
token: ${{ steps.generate-token.outputs.token }}
48+
49+
- name: Setup Node.js
50+
uses: actions/setup-node@v4
51+
with:
52+
node-version: '20'
53+
54+
- name: Install dependencies
55+
run: npm install
56+
57+
- name: Run script
58+
id: script
59+
uses: actions/github-script@v7
60+
with:
61+
github-token: ${{ steps.generate-token.outputs.token }}
62+
script: |
63+
const script = require('./scripts/contributor-issue-comment.js');
64+
return await script({github, context, core});
65+
env:
66+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
67+
SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }}
68+
69+
- name: Send Slack notification about GitHub comment
70+
uses: slackapi/[email protected]
71+
with:
72+
webhook-type: incoming-webhook
73+
webhook: ${{ steps.script.outputs.webhook_url }}
74+
payload: >
75+
{
76+
"text": "${{ steps.script.outputs.slack_notification_comment }}"
77+
}
78+
79+
- name: Send Slack notification about GitHub bot reply
80+
if: ${{ steps.script.outputs.bot_replied }}
81+
uses: slackapi/[email protected]
82+
with:
83+
webhook-type: incoming-webhook
84+
webhook: ${{ steps.script.outputs.webhook_url }}
85+
payload: >
86+
{
87+
"text": "${{ steps.script.outputs.slack_notification_bot_comment }}"
88+
}
89+

.github/workflows/notify_team_new_comment.yml

Lines changed: 0 additions & 37 deletions
This file was deleted.

scripts/constants.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const LE_BOT_USERNAME = 'learning-equality-bot[bot]';
2+
3+
// close contributors are treated a bit special in some workflows,
4+
// for example, we receive a high priority notification about their
5+
// comments on all issues rather than just on 'help wanted' issues
6+
const CLOSE_CONTRIBUTORS = ['BabyElias', 'Dimi20cen', 'EshaanAgg', 'GarvitSinghal47', 'habibayman', 'iamshobhraj', 'indirectlylit', 'Jakoma02', 'KshitijThareja', 'muditchoudhary', 'nathanaelg16', 'nikkuAg', 'Sahil-Sinha-11', 'shivam-daksh', 'shruti862', 'thesujai', 'WinnyChang'];
7+
8+
const KEYWORDS_DETECT_ASSIGNMENT_REQUEST = [
9+
'assign', 'assigned',
10+
'work', 'working',
11+
'contribute', 'contributing',
12+
'request', 'requested',
13+
'pick', 'picked', 'picking',
14+
'address', 'addressing',
15+
'handle', 'handling',
16+
'solve', 'solving', 'resolve', 'resolving',
17+
'try', 'trying',
18+
'grab', 'grabbing',
19+
'claim', 'claimed',
20+
'interest', 'interested',
21+
'do', 'doing',
22+
'help',
23+
'take',
24+
'want',
25+
'would like',
26+
'own',
27+
'on it',
28+
'available',
29+
'got this'
30+
];
31+
32+
const ISSUE_LABEL_HELP_WANTED = 'help wanted';
33+
34+
const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! 👋 \n\n Thanks so much for your interest! **This issue is not open for contribution. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to help—you're welcome to find a more suitable issue, and let us know if you have any questions. 😊`;
35+
36+
module.exports = {
37+
LE_BOT_USERNAME,
38+
CLOSE_CONTRIBUTORS,
39+
KEYWORDS_DETECT_ASSIGNMENT_REQUEST,
40+
ISSUE_LABEL_HELP_WANTED,
41+
BOT_MESSAGE_ISSUE_NOT_OPEN,
42+
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const {
2+
LE_BOT_USERNAME,
3+
CLOSE_CONTRIBUTORS,
4+
KEYWORDS_DETECT_ASSIGNMENT_REQUEST,
5+
ISSUE_LABEL_HELP_WANTED,
6+
BOT_MESSAGE_ISSUE_NOT_OPEN
7+
} = require('./constants');
8+
9+
module.exports = async ({ github, context, core }) => {
10+
try {
11+
const issueNumber = context.payload.issue.number;
12+
const issueUrl = context.payload.issue.html_url;
13+
const issueTitle = context.payload.issue.title;
14+
const escapedTitle = issueTitle.replace(/"/g, '\\"');
15+
const commentId = context.payload.comment.id;
16+
const commentTime = new Date(context.payload.comment.created_at);
17+
const oneHourBefore = new Date(commentTime - 3600000);
18+
const commentAuthor = context.payload.comment.user.login;
19+
const commentBody = context.payload.comment.body;
20+
const repo = context.repo.repo;
21+
const owner = context.repo.owner;
22+
const supportDevSlackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
23+
const supportDevNotificationsSlackWebhookUrl = process.env.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL;
24+
const keywordRegexes = KEYWORDS_DETECT_ASSIGNMENT_REQUEST
25+
.map(k => k.trim().toLowerCase())
26+
.filter(Boolean)
27+
.map(keyword => new RegExp(`\\b${keyword}\\b`, 'i'));
28+
29+
30+
31+
async function hasLabel(name) {
32+
let labels = [];
33+
try {
34+
const response = await github.rest.issues.listLabelsOnIssue({
35+
owner,
36+
repo,
37+
issue_number: issueNumber
38+
});
39+
labels = response.data.map(label => label.name);
40+
} catch (error) {
41+
core.warning(`⚠️ Failed to fetch labels on issue #${issueNumber}: ${error.message}`);
42+
labels = [];
43+
}
44+
return labels.some(label => label.toLowerCase() === name.toLowerCase());
45+
}
46+
47+
async function findRecentCommentsByUser(username) {
48+
try{
49+
let response = await github.rest.issues.listComments({
50+
owner,
51+
repo,
52+
issue_number: issueNumber,
53+
since: oneHourBefore.toISOString()
54+
});
55+
return response.data.filter(comment => comment.user.login === username);
56+
} catch (error) {
57+
core.warning(`⚠️ Failed to fetch comments on issue #${issueNumber}: ${error.message}`);
58+
return [];
59+
}
60+
}
61+
62+
async function botReply(){
63+
let response = null;
64+
try {
65+
response = await github.rest.issues.createComment({
66+
owner,
67+
repo,
68+
issue_number: issueNumber,
69+
body: BOT_MESSAGE_ISSUE_NOT_OPEN
70+
});
71+
if (response?.data?.html_url) {
72+
core.setOutput('bot_replied', true);
73+
const slackMessage = `*[${repo}] <${response.data.html_url}|Bot response sent> on issue: <${issueUrl}|${escapedTitle}>*`;
74+
core.setOutput('slack_notification_bot_comment', slackMessage);
75+
}
76+
} catch (error) {
77+
core.warning(`Failed to post bot comment: ${error.message}`);
78+
core.setOutput('bot_replied', false);
79+
}
80+
return response;
81+
}
82+
83+
84+
if (await hasLabel(ISSUE_LABEL_HELP_WANTED) || CLOSE_CONTRIBUTORS.includes(commentAuthor)) {
85+
core.setOutput('webhook_url', supportDevSlackWebhookUrl);
86+
} else {
87+
core.setOutput('webhook_url', supportDevNotificationsSlackWebhookUrl);
88+
const matchedKeyword = keywordRegexes.find(regex => regex.test(commentBody));
89+
// post a bot reply if there is matched keyword and no previous bot comment in past hour
90+
if(matchedKeyword){
91+
let lastBotComment;
92+
let PastBotComments = await findRecentCommentsByUser(LE_BOT_USERNAME);
93+
if(PastBotComments.length > 0){
94+
lastBotComment = PastBotComments.at(-1);
95+
core.setOutput('bot_replied', false);
96+
} else if(PastBotComments.length === 0){
97+
console.log("bot is replying");
98+
lastBotComment = await botReply();
99+
}
100+
}
101+
}
102+
103+
const message = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${escapedTitle}> by ${commentAuthor}*`;
104+
core.setOutput('slack_notification_comment', message);
105+
106+
} catch (error) {
107+
core.setFailed(`Action failed with error: ${error.message}`);
108+
}
109+
};

0 commit comments

Comments
 (0)