Skip to content

Commit

Permalink
ci: add workflow for automated vote notification (#1337)
Browse files Browse the repository at this point in the history
Co-authored-by: V Thulisile Sibanda <66913810+thulieblack@users.noreply.github.com>%0ACo-authored-by: asyncapi-bot <bot+chan@asyncapi.io>
  • Loading branch information
Shurtu-gal and asyncapi-bot authored Feb 12, 2025
1 parent a8175b0 commit 516142d
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 1 deletion.
130 changes: 130 additions & 0 deletions .github/workflows/vote-notify-helpers/index.js
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;
}
}
130 changes: 130 additions & 0 deletions .github/workflows/vote-notify.yml
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
.terraform
*tfstate.backup
*.tfvars
*.tfvars
.vote_state/

0 comments on commit 516142d

Please sign in to comment.