-
-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ci: add workflow for automated vote notification (#1337)
Co-authored-by: V Thulisile Sibanda <66913810+thulieblack@users.noreply.github.com>%0ACo-authored-by: asyncapi-bot <bot+chan@asyncapi.io>
- Loading branch information
1 parent
a8175b0
commit 516142d
Showing
3 changed files
with
262 additions
and
1 deletion.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
module.exports = { filterIssues, getTSCLeftToVote, sendSlackNotification }; | ||
|
||
const axios = require('axios'); | ||
|
||
/** | ||
* | ||
* @param {Array} issues list of issues having `vote open` label | ||
* @param {Object} state has the last notified date for each issue | ||
* @param {Object} config configuration object (e.g. days) | ||
* @returns {Object} state and issuesToNotify | ||
*/ | ||
function filterIssues(issues, state, config) { | ||
// Add new issues and close the issues which are already closed and present in the state | ||
const newIssues = issues.filter(issue => !state[issue.number]); | ||
const closedIssues = Object.keys(state).filter(issue => !issues.find(i => i.number === parseInt(issue))); | ||
|
||
// Can be extended later to include more configuration, e.g. deadline for voting | ||
const { days } = config; | ||
|
||
console.debug("Config: ", config); | ||
|
||
// Make last_notified null for new issues to notify the TSC members | ||
for (const issue of newIssues) { | ||
state[issue.number] = { | ||
status: 'open', | ||
last_notified: null, | ||
} | ||
} | ||
|
||
for (const issue of closedIssues) { | ||
state[issue] = { | ||
...state[issue], | ||
status: 'closed', | ||
} | ||
} | ||
|
||
const issuesToNotify = issues.filter(issue => | ||
state[issue.number].status === 'open' && | ||
(!state[issue.number].last_notified || | ||
new Date(state[issue.number].last_notified).getTime() + days * 24 * 60 * 60 * 1000 < new Date().getTime()) // Notify every {days} days | ||
); | ||
|
||
return { | ||
state, | ||
issuesToNotify | ||
}; | ||
} | ||
|
||
/** | ||
* | ||
* @param {Object} issue Issue object | ||
* @param {Array} tscMembers List of TSC members | ||
* @param {Object} github GitHub object | ||
* @param {Object} context GitHub context object | ||
* @returns | ||
*/ | ||
async function getTSCLeftToVote(issue, tscMembers, github, context) { | ||
try { | ||
const { data: comments } = await github.rest.issues.listComments({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
issue_number: issue.number, | ||
}); | ||
|
||
// Finds the comment created by the bot where everybody reacts | ||
const voteOpeningComment = comments.findLast(comment => comment.body.includes('Vote created') && comment.user.login === 'git-vote[bot]'); | ||
|
||
if (!voteOpeningComment) { | ||
console.log(`Vote Opening Comment not found for issue #${issue.number}`); | ||
return; | ||
} | ||
|
||
const { data: reactions } = await github.rest.reactions.listForIssueComment({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
comment_id: voteOpeningComment.id, | ||
}); | ||
|
||
// Only 👍,👎, and 👀 are valid votes | ||
const validEmojis = ['+1', '-1', 'eyes']; | ||
const validReactions = reactions.filter(reaction => validEmojis.includes(reaction.content)); | ||
|
||
// Filter out the TSC members who have not voted yet and who have a Slack account (in the MAINTAINERS.yaml file) | ||
const leftToVote = tscMembers.filter(member => member.slack && !validReactions.find(reaction => reaction.user.login === member.github)); | ||
return { | ||
leftToVote, | ||
daysSinceStart: Math.floor((new Date().getTime() - new Date(voteOpeningComment.created_at).getTime()) / (1000 * 60 * 60 * 24)), | ||
} | ||
} catch (error) { | ||
console.log(`Error fetching comments and reactions for issue #${issue.number}: ${error}`); | ||
return []; | ||
} | ||
} | ||
|
||
/** | ||
* | ||
* @param {Object} member TSC member object | ||
* @param {Object} issue Issue object | ||
* @param {String} slackToken Slack token | ||
* | ||
* @returns {Boolean} true if Slack notification sent successfully, false otherwise | ||
*/ | ||
async function sendSlackNotification(member, issue, daysSinceStart, slackToken) { | ||
const message = `👋 Hi ${member.name},\nWe need your vote on the following topic: *${issue.title}*.\n*Issue Details*: ${issue.html_url}\n*Days since voting started: ${daysSinceStart}*\nYour input is crucial to our decision-making process. Please take a moment to review the voting topic and share your thoughts.\nThank you for your contribution! 🙏`; | ||
|
||
// Sending Slack DM via API | ||
try { | ||
const response = await axios.post('https://slack.com/api/chat.postMessage', { | ||
channel: member.slack, | ||
text: message, | ||
}, { | ||
headers: { | ||
'Authorization': `Bearer ${slackToken}`, | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
|
||
if (!response.data.ok) { | ||
console.error(`Error sending Slack DM to ${member.name}: ${response.data.error}`); | ||
return false; | ||
} else { | ||
console.log(`Slack DM sent to ${member.name}`); | ||
return true; | ||
} | ||
} catch (error) { | ||
console.error(`Error sending Slack DM to ${member.name}: ${error.message}`); | ||
|
||
return false; | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
name: Notify TSC Members about Voting Status | ||
|
||
on: | ||
schedule: | ||
# Daily at 9:00 UTC | ||
- cron: '0 9 * * *' | ||
|
||
jobs: | ||
notify-tsc-members: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
# To store the state of the votes and the last time the TSC members were notified | ||
# The format of the file is: | ||
# { | ||
# "issue_number": { | ||
# "status": "open" | "closed", | ||
# "last_notified": "2021-09-01T00:00:00Z" | ||
# } | ||
# } | ||
# Example: https://github.com/ash17290/asyncapi-community/blob/vote_state/vote_status.json | ||
- uses: jorgebg/stateful-action@bd279992190b64c6a5906c3b75a6f2835823ab46 | ||
id: state | ||
with: | ||
branch: vote_state | ||
|
||
# List all the open issues with the label "vote open" | ||
- name: List current open issues | ||
uses: actions/github-script@v7 | ||
id: list | ||
with: | ||
script: | | ||
const { data: issues } = await github.rest.issues.listForRepo({ | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
labels: 'vote open' | ||
}); | ||
return issues; | ||
github-token: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
# Fetch the current state from the vote_status.json file | ||
- name: Fetch Current State | ||
id: fetch | ||
run: | | ||
# This file comes from the branch vote_state which is created by the stateful-action | ||
# Eg: https://github.com/ash17290/asyncapi-community/tree/vote_state | ||
cd .vote_state | ||
# Check if the file exists, not empty and readable and create if not | ||
if [ ! -f vote_status.json ] || [ ! -s vote_status.json ] || [ ! -r vote_status.json ]; then | ||
echo "Intializing vote_status.json" | ||
echo "{}" > vote_status.json | ||
fi | ||
# Read the file and export it as JSON | ||
export json=$(cat vote_status.json | jq -c) | ||
echo "::debug::vote_status=$json" | ||
# Store in GitHub Output | ||
echo "vote_status=$json" >> $GITHUB_OUTPUT | ||
# Needed as axios and js-yaml are not available in the default environment | ||
- name: Install the dependencies | ||
run: npm install js-yaml@4.1.0 axios@1.7.4 | ||
shell: bash | ||
|
||
- name: Notify TSC Members | ||
uses: actions/github-script@v7 | ||
id: notify | ||
with: | ||
script: | | ||
const yaml = require('js-yaml'); | ||
const fs = require('fs'); | ||
const axios = require('axios'); | ||
// Creates list of issues for which the TSC members need to be notified | ||
// The number of buffer days is easily configurable by changing the value of the days variable | ||
const { filterIssues, getTSCLeftToVote, sendSlackNotification } = require('./.github/workflows/vote-notify-helpers/index.js'); | ||
const issues = ${{ steps.list.outputs.result }}; | ||
const initialState = ${{ steps.fetch.outputs.vote_status }}; | ||
const config = { | ||
days: 5 | ||
} | ||
const { issuesToNotify, state } = filterIssues(issues, initialState, config); | ||
const tscMembers = yaml.load(fs.readFileSync('MAINTAINERS.yaml', 'utf8')).filter(member => member.isTscMember); | ||
const failingSlackIds = new Set(); | ||
console.log(`Issues to notify: ${JSON.stringify(issuesToNotify.map(issue => issue.number))}`); | ||
for (const issue of issuesToNotify) { | ||
const { leftToVote, daysSinceStart } = await getTSCLeftToVote(issue, tscMembers, github, context); | ||
for (const member of leftToVote) { | ||
console.log(`Notifying ${member.name} about issue #${issue.number}`); | ||
if (!await sendSlackNotification(member, issue, daysSinceStart, `${{ secrets.SLACK_DM_TSC }}`)) { | ||
failingSlackIds.add(member.slack); | ||
} | ||
} | ||
state[issue.number].last_notified = new Date().toISOString(); | ||
} | ||
// Store the failing Slack IDs in GITHUB_ENV | ||
if (failingSlackIds.size > 0) { | ||
process.env.FAILED_SLACK_IDS = Array.from(failingSlackIds).join(','); | ||
core.exportVariable('FAILED_SLACK_IDS', JSON.stringify(Array.from(failingSlackIds))); | ||
} | ||
// Store the state back | ||
return JSON.stringify(state); | ||
- name: Update State | ||
if: ${{ steps.notify.outputs.result }} | ||
run: | | ||
echo ${{ steps.notify.outputs.result }} | jq > ./.vote_state/vote_status.json | ||
- name: Notify about failing Slack DMs | ||
if: env.FAILED_SLACK_IDS | ||
uses: rtCamp/action-slack-notify@v2 | ||
env: | ||
SLACK_WEBHOOK: ${{secrets.SLACK_CI_FAIL_NOTIFY}} | ||
SLACK_TITLE: 🚨 Vote notifications could'nt be sent to following IDs 🚨 | ||
SLACK_MESSAGE: ${{ env.FAILED_SLACK_IDS }} | ||
MSG_MINIMAL: true |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
node_modules | ||
.terraform | ||
*tfstate.backup | ||
*.tfvars | ||
*.tfvars | ||
.vote_state/ |