diff --git a/.github/workflows/call-contributor-issue-comment.yml b/.github/workflows/call-contributor-issue-comment.yml new file mode 100644 index 0000000..4e8b7db --- /dev/null +++ b/.github/workflows/call-contributor-issue-comment.yml @@ -0,0 +1,14 @@ +name: Handle contributor comment on GitHub issue + +on: + issue_comment: + types: [created] + +jobs: + call-workflow: + uses: learningequality/.github/.github/workflows/contributor-issue-comment.yml@main + secrets: + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml new file mode 100644 index 0000000..c26bd48 --- /dev/null +++ b/.github/workflows/contributor-issue-comment.yml @@ -0,0 +1,89 @@ +name: Handle contributor comment on GitHub issue + +on: + workflow_call: + secrets: + LE_BOT_APP_ID: + description: "GitHub App ID for authentication" + required: true + LE_BOT_PRIVATE_KEY: + description: "GitHub App Private Key for authentication" + required: true + SLACK_WEBHOOK_URL: + required: true + description: "Webhook URL for Slack #support-dev channel" + SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: + required: true + description: "Webhook URL for Slack #support-dev-notifications channel" + + +jobs: + process-issue-comment: + name: Process issue comment + + if: >- + ${{ + !github.event.issue.pull_request && + github.event.comment.author_association != 'MEMBER' && + github.event.comment.author_association != 'OWNER' && + github.event.comment.user.login != 'sentry-io[bot]' && + github.event.comment.user.login != 'learning-equality-bot[bot]' + }} + + runs-on: ubuntu-latest + steps: + - name: Generate App Token + id: generate-token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.LE_BOT_APP_ID }} + private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} + + - name: Checkout .github repository + uses: actions/checkout@v4 + with: + repository: learningequality/.github + ref: main + token: ${{ steps.generate-token.outputs.token }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Run script + id: script + uses: actions/github-script@v7 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const script = require('./scripts/contributor-issue-comment.js'); + return await script({github, context, core}); + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} + + - name: Send Slack notification about GitHub comment + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook-type: incoming-webhook + webhook: ${{ steps.script.outputs.webhook_url }} + payload: > + { + "text": "${{ steps.script.outputs.slack_notification_comment }}" + } + + - name: Send Slack notification about GitHub bot reply + if: ${{ steps.script.outputs.bot_replied }} + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook-type: incoming-webhook + webhook: ${{ steps.script.outputs.webhook_url }} + payload: > + { + "text": "${{ steps.script.outputs.slack_notification_bot_comment }}" + } + diff --git a/.github/workflows/notify_team_new_comment.yml b/.github/workflows/notify_team_new_comment.yml deleted file mode 100644 index 6bf8a32..0000000 --- a/.github/workflows/notify_team_new_comment.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Send a slack notification when a contributor comments on issue - -on: - workflow_call: - secrets: - SLACK_WEBHOOK_URL: - required: true - -jobs: - contributor_issue_comment: - name: Contributor issue comment - - if: >- - ${{ - !github.event.issue.pull_request && - github.event.comment.author_association != 'MEMBER' && - github.event.comment.author_association != 'OWNER' && - github.event.comment.user.login != 'sentry-io[bot]' && - github.event.comment.user.login != 'learning-equality-bot[bot]' - }} - - runs-on: ubuntu-latest - steps: - - name: Escape title double quotes - id: escape_title - env: - ISSUE_TITLE: ${{ github.event.issue.title }} - run: echo "ISSUE_TITLE=${ISSUE_TITLE//\"/\\\"}" >> "$GITHUB_OUTPUT" - - - name: Send message to Slack channel - uses: slackapi/slack-github-action@v2.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - text: "*[${{ github.event.repository.name }}] New comment on issue: <${{ github.event.issue.html_url }}#issuecomment-${{ github.event.comment.id }}|${{ steps.escape_title.outputs.ISSUE_TITLE }} by ${{ github.event.comment.user.login }}>*" - diff --git a/scripts/constants.js b/scripts/constants.js new file mode 100644 index 0000000..0033d1e --- /dev/null +++ b/scripts/constants.js @@ -0,0 +1,42 @@ +const LE_BOT_USERNAME = 'learning-equality-bot[bot]'; + +// close contributors are treated a bit special in some workflows, +// for example, we receive a high priority notification about their +// comments on all issues rather than just on 'help wanted' issues +const CLOSE_CONTRIBUTORS = ['BabyElias', 'Dimi20cen', 'EshaanAgg', 'GarvitSinghal47', 'habibayman', 'iamshobhraj', 'indirectlylit', 'Jakoma02', 'KshitijThareja', 'muditchoudhary', 'nathanaelg16', 'nikkuAg', 'Sahil-Sinha-11', 'shivam-daksh', 'shruti862', 'thesujai', 'WinnyChang']; + +const KEYWORDS_DETECT_ASSIGNMENT_REQUEST = [ + 'assign', 'assigned', + 'work', 'working', + 'contribute', 'contributing', + 'request', 'requested', + 'pick', 'picked', 'picking', + 'address', 'addressing', + 'handle', 'handling', + 'solve', 'solving', 'resolve', 'resolving', + 'try', 'trying', + 'grab', 'grabbing', + 'claim', 'claimed', + 'interest', 'interested', + 'do', 'doing', + 'help', + 'take', + 'want', + 'would like', + 'own', + 'on it', + 'available', + 'got this' +]; + +const ISSUE_LABEL_HELP_WANTED = 'help wanted'; + +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. 😊`; + +module.exports = { + LE_BOT_USERNAME, + CLOSE_CONTRIBUTORS, + KEYWORDS_DETECT_ASSIGNMENT_REQUEST, + ISSUE_LABEL_HELP_WANTED, + BOT_MESSAGE_ISSUE_NOT_OPEN, +}; diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js new file mode 100644 index 0000000..f89b171 --- /dev/null +++ b/scripts/contributor-issue-comment.js @@ -0,0 +1,109 @@ +const { + LE_BOT_USERNAME, + CLOSE_CONTRIBUTORS, + KEYWORDS_DETECT_ASSIGNMENT_REQUEST, + ISSUE_LABEL_HELP_WANTED, + BOT_MESSAGE_ISSUE_NOT_OPEN +} = require('./constants'); + +module.exports = async ({ github, context, core }) => { + try { + const issueNumber = context.payload.issue.number; + const issueUrl = context.payload.issue.html_url; + const issueTitle = context.payload.issue.title; + const escapedTitle = issueTitle.replace(/"/g, '\\"'); + const commentId = context.payload.comment.id; + const commentTime = new Date(context.payload.comment.created_at); + const oneHourBefore = new Date(commentTime - 3600000); + const commentAuthor = context.payload.comment.user.login; + const commentBody = context.payload.comment.body; + const repo = context.repo.repo; + const owner = context.repo.owner; + const supportDevSlackWebhookUrl = process.env.SLACK_WEBHOOK_URL; + const supportDevNotificationsSlackWebhookUrl = process.env.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL; + const keywordRegexes = KEYWORDS_DETECT_ASSIGNMENT_REQUEST + .map(k => k.trim().toLowerCase()) + .filter(Boolean) + .map(keyword => new RegExp(`\\b${keyword}\\b`, 'i')); + + + + async function hasLabel(name) { + let labels = []; + try { + const response = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: issueNumber + }); + labels = response.data.map(label => label.name); + } catch (error) { + core.warning(`⚠️ Failed to fetch labels on issue #${issueNumber}: ${error.message}`); + labels = []; + } + return labels.some(label => label.toLowerCase() === name.toLowerCase()); + } + + async function findRecentCommentsByUser(username) { + try{ + let response = await github.rest.issues.listComments({ + owner, + repo, + issue_number: issueNumber, + since: oneHourBefore.toISOString() + }); + return response.data.filter(comment => comment.user.login === username); + } catch (error) { + core.warning(`⚠️ Failed to fetch comments on issue #${issueNumber}: ${error.message}`); + return []; + } + } + + async function botReply(){ + let response = null; + try { + response = await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: BOT_MESSAGE_ISSUE_NOT_OPEN + }); + if (response?.data?.html_url) { + core.setOutput('bot_replied', true); + const slackMessage = `*[${repo}] <${response.data.html_url}|Bot response sent> on issue: <${issueUrl}|${escapedTitle}>*`; + core.setOutput('slack_notification_bot_comment', slackMessage); + } + } catch (error) { + core.warning(`Failed to post bot comment: ${error.message}`); + core.setOutput('bot_replied', false); + } + return response; + } + + + if (await hasLabel(ISSUE_LABEL_HELP_WANTED) || CLOSE_CONTRIBUTORS.includes(commentAuthor)) { + core.setOutput('webhook_url', supportDevSlackWebhookUrl); + } else { + core.setOutput('webhook_url', supportDevNotificationsSlackWebhookUrl); + const matchedKeyword = keywordRegexes.find(regex => regex.test(commentBody)); + // post a bot reply if there is matched keyword and no previous bot comment in past hour + if(matchedKeyword){ + let lastBotComment; + let PastBotComments = await findRecentCommentsByUser(LE_BOT_USERNAME); + if(PastBotComments.length > 0){ + lastBotComment = PastBotComments.at(-1); + core.setOutput('bot_replied', false); + } else if(PastBotComments.length === 0){ + console.log("bot is replying"); + lastBotComment = await botReply(); + } + } + } + + const message = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${escapedTitle}> by ${commentAuthor}*`; + core.setOutput('slack_notification_comment', message); + + } catch (error) { + core.setFailed(`Action failed with error: ${error.message}`); + } +};