Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add workflow for automated vote notification #1337

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .github/workflows/vote-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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"
# }
# }
- 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({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will it list PRs as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so does gitvote allow PR voting as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Facing a problem with PRs though, getting filtered results by vote-open label is not possible. Should I go ahead with manually filtering the whole list of PRs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, you either do it "manually"

          const { data: pullRequests } = await github.pulls.list({
            owner,
            repo: repoName,
            state: 'open'
          });
          
          const prsWithLabel = pullRequests.filter(pr => 
            pr.labels.some(label => label.name === 'vote-open')
          );

or use search API

const { data } = await github.search.issuesAndPullRequests({
            q: `repo:${owner}/${repoName} is:pr is:open label:"vote-open"`,
          });

I personally have bad experience with search and its accuracy, so I guess manual is better

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: |
cd .vote_state
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where it comes from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, please extend description then, for future generations :D


if [ ! -f vote_status.json ]; then
echo "{}" > vote_status.json
fi

export json=$(cat vote_status.json | jq -c)

echo "::debug::vote_status=$json"

# Store in GitHub Output
echo "vote_status=$json" >> $GITHUB_OUTPUT

- name: Install the dependencies
run: npm install [email protected] [email protected]
shell: bash

- name: Notify TSC Members
# if: steps.list.outputs.result.length > 0
uses: actions/github-script@v7
id: notify
with:
script: |
const yaml = require('js-yaml');
const fs = require('fs');
const axios = require('axios');

const issues = ${{ steps.list.outputs.result }};
const state = ${{ steps.fetch.outputs.vote_status }};

// Add new issues and close old ones
const newIssues = issues.filter(issue => !state[issue.number]);
const closedIssues = Object.keys(state).filter(issue => !issues.find(i => i.number === parseInt(issue)));

// Update state
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) + 7 * 24 * 60 * 60 * 1000 < new Date() || true)
);

const tscMembers = yaml.load(fs.readFileSync('MAINTAINERS.yaml', 'utf8'));

for (const issue of issuesToNotify) {
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
});

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}`);
continue;
}

const { data: reactions } = await github.rest.reactions.listForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: voteOpeningComment.id,
});

const validReactions = reactions.filter(reaction => reaction.content === '+1' || reaction.content === '-1');

const leftToVote = tscMembers.filter(member => !validReactions.find(reaction => reaction.user.login === member.github));

for (const member of leftToVote) {
console.log(`Notifying ${member.name} about issue #${issue.number}`);

const message = `👋 Hi ${member.name},\nYour vote is required on the following issue: *#${issue.number}*.\n*Issue Details*: <${issue.html_url}|View and cast your vote here>\nYour input is crucial to our decision-making process. Please take a moment to review the issue and share your thoughts.\nThank you for your contribution! 🙏`;
Shurtu-gal marked this conversation as resolved.
Show resolved Hide resolved

// Sending Slack DM via API
const response = await axios.post('https://slack.com/api/chat.postMessage', {
channel: member.slack,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to either set a proper error handling here, or just check it before calling API

slack is added manually to MAINTAINERS.yaml - there is no way to get the slack id in smart automated way. So there might be situation that we overlooked it and some TSC member do not have this information provided

  • we need to validate if slack property is added - and also have error handling in case wrong slack id is provided and API call fails
  • in both cases we need to learn about that issue and drop notification to proper channel using SLACK_CI_FAIL_NOTIFY secret - you will find examples of how we do it in other workflows

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, give me some time I will do this as well.

text: message,
}, {
headers: {
'Authorization': `Bearer ${{ secrets.SLACK_TOKEN }}`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use existing one, no need to do anything here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, regarding this I don't know which secret has access to post messages. Need chat:write to be specific. For more info:
https://api.slack.com/methods/chat.postMessage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, there basically is no secret like SLACK_TOKEN - we just need to somehow create a dedicated slack token for that, this is a strategy we had so far, any publishing to slack, for any reason, always has dedicated token

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

token under new app is under dedicated secret now: SLACK_DM_TSC with below scopes

Screenshot 2024-10-22 at 09 31 07

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested with below

curl -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer NOTSOFASTSOLDIER" \
-H "Content-Type: application/json" \
-d '{
  "channel": "U0572R8J927", 
  "text": "Hello, this is a test message from Lukasz Gornicki related to https://github.com/asyncapi/community/pull/1337!"
}'

did you get it @Shurtu-gal ?

'Content-Type': 'application/json',
},
});

console.log(`Slack DM sent to ${member.name}`);
}
state[issue.number].last_notified = new Date().toISOString();
}

return JSON.stringify(state);

- name: Update State
run: |
echo ${{ steps.notify.outputs.result }} | jq > ./.vote_state/vote_status.json
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/
Loading