@@ -584,178 +584,33 @@ jobs:
584584 PR_NUMBER : ${{ github.event.number == '' && inputs.pr_number || github.event.number }}
585585
586586 steps :
587- # Builds the recipient list (PR author + requested reviewers, or github.actor on workflow_dispatch)
588- # and resolves each GitHub username to a Slack user ID by scanning workspace profiles and picking the value of custom profile field (GitHub).
589- # Outputs `recipients` as JSON: [{ slack_id, github, role }, ...]. Unresolved users are skipped with a warning.
590- - name : Resolve Slack users for recipients
591- id : resolve
592- uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
593- env :
594- SLACK_BOT_TOKEN : ${{ secrets.SLACK_GHBOT_TOKEN }}
595- SLACK_GITHUB_FIELD_ID : " Xf0A2BPU8U77"
596- PR_AUTHOR : ${{ github.event.pull_request.user.login }}
597- REQUESTED_REVIEWERS : ${{ toJSON(github.event.pull_request.requested_reviewers) }}
598- GITHUB_ACTOR : ${{ github.actor }}
599- EVENT_NAME : ${{ github.event_name }}
587+ - name : Notify Slack
588+ uses : FlowFuse/github-actions-workflows/actions/slack_notification@slack_notification/v1
600589 with :
601- script : |
602- const token = process.env.SLACK_BOT_TOKEN;
603- const fieldId = process.env.SLACK_GITHUB_FIELD_ID;
604-
605- // Build recipient list with roles, deduped (first-added role wins: Author > Reviewer > Pusher).
606- // Bot accounts (e.g. dependabot[bot]) are skipped — they have no Slack profile.
607- const recipients = [];
608- const seen = new Set();
609- const add = (login, role) => {
610- if (!login) return;
611- if (login.endsWith('[bot]')) return;
612- const key = login.toLowerCase();
613- if (seen.has(key)) return;
614- seen.add(key);
615- recipients.push({ github: key, role });
616- };
617-
618- if (process.env.EVENT_NAME === 'workflow_dispatch') {
619- add(process.env.GITHUB_ACTOR, 'Trigger');
620- } else {
621- add(process.env.PR_AUTHOR, 'Author');
622- const reviewers = JSON.parse(process.env.REQUESTED_REVIEWERS || '[]');
623- for (const r of reviewers) add(r.login, 'Reviewer');
624- add(process.env.GITHUB_ACTOR, 'Pusher');
625- }
626-
627- if (recipients.length === 0) {
628- core.warning('No recipients to notify');
629- core.setOutput('recipients', '[]');
630- return;
590+ bot-token : ${{ secrets.SLACK_GHBOT_TOKEN }}
591+ mode : contributors
592+ values : |
593+ {
594+ "PR_TITLE": ${{ toJSON(github.event.pull_request.title || format('#{0}', env.PR_NUMBER)) }},
595+ "LOGS_URL": ${{ toJSON(format('https://{0}/explore?orgId=1&left=%7B%22datasource%22:%22P8E80F9AEF21F6940%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22editorMode%22:%22code%22,%22expr%22:%22%7Bnamespace%3D%5C%22pr-{1}%5C%22%7D%20%7C%3D%20%60%60%20%7C%20json%22,%22queryType%22:%22range%22%7D%5D,%22range%22:%7B%22from%22:%22now-30m%22,%22to%22:%22now%22%7D%7D', secrets.STAGING_GRAFANA_URL, env.PR_NUMBER)) }}
631596 }
632-
633- // Build Slack username -> ID map by scanning workspace profiles
634- const targets = new Set(recipients.map(r => r.github));
635- const resolved = new Map();
636- let cursor;
637- outer: do {
638- const params = new URLSearchParams({ limit: '200' });
639- if (cursor) params.set('cursor', cursor);
640- const res = await fetch(`https://slack.com/api/users.list?${params}`, {
641- headers: { Authorization: `Bearer ${token}` }
642- });
643- const data = await res.json();
644- if (!data.ok) return core.setFailed(`Slack users.list error: ${data.error}`);
645- for (const member of data.members) {
646- if (member.deleted || member.is_bot) continue;
647- const profileRes = await fetch(`https://slack.com/api/users.profile.get?user=${member.id}`, {
648- headers: { Authorization: `Bearer ${token}` }
649- });
650- const profileData = await profileRes.json();
651- if (!profileData.ok) continue;
652- const ghField = profileData.profile?.fields?.[fieldId]?.value;
653- if (!ghField) continue;
654- const ghLower = ghField.toLowerCase();
655- if (targets.has(ghLower)) {
656- resolved.set(ghLower, member.id);
657- if (resolved.size === targets.size) break outer;
658- }
659- }
660- cursor = data.response_metadata?.next_cursor;
661- } while (cursor);
662-
663- const output = [];
664- for (const r of recipients) {
665- const slack_id = resolved.get(r.github);
666- if (slack_id) {
667- output.push({ slack_id, github: r.github, role: r.role });
668- } else {
669- core.warning(`No Slack user found with GitHub username: ${r.github} (role: ${r.role})`);
670- }
671- }
672- core.info(`Resolved ${output.length}/${recipients.length} recipients`);
673- core.setOutput('recipients', JSON.stringify(output));
674-
675- # Sends one DM per resolved recipient via Slack chat.postMessage.
676- # Same message content for everyone, with a *Role:* field indicating Author / Reviewer / Trigger.
677- # Step fails only if all sends fail; partial failures are logged as warnings.
678- - name : Send Slack notifications
679- if : steps.resolve.outputs.recipients != '' && steps.resolve.outputs.recipients != '[]'
680- uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
681- env :
682- SLACK_BOT_TOKEN : ${{ secrets.SLACK_GHBOT_TOKEN }}
683- RECIPIENTS : ${{ steps.resolve.outputs.recipients }}
684- PR_NUMBER : ${{ env.PR_NUMBER }}
685- PR_TITLE : ${{ github.event.pull_request.title }}
686- PR_AUTHOR : ${{ github.event.pull_request.user.login }}
687- DEPLOY_RESULT : ${{ needs.deploy.result }}
688- COMMIT_SHA : ${{ github.event.pull_request.head.sha }}
689- SERVER_URL : ${{ github.server_url }}
690- REPOSITORY : ${{ github.repository }}
691- RUN_ID : ${{ github.run_id }}
692- GRAFANA_URL : ${{ secrets.STAGING_GRAFANA_URL }}
693- with :
694- script : |
695- const token = process.env.SLACK_BOT_TOKEN;
696- const recipients = JSON.parse(process.env.RECIPIENTS);
697- const prNumber = process.env.PR_NUMBER;
698- const prTitle = process.env.PR_TITLE || `#${prNumber}`;
699- const prAuthor = process.env.PR_AUTHOR || 'n/a';
700- const deployResult = process.env.DEPLOY_RESULT;
701- const commitSha = process.env.COMMIT_SHA;
702- const serverUrl = process.env.SERVER_URL;
703- const repository = process.env.REPOSITORY;
704- const runId = process.env.RUN_ID;
705- const grafanaUrl = process.env.GRAFANA_URL;
706-
707- const statusText = deployResult === 'success' ? ':white_check_mark: Success' : ':x: Failure ';
708- const logsUrl = `https://${grafanaUrl}/explore?orgId=1&left=%7B%22datasource%22:%22P8E80F9AEF21F6940%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22editorMode%22:%22code%22,%22expr%22:%22%7Bnamespace%3D%5C%22pr-${prNumber}%5C%22%7D%20%7C%3D%20%60%60%20%7C%20json%22,%22queryType%22:%22range%22%7D%5D,%22range%22:%7B%22from%22:%22now-30m%22,%22to%22:%22now%22%7D%7D`;
709-
710- let failures = 0;
711- for (const r of recipients) {
712- const blocks = [
713- {
714- type: 'header',
715- text: { type: 'plain_text', text: `Pull Request ${prNumber} pre-staging deployment`, emoji: true }
716- },
717- {
718- type: 'section',
719- fields: [
720- { type: 'mrkdwn', text: `*Status:*\n${statusText}` },
721- { type: 'mrkdwn', text: `*Role:*\n${r.role}` },
722- { type: 'mrkdwn', text: `*Pull Request:*\n<https://github.com/FlowFuse/flowfuse/pull/${prNumber}|${prTitle}>` },
723- { type: 'mrkdwn', text: `*Workflow run:*\n<${serverUrl}/${repository}/actions/runs/${runId}|View>` }
724- ]
725- },
726- {
727- type: 'section',
728- fields: [
729- { type: 'mrkdwn', text: `*Author:*\n${prAuthor}` },
730- { type: 'mrkdwn', text: `*Commit SHA:*\n<${serverUrl}/${repository}/commit/${commitSha}|${commitSha}>` },
731- { type: 'mrkdwn', text: `*Deployed to:*\n<https://${prNumber}.flowfuse.dev|https://${prNumber}.flowfuse.dev>` },
732- { type: 'mrkdwn', text: `*Logs:*\n<${logsUrl}|View>` }
733- ]
734- }
735- ];
736-
737- const res = await fetch('https://slack.com/api/chat.postMessage', {
738- method: 'POST',
739- headers: {
740- Authorization: `Bearer ${token}`,
741- 'Content-Type': 'application/json; charset=utf-8'
742- },
743- body: JSON.stringify({ channel: r.slack_id, blocks })
744- });
745- const data = await res.json();
746- if (!data.ok) {
747- core.warning(`Slack chat.postMessage failed for ${r.github} (${r.slack_id}): ${data.error}`);
748- failures++;
749- } else {
750- core.info(`Notified ${r.github} (${r.role}) at ${r.slack_id}`);
751- }
752- }
753-
754- if (failures > 0 && failures === recipients.length) {
755- core.setFailed(`All ${failures} Slack notifications failed`);
756- }
757-
758-
597+ blocks : |
598+ [
599+ { "type": "header", "text": { "type": "plain_text", "text": "Pull Request ${{ env.PR_NUMBER }} pre-staging deployment", "emoji": true } },
600+ { "type": "section", "fields": [
601+ { "type": "mrkdwn", "text": "*Status:*\n${{ needs.deploy.result == 'success' && ':white_check_mark: Success' || ':x: Failure' }}" },
602+ { "type": "mrkdwn", "text": "*Role:*\n{{ROLE}}" },
603+ { "type": "mrkdwn", "text": "*Pull Request:*\n<https://github.com/FlowFuse/flowfuse/pull/${{ env.PR_NUMBER }}|{{PR_TITLE}}>" },
604+ { "type": "mrkdwn", "text": "*Workflow run:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View>" }
605+ ] },
606+ { "type": "section", "fields": [
607+ { "type": "mrkdwn", "text": "*Author:*\n${{ github.event.pull_request.user.login || 'n/a' }}" },
608+ { "type": "mrkdwn", "text": "*Commit SHA:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }}|${{ github.event.pull_request.head.sha }}>" },
609+ { "type": "mrkdwn", "text": "*Deployed to:*\n<https://${{ env.PR_NUMBER }}.flowfuse.dev|https://${{ env.PR_NUMBER }}.flowfuse.dev>" },
610+ { "type": "mrkdwn", "text": "*Logs:*\n<{{LOGS_URL}}|View>" }
611+ ] }
612+ ]
613+
759614 destroy :
760615 name : Remove application
761616 needs : [ validate-user ]
0 commit comments