Send Mattermost notification #836
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: Send Mattermost notification | |
| on: | |
| workflow_run: | |
| workflows: [CI check] | |
| types: [completed] | |
| permissions: | |
| actions: read | |
| contents: read | |
| issues: read | |
| pull-requests: read | |
| jobs: | |
| send-mm-notification: | |
| if: ${{ github.event.workflow_run.event == 'pull_request' || github.event.workflow_run.event == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download PR number artifact | |
| id: pr-artifact | |
| if: ${{ github.event.workflow_run.event == 'pull_request' }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ secrets.ADMIN_PAT || github.token }} | |
| run: | | |
| ARTIFACT_ID=$(gh api \ | |
| "/repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/artifacts" \ | |
| --jq '.artifacts[] | select(.name == "pr-number") | .id' | head -1) | |
| if [ -n "$ARTIFACT_ID" ]; then | |
| gh api "/repos/${{ github.repository }}/actions/artifacts/$ARTIFACT_ID/zip" > artifact.zip | |
| unzip -p artifact.zip > pr-number.txt | |
| echo "pr_number=$(cat pr-number.txt | tr -d '[:space:]')" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Parse preferences and send Mattermost notifications | |
| uses: actions/github-script@v7 | |
| env: | |
| MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }} | |
| MATTERMOST_BOT_TOKEN: ${{ secrets.MATTERMOST_BOT_TOKEN }} | |
| DEFAULT_CHANNEL: ${{ vars.MM_CHANNEL }} | |
| PR_NUMBER_FROM_ARTIFACT: ${{ steps.pr-artifact.outputs.pr_number }} | |
| with: | |
| github-token: ${{ secrets.ADMIN_PAT || github.token }} | |
| script: | | |
| const workflowRun = context.payload.workflow_run; | |
| const state = workflowRun.conclusion === 'success' ? 'success' : 'failure'; | |
| const emoji = state === 'success' ? '✅' : ':notlikethis:'; | |
| const defaultChannel = process.env.DEFAULT_CHANNEL; | |
| const promptText = 'Check where you would like a Mattermost message to be sent'; | |
| async function resolveNotificationTargets(pr) { | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number | |
| }); | |
| const mattermostComment = comments.find(c => c.body && c.body.includes(promptText)); | |
| if (!mattermostComment) { | |
| console.log('No permission comment found. Skipping.'); | |
| return null; | |
| } | |
| const escapedDefaultChannel = defaultChannel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| const wantsDM = /-\s*\[[xX]\]\s*Direct message/i.test(mattermostComment.body); | |
| const wantsChannel = new RegExp(`-\\s*\\[[xX]\\]\\s*~${escapedDefaultChannel}`, 'i').test(mattermostComment.body); | |
| if (!wantsDM && !wantsChannel) { | |
| console.log('No options selected. Skipping.'); | |
| return null; | |
| } | |
| const targetChannels = []; | |
| let extraNote = ''; | |
| if (wantsChannel) { | |
| targetChannels.push(defaultChannel); | |
| } | |
| if (wantsDM) { | |
| try { | |
| const { data: commitData } = await github.rest.repos.getCommit({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: pr.head.sha | |
| }); | |
| const email = commitData.commit.author.email; | |
| const mattermostURL = new URL(process.env.MATTERMOST_WEBHOOK_URL).origin; | |
| const mattermostResponse = await fetch(`${mattermostURL}/api/v4/users/email/${email}`, { | |
| headers: { Authorization: `Bearer ${process.env.MATTERMOST_BOT_TOKEN}` } | |
| }); | |
| if (mattermostResponse.ok) { | |
| const mattermostUser = await mattermostResponse.json(); | |
| targetChannels.push(`@${mattermostUser.username}`); | |
| } else { | |
| extraNote = `*(Note: Could not find Mattermost account for ${email}. Falling back to ~${defaultChannel})*`; | |
| if (!targetChannels.includes(defaultChannel)) targetChannels.push(defaultChannel); | |
| } | |
| } catch (error) { | |
| console.error('Mattermost lookup failed:', error); | |
| extraNote = `*(Note: Mattermost user lookup failed. Falling back to ~${defaultChannel})*`; | |
| if (!targetChannels.includes(defaultChannel)) targetChannels.push(defaultChannel); | |
| } | |
| } | |
| return { targetChannels, extraNote }; | |
| } | |
| let statusMessage = ''; | |
| let targetChannels = []; | |
| if (workflowRun.event === 'pull_request') { | |
| let pullRequest = workflowRun.pull_requests[0]; | |
| if (!pullRequest) { | |
| const artifactPrNumber = parseInt(process.env.PR_NUMBER_FROM_ARTIFACT || ''); | |
| if (!artifactPrNumber) { | |
| console.log('No pull request found. Skipping.'); | |
| return; | |
| } | |
| pullRequest = { number: artifactPrNumber }; | |
| console.log(`Resolved pull request #${artifactPrNumber} from pr-number artifact.`); | |
| } | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pullRequest.number | |
| }); | |
| const result = await resolveNotificationTargets(pr); | |
| if (!result) return; | |
| ({ targetChannels } = result); | |
| statusMessage = `${emoji} CI Checks for [PR #${pr.number} ${pr.title}](${pr.html_url}) finished: **${state}**.`; | |
| if (result.extraNote) statusMessage += `\n${result.extraNote}`; | |
| } else if (workflowRun.event === 'merge_group') { | |
| // Merge queue branch format: gh-readonly-queue/{base}/pr-{number}-{sha} | |
| const match = workflowRun.head_branch.match(/pr-(\d+)-/); | |
| if (!match) { | |
| console.log(`Could not extract PR number from merge queue branch: ${workflowRun.head_branch}. Skipping.`); | |
| return; | |
| } | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: parseInt(match[1]) | |
| }); | |
| const result = await resolveNotificationTargets(pr); | |
| if (!result) return; | |
| ({ targetChannels } = result); | |
| const mergeOutcome = state === 'success' ? `merged to \`${pr.base.ref}\`` : 'removed from merge queue'; | |
| statusMessage = `${emoji} [PR #${pr.number} ${pr.title}](${pr.html_url}) ${mergeOutcome}.`; | |
| if (result.extraNote) statusMessage += `\n${result.extraNote}`; | |
| } else { | |
| console.log(`Unsupported workflow_run event: ${workflowRun.event}. Skipping.`); | |
| return; | |
| } | |
| targetChannels = [...new Set(targetChannels)]; | |
| if (targetChannels.length === 0) { | |
| console.log('No targets resolved. Skipping.'); | |
| return; | |
| } | |
| console.log(`Sending notifications to: ${targetChannels.join(', ')}`); | |
| for (const channel of targetChannels) { | |
| const payload = { | |
| text: statusMessage, | |
| channel, | |
| username: 'maasbot', | |
| icon_url: 'https://launchpadlibrarian.net/91453111/Phoenix_landing-64x64.jpg' | |
| }; | |
| const response = await fetch(process.env.MATTERMOST_WEBHOOK_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!response.ok) { | |
| core.setFailed(`Failed to send to ${channel}: ${response.status} ${response.statusText}`); | |
| return; | |
| } | |
| console.log(`Successfully sent to ${channel}`); | |
| } |