diff --git a/.github/workflows/vote-notify.yml b/.github/workflows/vote-notify.yml new file mode 100644 index 000000000..fe39ddf55 --- /dev/null +++ b/.github/workflows/vote-notify.yml @@ -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({ + 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 + + 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 js-yaml@4.1.0 axios@1.7.4 + 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},\nWe need your vote on the following topic: *${issue.title}*.\n*Issue Details*: ${issue.html_url}\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 + const response = await axios.post('https://slack.com/api/chat.postMessage', { + channel: member.slack, + text: message, + }, { + headers: { + 'Authorization': `Bearer ${{ secrets.SLACK_TOKEN }}`, + '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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b0f0a309f..443b46d59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules .terraform *tfstate.backup -*.tfvars \ No newline at end of file +*.tfvars +.vote_state/ \ No newline at end of file