Report seated PRs #149
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
name: 'Notify about seated PRs' | |
on: | |
schedule: | |
- cron: '0 10 * * *' | |
workflow_dispatch: | |
inputs: | |
current_user: | |
description: 'Limit execution of this workflow to the current user' | |
type: string | |
required: false | |
default: U0125JCDSSE | |
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 | |
id: 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(0); | |
} | |
core.setOutput('continue', 'true'); | |
- id: fetch-seated-prs | |
name: Fetch Seated PRs | |
if: success() && steps.filter-execution.outputs.continue == 'true' | |
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 member = '${{ matrix.member }}'; | |
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 mappings = ${{ needs.slack-channel-resolver.outputs.mappings_json }}; | |
const foundMapping = mappings.find(mapping => mapping.slack_id === member) | |
if (!foundMapping) { | |
core.warning(`Slack user Id [${member}] cannot be found, exiting`); | |
process.exit(0); | |
} | |
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 login = foundMapping.github_user; | |
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() && steps.build-message.outputs.message != '' | |
shell: bash | |
run: | | |
channel=${{ matrix.member }} | |
echo "Sending notification to ${channel}" | |
if [[ -x '${{ inputs.current_user }}' || "${channel}" == '${{ inputs.current_user }}' ]]; then | |
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 | |
fi |