ci: Improve slack notification for the Tests workflow (#114)
#250
This file contains hidden or 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: Tests | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| jobs: | |
| unit-tests: | |
| name: Unit tests | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| node-version: [24.x] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: Use Node.js ${{ matrix.node-version }} | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| - name: Install Dependencies | |
| run: npm ci | |
| - name: Lint | |
| run: npm run lint | |
| - name: Unit tests | |
| run: npm run test | |
| notify-slack: | |
| name: Notify on failure | |
| needs: | |
| - unit-tests | |
| if: failure() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve Slack recipients | |
| id: resolve | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| SLACK_BOT_TOKEN: ${{ secrets.SLACK_GHBOT_TOKEN }} | |
| SLACK_GITHUB_FIELD_ID: "Xf0A2BPU8U77" | |
| CHANNEL_ID: "C067BD0377F" | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| ACTOR: ${{ github.actor }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| with: | |
| script: | | |
| const token = process.env.SLACK_BOT_TOKEN; | |
| const fieldId = process.env.SLACK_GITHUB_FIELD_ID; | |
| // Resolve a GitHub login -> Slack user ID by scanning workspace profiles and | |
| // matching the custom profile field (GitHub). Returns undefined if not found. | |
| async function resolveSlackId(login) { | |
| const target = login.toLowerCase(); | |
| let cursor; | |
| do { | |
| const params = new URLSearchParams({ limit: '200' }); | |
| if (cursor) params.set('cursor', cursor); | |
| const res = await fetch(`https://slack.com/api/users.list?${params}`, { | |
| headers: { Authorization: `Bearer ${token}` } | |
| }); | |
| const data = await res.json(); | |
| if (!data.ok) { core.setFailed(`Slack users.list error: ${data.error}`); return; } | |
| for (const member of data.members) { | |
| if (member.deleted || member.is_bot) continue; | |
| const profileRes = await fetch(`https://slack.com/api/users.profile.get?user=${member.id}`, { | |
| headers: { Authorization: `Bearer ${token}` } | |
| }); | |
| const profileData = await profileRes.json(); | |
| if (!profileData.ok) continue; | |
| const ghField = profileData.profile?.fields?.[fieldId]?.value; | |
| if (!ghField) continue; | |
| if (ghField.toLowerCase() === target) return member.id; | |
| } | |
| cursor = data.response_metadata?.next_cursor; | |
| } while (cursor); | |
| return undefined; | |
| } | |
| // Push to main - notify the channel, mentioning the actor who pushed. | |
| if (process.env.EVENT_NAME !== 'pull_request') { | |
| const actor = process.env.ACTOR; | |
| const mention = actor ? await resolveSlackId(actor) : undefined; | |
| if (!mention) core.warning(`No Slack user found with GitHub username: ${actor}`); | |
| core.setOutput('recipients', JSON.stringify([{ slack_id: process.env.CHANNEL_ID, role: 'Channel', mention: mention || null }])); | |
| return; | |
| } | |
| // Pull request - notify the author. Bot authors have no Slack profile: skip silently. | |
| const author = process.env.PR_AUTHOR; | |
| if (!author || author.endsWith('[bot]')) { | |
| core.setOutput('recipients', '[]'); | |
| return; | |
| } | |
| const slackId = await resolveSlackId(author); | |
| if (!slackId) { | |
| core.warning(`No Slack user found with GitHub username: ${author}`); | |
| core.setOutput('recipients', '[]'); | |
| return; | |
| } | |
| core.setOutput('recipients', JSON.stringify([{ slack_id: slackId, github: author.toLowerCase(), role: 'Author' }])); | |
| - name: Send Slack notification | |
| if: steps.resolve.outputs.recipients != '' && steps.resolve.outputs.recipients != '[]' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| SLACK_BOT_TOKEN: ${{ secrets.SLACK_GHBOT_TOKEN }} | |
| RECIPIENTS: ${{ steps.resolve.outputs.recipients }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| REF_NAME: ${{ github.ref_name }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| COMMIT_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| ACTOR: ${{ github.actor }} | |
| SERVER_URL: ${{ github.server_url }} | |
| REPOSITORY: ${{ github.repository }} | |
| RUN_ID: ${{ github.run_id }} | |
| with: | |
| script: | | |
| const token = process.env.SLACK_BOT_TOKEN; | |
| const recipients = JSON.parse(process.env.RECIPIENTS); | |
| const isBranch = process.env.EVENT_NAME !== 'pull_request'; | |
| const refName = process.env.REF_NAME; | |
| const prNumber = process.env.PR_NUMBER; | |
| const commitSha = process.env.COMMIT_SHA; | |
| const actor = process.env.ACTOR; | |
| const serverUrl = process.env.SERVER_URL; | |
| const repository = process.env.REPOSITORY; | |
| const runId = process.env.RUN_ID; | |
| const headerText = isBranch | |
| ? `Tests failed against ${refName} branch` | |
| : `Tests failed against #${prNumber} pull request`; | |
| const summaryIcon = isBranch ? 'no_entry' : 'warning'; | |
| const summaryText = isBranch | |
| ? ' Deployment to FFC environments will not happen until this issue is resolved.' | |
| : ' Please resolve the problem before merging your changes into the main branch.'; | |
| const refLink = isBranch | |
| ? `*Branch:* ${refName}` | |
| : `*Pull request:* <${serverUrl}/${repository}/pull/${prNumber}|${prNumber}>`; | |
| let failures = 0; | |
| for (const r of recipients) { | |
| // Channel posts mention the actor's Slack ID (a real ping) if resolved; | |
| // otherwise (and for DMs) show the plain GitHub login. | |
| const authorText = r.mention ? `<@${r.mention}>` : actor; | |
| const blocks = [ | |
| { type: 'header', text: { type: 'plain_text', text: `:x: ${headerText}`, emoji: true } }, | |
| { type: 'divider' }, | |
| { type: 'rich_text', elements: [ { type: 'rich_text_section', elements: [ | |
| { type: 'emoji', name: summaryIcon }, | |
| { type: 'text', text: summaryText, style: { bold: true } } | |
| ] } ] }, | |
| { type: 'divider' }, | |
| { type: 'section', fields: [ | |
| { type: 'mrkdwn', text: `*Author:* ${authorText}` }, | |
| { type: 'mrkdwn', text: `<${serverUrl}/${repository}/actions/runs/${runId}|View failed workflow>` }, | |
| { type: 'mrkdwn', text: `*Last commit:* <${serverUrl}/${repository}/commit/${commitSha}|${commitSha}>` }, | |
| { type: 'mrkdwn', text: refLink } | |
| ] } | |
| ]; | |
| const res = await fetch('https://slack.com/api/chat.postMessage', { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| 'Content-Type': 'application/json; charset=utf-8' | |
| }, | |
| body: JSON.stringify({ channel: r.slack_id, blocks }) | |
| }); | |
| const data = await res.json(); | |
| if (!data.ok) { | |
| core.warning(`Slack chat.postMessage failed for ${r.slack_id}: ${data.error}`); | |
| failures++; | |
| } else { | |
| core.info(`Notified ${r.slack_id} (${r.role})`); | |
| } | |
| } | |
| if (failures > 0 && failures === recipients.length) { | |
| core.setFailed(`All ${failures} Slack notifications failed`); | |
| } |