From 9dea6672b1ed123dab63f57b76a62ddaf5965912 Mon Sep 17 00:00:00 2001 From: Victor Alfaro Date: Mon, 9 Sep 2024 11:30:52 -0600 Subject: [PATCH] #28718: Adding GH workflow to fetch open PRs and notify about them to assigned dev (#29931) --- .../cicd_scheduled_notify-seated-prs.yml | 196 ++++++++++++++++++ .../workflows/issue_comp_frontend-notify.yml | 2 +- .../issue_comp_next-release-label.yml | 14 +- .../utility_slack-channel-resolver.yml | 5 +- 4 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/cicd_scheduled_notify-seated-prs.yml diff --git a/.github/workflows/cicd_scheduled_notify-seated-prs.yml b/.github/workflows/cicd_scheduled_notify-seated-prs.yml new file mode 100644 index 000000000000..61b174cde97f --- /dev/null +++ b/.github/workflows/cicd_scheduled_notify-seated-prs.yml @@ -0,0 +1,196 @@ +name: 'Notify about seated PRs' +on: + schedule: + - cron: '0 10 * * *' + workflow_dispatch: +env: + PR_DAY_THRESHOLD: 3 + DRAFT_PR_DAY_THRESHOLD: 5 + REPO: core + +jobs: + resolve-seated-prs: + runs-on: ubuntu-22.04 + outputs: + seated_prs: ${{ steps.fetch-seated-prs.outputs.seated_prs }} + members: ${{ steps.fetch-seated-prs.outputs.members }} + members_json: ${{ steps.fetch-seated-prs.outputs.members_json }} + steps: + - run: echo 'GitHub context' + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + - name: Filter execution + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const day = new Date().getDay(); + console.log(new Date()); + if (day === 0 || day === 6) { + console.log('It\'s (happy) weekend, not sending any notifications'); + process.exit(1); + } + - id: fetch-seated-prs + name: Fetch Seated PRs + if: success() + uses: actions/github-script@v7 + with: + result-encoding: string + retries: 3 + retry-exempt-status-codes: 400,401 + script: | + const prDayThreshold = ${{ env.PR_DAY_THRESHOLD }}; + const draftPrDayThreshold = ${{ env.DRAFT_PR_DAY_THRESHOLD }}; + const now = new Date(); + const seatedPrs = []; + const excludedUsers = ['dependabot[bot]'] + + const fetchOpenPrs = async () => { + const opts = github.rest.pulls.list.endpoint.merge({ + ...{ + owner: '${{ github.repository_owner }}', + repo: '${{ env.REPO }}', + per_page: 100 + } + }); + + return await github.paginate(opts); + }; + + const isPrSeated = (pr) => { + const createdAt = new Date(Date.parse(pr.created_at)); + console.log(`Now: ${now} / CreatedAt: ${createdAt}`); + + let weekdaysCount = 0; + for (let date = new Date(createdAt); date <= now; date.setDate(date.getDate() + 1)) { + const dayOfWeek = date.getDay(); + if (dayOfWeek !== 0 && dayOfWeek !== 6) { + weekdaysCount++; + } + } + + const threshold = pr.draft ? draftPrDayThreshold : prDayThreshold; + return weekdaysCount >= threshold; + }; + + const addPr = (pr, login) => { + if (!isPrSeated(pr)) { + return; + } + + let userPrs = seatedPrs.find(pr => pr.login === login); + if (!userPrs) { + userPrs = { + login, + prs: [] + }; + seatedPrs.push(userPrs); + } + + userPrs.prs.push({ + pr_number: pr.number, + url: pr.html_url, + draft: pr.draft, + created_at: pr.created_at, + updated_at: pr.updated_at + }); + }; + + const handlePr = (pr) => { + const login = pr.user.login; + if (excludedUsers.includes(login)) { + return; + } + + addPr(pr, login); + }; + + const prs = await fetchOpenPrs(); + console.log(`PRs size: [${prs.length}]`); + + prs.forEach(handlePr); + const members = seatedPrs.map(pr => pr.login); + console.log(`Seated PRs size: [${seatedPrs.length}]`); + console.log(JSON.stringify(seatedPrs, null, 2)); + console.log(`Users: ${JSON.stringify(members)}`); + + core.setOutput('seated_prs', JSON.stringify(seatedPrs)); + core.setOutput('members', members.join(' ')); + core.setOutput('members_json', JSON.stringify(members)); + + slack-channel-resolver: + name: Resolve Slack Channel + needs: resolve-seated-prs + if: success() && needs.resolve-seated-prs.outputs.members + uses: ./.github/workflows/utility_slack-channel-resolver.yml + with: + github_users: ${{ needs.resolve-seated-prs.outputs.members }} + secrets: + CI_MACHINE_USER: ${{ secrets.CI_MACHINE_USER }} + CI_MACHINE_TOKEN: ${{ secrets.CI_MACHINE_TOKEN }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + notify-seated-prs: + runs-on: ubuntu-22.04 + needs: [resolve-seated-prs, slack-channel-resolver] + if: success() + name: Notifying team member ${{ matrix.member }} + strategy: + fail-fast: false + matrix: + member: ${{ fromJSON(needs.slack-channel-resolver.outputs.channel_ids) }} + steps: + - name: Build Message + id: build-message + uses: actions/github-script@v7 + with: + result-encoding: string + script: | + const urlMapper = (pr) => `- ${pr.url}`; + + const prDayThreshold = ${{ env.PR_DAY_THRESHOLD }}; + const draftPrDayThreshold = ${{ env.DRAFT_PR_DAY_THRESHOLD }}; + const seatedPrs = ${{ needs.resolve-seated-prs.outputs.seated_prs }} + const members = ${{ needs.resolve-seated-prs.outputs.members_json }} + const channels = ${{ needs.slack-channel-resolver.outputs.channel_ids }} + console.log(JSON.stringify(members, null, 2)); + console.log(JSON.stringify(channels, null, 2)); + + const idx = channels.findIndex(channel => channel === '${{ matrix.member }}'); + if (idx === -1) { + console.log('Could not find channel [${{ matrix.member }}], skipping this'); + process.exit(2); + } + + const login = members[idx]; + const userPrs = seatedPrs.find(pr => pr.login === login); + const prs = userPrs.prs.filter(pr => !pr.draft).map(urlMapper); + const draftPrs = userPrs.prs.filter(pr => pr.draft).map(urlMapper); + const prStatement = `The following PRs have at least *${prDayThreshold}* days since created: + ${prs.join('\n')}`; + const draftPrStatement = `The following *draft* PRs have at least *${draftPrDayThreshold}* days since created: + ${draftPrs.join('\n')}`; + + let message = `:hurtrealbad: Attention dev *${login}*! You have PRs seated for a while.`; + if (prs.length > 0) { + message += `\n${prStatement}` + } + if (draftPrs.length > 0) { + message += `\n${draftPrStatement}` + } + + message += `\n\nYou can always check your PRs at: https://github.com/${{ github.repository_owner }}/${{ env.REPO }}/pulls/${login}` + + core.setOutput('message', message); + - name: Notify member + if: success() + shell: bash + run: | + channel=${{ matrix.member }} + + curl -X POST \ + -H "Content-type: application/json" \ + -H "Authorization: Bearer ${{ secrets.SLACK_BOT_TOKEN }}" \ + -d "{ \"channel\":\"${channel}\",\"text\":\"${{ steps.build-message.outputs.message }}\"}" \ + -s \ + https://slack.com/api/chat.postMessage diff --git a/.github/workflows/issue_comp_frontend-notify.yml b/.github/workflows/issue_comp_frontend-notify.yml index 9df0ec59f4e6..a45f2aa5fb0c 100644 --- a/.github/workflows/issue_comp_frontend-notify.yml +++ b/.github/workflows/issue_comp_frontend-notify.yml @@ -150,7 +150,7 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} frontend-notify: - name: Notify team member ${{ matrix.members }} + name: Notify team member ${{ matrix.member }} needs: [resolve-data, slack-channel-resolver] runs-on: ubuntu-20.04 if: success() && needs.resolve-data.outputs.issue_number && needs.slack-channel-resolver.outputs.channel_ids diff --git a/.github/workflows/issue_comp_next-release-label.yml b/.github/workflows/issue_comp_next-release-label.yml index 8259aa4d0c6a..250e422acff6 100644 --- a/.github/workflows/issue_comp_next-release-label.yml +++ b/.github/workflows/issue_comp_next-release-label.yml @@ -1,5 +1,5 @@ # action.yml -name: 'QA not needed update' +name: 'Label to Next Release' on: workflow_call: secrets: @@ -22,6 +22,7 @@ jobs: runs-on: ubuntu-20.04 env: QA_NOT_NEEDED_LABEL: 'QA : Not Needed' + REPO: core steps: - run: echo 'GitHub context' env: @@ -51,11 +52,12 @@ jobs: github-token: ${{ secrets.CI_MACHINE_TOKEN }} script: | async function getIssue(issueNumber) { - return github.rest.issues.get({ + const response = await github.rest.issues.get({ issue_number: issueNumber, owner: '${{ github.repository_owner }}', - repo: 'core' + repo: '${{ env.REPO }}' }); + return response.data; } let issue = context.payload.issue; @@ -71,7 +73,7 @@ jobs: } console.log(`Issue: ${JSON.stringify(issue, null, 2)}`); - const issueNumber = issue.data.number; + const issueNumber = issue.number; const dropAndLearnText = 'Drop Everything & Learn'; if (issue.data.title.includes(dropAndLearnText)) { @@ -89,9 +91,9 @@ jobs: await github.rest.issues.addLabels({ issue_number: issueNumber, owner: '${{ github.repository_owner }}', - repo: 'core', + repo: '${{ env.REPO }}', labels: ['Next Release'] }); const updated = await getIssue(issueNumber); - console.log(`Labels: ${JSON.stringify(updated.data.labels, null, 2)}`); + console.log(`Labels: ${JSON.stringify(updated.labels, null, 2)}`); diff --git a/.github/workflows/utility_slack-channel-resolver.yml b/.github/workflows/utility_slack-channel-resolver.yml index 35f63dd85fcd..8ad2110a5803 100644 --- a/.github/workflows/utility_slack-channel-resolver.yml +++ b/.github/workflows/utility_slack-channel-resolver.yml @@ -72,6 +72,7 @@ jobs: slack_mapping_url=${githack_core_repo_url}/${{ inputs.branch }}/.github/data/${slack_mappings_file} json=$(curl -s ${slack_mapping_url}) + echo "Looking for [${github_user}]" channel_ids= for github_user in "${github_users_array[@]}"; do channel_id=$( \ @@ -94,7 +95,7 @@ jobs: ) echo "Resolved user email: [${user_email}]" - if [[ -n "${user_email}" ]]; then + if [[ -n "${user_email}" && "${user_email}" != 'null' ]]; then channel_id=$( \ curl \ --request GET \ @@ -109,6 +110,8 @@ jobs: ) echo "Resolved channel id [${channel_id}] from email [${user_email}]" + else + echo "Could not resolve email for [${github_user}], skipping it" fi fi