Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 21 additions & 145 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,149 +34,25 @@ jobs:
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 }}
- name: Notify Slack
uses: FlowFuse/github-actions-workflows/actions/slack_notification@slack_notification/v1
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`);
}
bot-token: ${{ secrets.SLACK_GHBOT_TOKEN }}
mode: ${{ github.event_name == 'pull_request' && 'author' || 'channel' }}
mention-actor: ${{ github.event_name != 'pull_request' }}
blocks: |
[
{ "type": "header", "text": { "type": "plain_text", "text": ":x: ${{ github.event_name == 'pull_request' && format('Tests failed against #{0} pull request', github.event.pull_request.number) || format('Tests failed against {0} branch', github.ref_name) }}", "emoji": true } },
{ "type": "divider" },
{ "type": "rich_text", "elements": [ { "type": "rich_text_section", "elements": [
{ "type": "emoji", "name": "${{ github.event_name == 'pull_request' && 'warning' || 'no_entry' }}" },
{ "type": "text", "text": "${{ github.event_name == 'pull_request' && ' Please resolve the problem before merging your changes into the main branch.' || ' Deployment to FFC environments will not happen until this issue is resolved.' }}", "style": { "bold": true } }
] } ] },
{ "type": "divider" },
{ "type": "section", "fields": [
{ "type": "mrkdwn", "text": "*Author:* ${{ github.event_name == 'pull_request' && github.actor || '{{MENTION}}' }}" },
{ "type": "mrkdwn", "text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View failed workflow>" },
{ "type": "mrkdwn", "text": "${{ format('*Last commit:* <{0}/{1}/commit/{2}|{2}>', github.server_url, github.repository, github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha) }}" },
{ "type": "mrkdwn", "text": "${{ github.event_name == 'pull_request' && format('*Pull request:* <{0}/{1}/pull/{2}|{2}>', github.server_url, github.repository, github.event.pull_request.number) || format('*Branch:* {0}', github.ref_name) }}" }
] }
]