Skip to content

Commit 4c9b3f6

Browse files
authored
ci: Improve slack notification for the Tests workflow (#114)
1 parent 5094d7c commit 4c9b3f6

1 file changed

Lines changed: 151 additions & 21 deletions

File tree

.github/workflows/tests.yml

Lines changed: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ jobs:
1010
unit-tests:
1111
name: Unit tests
1212
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
node-version: [24.x]
1316
steps:
1417
- name: Checkout
1518
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
1619
with:
1720
fetch-depth: 0
18-
- name: Use Node.js 24
21+
- name: Use Node.js ${{ matrix.node-version }}
1922
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
20-
with:
21-
node-version: 24
2223
- name: Install Dependencies
2324
run: npm ci
2425
- name: Lint
@@ -28,25 +29,154 @@ jobs:
2829

2930
notify-slack:
3031
name: Notify on failure
31-
needs: [ unit-tests ]
32+
needs:
33+
- unit-tests
3234
if: failure()
3335
runs-on: ubuntu-latest
34-
3536
steps:
36-
- name: Map users
37-
id: map-actor-to-slack
38-
uses: icalia-actions/map-github-actor@e568d1dd6023e406a1db36db4e1e0b92d9dd7824 # v0.0.2
39-
with:
40-
actor-map: ${{ vars.SLACK_GITHUB_USERS_MAP }}
41-
default-mapping: C067BD0377F
42-
43-
- name: Send notification
44-
uses: ravsamhq/notify-slack-action@be814b201e233b2dc673608aa46e5447c8ab13f2 # v2.5.0
45-
with:
46-
status: 'failure'
47-
notification_title: 'FlowFuse Tests Pipeline'
48-
footer: "<{run_url}|View Run>"
49-
mention_users: ${{ steps.map-actor-to-slack.outputs.actor-mapping }}
50-
env:
51-
SLACK_WEBHOOK_URL: ${{ secrets.GH_WORKFLOWS_WEBHOOK }}
37+
- name: Resolve Slack recipients
38+
id: resolve
39+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
40+
env:
41+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_GHBOT_TOKEN }}
42+
SLACK_GITHUB_FIELD_ID: "Xf0A2BPU8U77"
43+
CHANNEL_ID: "C067BD0377F"
44+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
45+
ACTOR: ${{ github.actor }}
46+
EVENT_NAME: ${{ github.event_name }}
47+
with:
48+
script: |
49+
const token = process.env.SLACK_BOT_TOKEN;
50+
const fieldId = process.env.SLACK_GITHUB_FIELD_ID;
51+
52+
// Resolve a GitHub login -> Slack user ID by scanning workspace profiles and
53+
// matching the custom profile field (GitHub). Returns undefined if not found.
54+
async function resolveSlackId(login) {
55+
const target = login.toLowerCase();
56+
let cursor;
57+
do {
58+
const params = new URLSearchParams({ limit: '200' });
59+
if (cursor) params.set('cursor', cursor);
60+
const res = await fetch(`https://slack.com/api/users.list?${params}`, {
61+
headers: { Authorization: `Bearer ${token}` }
62+
});
63+
const data = await res.json();
64+
if (!data.ok) { core.setFailed(`Slack users.list error: ${data.error}`); return; }
65+
for (const member of data.members) {
66+
if (member.deleted || member.is_bot) continue;
67+
const profileRes = await fetch(`https://slack.com/api/users.profile.get?user=${member.id}`, {
68+
headers: { Authorization: `Bearer ${token}` }
69+
});
70+
const profileData = await profileRes.json();
71+
if (!profileData.ok) continue;
72+
const ghField = profileData.profile?.fields?.[fieldId]?.value;
73+
if (!ghField) continue;
74+
if (ghField.toLowerCase() === target) return member.id;
75+
}
76+
cursor = data.response_metadata?.next_cursor;
77+
} while (cursor);
78+
return undefined;
79+
}
80+
81+
// Push to main - notify the channel, mentioning the actor who pushed.
82+
if (process.env.EVENT_NAME !== 'pull_request') {
83+
const actor = process.env.ACTOR;
84+
const mention = actor ? await resolveSlackId(actor) : undefined;
85+
if (!mention) core.warning(`No Slack user found with GitHub username: ${actor}`);
86+
core.setOutput('recipients', JSON.stringify([{ slack_id: process.env.CHANNEL_ID, role: 'Channel', mention: mention || null }]));
87+
return;
88+
}
89+
90+
// Pull request - notify the author. Bot authors have no Slack profile: skip silently.
91+
const author = process.env.PR_AUTHOR;
92+
if (!author || author.endsWith('[bot]')) {
93+
core.setOutput('recipients', '[]');
94+
return;
95+
}
96+
97+
const slackId = await resolveSlackId(author);
98+
if (!slackId) {
99+
core.warning(`No Slack user found with GitHub username: ${author}`);
100+
core.setOutput('recipients', '[]');
101+
return;
102+
}
103+
core.setOutput('recipients', JSON.stringify([{ slack_id: slackId, github: author.toLowerCase(), role: 'Author' }]));
104+
105+
- name: Send Slack notification
106+
if: steps.resolve.outputs.recipients != '' && steps.resolve.outputs.recipients != '[]'
107+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
108+
env:
109+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_GHBOT_TOKEN }}
110+
RECIPIENTS: ${{ steps.resolve.outputs.recipients }}
111+
EVENT_NAME: ${{ github.event_name }}
112+
REF_NAME: ${{ github.ref_name }}
113+
PR_NUMBER: ${{ github.event.pull_request.number }}
114+
COMMIT_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
115+
ACTOR: ${{ github.actor }}
116+
SERVER_URL: ${{ github.server_url }}
117+
REPOSITORY: ${{ github.repository }}
118+
RUN_ID: ${{ github.run_id }}
119+
with:
120+
script: |
121+
const token = process.env.SLACK_BOT_TOKEN;
122+
const recipients = JSON.parse(process.env.RECIPIENTS);
123+
const isBranch = process.env.EVENT_NAME !== 'pull_request';
124+
const refName = process.env.REF_NAME;
125+
const prNumber = process.env.PR_NUMBER;
126+
const commitSha = process.env.COMMIT_SHA;
127+
const actor = process.env.ACTOR;
128+
const serverUrl = process.env.SERVER_URL;
129+
const repository = process.env.REPOSITORY;
130+
const runId = process.env.RUN_ID;
131+
132+
const headerText = isBranch
133+
? `Tests failed against ${refName} branch`
134+
: `Tests failed against #${prNumber} pull request`;
135+
const summaryIcon = isBranch ? 'no_entry' : 'warning';
136+
const summaryText = isBranch
137+
? ' Deployment to FFC environments will not happen until this issue is resolved.'
138+
: ' Please resolve the problem before merging your changes into the main branch.';
139+
const refLink = isBranch
140+
? `*Branch:* ${refName}`
141+
: `*Pull request:* <${serverUrl}/${repository}/pull/${prNumber}|${prNumber}>`;
142+
143+
let failures = 0;
144+
for (const r of recipients) {
145+
// Channel posts mention the actor's Slack ID (a real ping) if resolved;
146+
// otherwise (and for DMs) show the plain GitHub login.
147+
const authorText = r.mention ? `<@${r.mention}>` : actor;
148+
const blocks = [
149+
{ type: 'header', text: { type: 'plain_text', text: `:x: ${headerText}`, emoji: true } },
150+
{ type: 'divider' },
151+
{ type: 'rich_text', elements: [ { type: 'rich_text_section', elements: [
152+
{ type: 'emoji', name: summaryIcon },
153+
{ type: 'text', text: summaryText, style: { bold: true } }
154+
] } ] },
155+
{ type: 'divider' },
156+
{ type: 'section', fields: [
157+
{ type: 'mrkdwn', text: `*Author:* ${authorText}` },
158+
{ type: 'mrkdwn', text: `<${serverUrl}/${repository}/actions/runs/${runId}|View failed workflow>` },
159+
{ type: 'mrkdwn', text: `*Last commit:* <${serverUrl}/${repository}/commit/${commitSha}|${commitSha}>` },
160+
{ type: 'mrkdwn', text: refLink }
161+
] }
162+
];
163+
const res = await fetch('https://slack.com/api/chat.postMessage', {
164+
method: 'POST',
165+
headers: {
166+
Authorization: `Bearer ${token}`,
167+
'Content-Type': 'application/json; charset=utf-8'
168+
},
169+
body: JSON.stringify({ channel: r.slack_id, blocks })
170+
});
171+
const data = await res.json();
172+
if (!data.ok) {
173+
core.warning(`Slack chat.postMessage failed for ${r.slack_id}: ${data.error}`);
174+
failures++;
175+
} else {
176+
core.info(`Notified ${r.slack_id} (${r.role})`);
177+
}
178+
}
52179
180+
if (failures > 0 && failures === recipients.length) {
181+
core.setFailed(`All ${failures} Slack notifications failed`);
182+
}

0 commit comments

Comments
 (0)