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
237 changes: 150 additions & 87 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,93 +279,156 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Map users
id: map-actor-to-slack
uses: icalia-actions/map-github-actor@e568d1dd6023e406a1db36db4e1e0b92d9dd7824 # v0.0.2
with:
actor-map: ${{ vars.SLACK_GITHUB_USERS_MAP }}
default-mapping: C067BD0377F

- name: Generate payload variables
run: |
if [[ "${{ github.ref_name }}" == 'main' || "${{ github.ref_name }}" == 'maintenance' ]] ; then
echo "HEADER_MESSAGE=Tests failed against ${{ github.ref_name }} branch" >> $GITHUB_ENV
echo "SUMMARY_ICON=no_entry" >> $GITHUB_ENV
echo "SUMMARY_MESSAGE= Deployment to FFC environments will not happen until this issue is resolved." >> $GITHUB_ENV
echo "LAST_COMMIT_SHA=${{ github.sha}}" >> $GITHUB_ENV
echo "PR_LINK=*Branch:* ${{ github.ref_name }}" >> $GITHUB_ENV
else
echo "HEADER_MESSAGE=Tests failed against ${{ github.event.number }} pull request" >> $GITHUB_ENV
echo "SUMMARY_ICON=warning" >> $GITHUB_ENV
echo "SUMMARY_MESSAGE= Please resolve the problem before merging your changes into the main branch." >> $GITHUB_ENV
echo "LAST_COMMIT_SHA=${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV
echo "PR_LINK=*Pull request:* <https://github.com/FlowFuse/flowfuse/pull/${{ github.event.pull_request.number }}|${{ github.event.pull_request.number }}>" >> $GITHUB_ENV
fi
# On pull_request failure: resolve the PR author's Slack ID by scanning Slack
# profiles and matching the custom profile field (GitHub). Bot authors are skipped
# silently; an unresolved author logs a warning and no notification is sent.
# On push to main: target the gh-pipelines channel directly.
# Outputs `recipients` as JSON: [{ slack_id, github?, role }].
- 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;

- name: Send notification
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_GHBOT_TOKEN }}
payload: |
{
"channel": "C067BD0377F",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":x: ${{ env.HEADER_MESSAGE }}",
"emoji": true
// 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;
}
},
{
"type": "divider"
},
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "emoji",
"name": "${{ env.SUMMARY_ICON }}"
},
{
"type": "text",
"text": " ${{ env.SUMMARY_MESSAGE }}",
"style": {
"bold": true
}
}
]
}
]
},
{
"type": "divider"
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Author:* <@${{ steps.map-actor-to-slack.outputs.actor-mapping }}>"
},
{
"type": "mrkdwn",
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View failed workflow>"
},
{
"type": "mrkdwn",
"text": "*Last commit:* <${{ github.server_url }}/${{ github.repository }}/commit/${{ env.LAST_COMMIT_SHA }}|${{ env.LAST_COMMIT_SHA }}>"
},
{
"type": "mrkdwn",
"text": "${{ env.PR_LINK }}"
}
]
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' }]));

# Sends the failure notification to each recipient (DM to the PR author, or the channel
# on main). chat.postMessage accepts a user ID or a channel ID as `channel`.
- 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`);
}
Loading