From d82f3c1f49364b217aa6dae5f02fd917fe6b8e87 Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Mon, 27 Jul 2026 12:16:05 +0530 Subject: [PATCH] Fix Slack notifications: 'fields' is not a Block Kit block type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slack notifications have never been deliverable. Both payload builders in src/notifications/index.js emitted a block of `{ type: 'fields', fields }`, but Block Kit has no `fields` block type — fields belong on a `section` block. Slack rejects the entire attachment with: 400 invalid_attachments Verified against a live Incoming Webhook: failing before this change, delivering successfully after it. The bogus type had propagated to every consumer, so each one is updated to read fields off a section block while still tolerating the old shape, which keeps any in-flight or persisted payloads converting correctly: - channels/teams.js - facts extraction - channels/discord.js - embed fields - channels/text.js - plain-text flattening (covered by the existing "slackPayloadToText renders blocks as readable plain text" test, which caught this consumer) Teams and Discord converters produce identical output to before the change. tests/notifications-channels.test.js, harness-notifications.test.js and notify-hook.test.js pass 21/21. Co-Authored-By: Claude Opus 5 (1M context) --- src/notifications/channels/discord.js | 3 ++- src/notifications/channels/teams.js | 3 ++- src/notifications/channels/text.js | 3 ++- src/notifications/index.js | 6 ++++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/notifications/channels/discord.js b/src/notifications/channels/discord.js index 5d875ddc..847c1daf 100644 --- a/src/notifications/channels/discord.js +++ b/src/notifications/channels/discord.js @@ -21,7 +21,8 @@ export function convertSlackToDiscord(slackPayload) { if (block.type === 'section' && block.text) { descriptionLines.push(block.text.text); } - if (block.type === 'fields' && block.fields) { + // Fields ride on a section block; 'fields' is tolerated for older payloads. + if ((block.type === 'section' || block.type === 'fields') && block.fields) { for (const f of block.fields) { const raw = f.text || ''; const parts = raw.split('\n'); diff --git a/src/notifications/channels/teams.js b/src/notifications/channels/teams.js index 28b86a5e..0b6bc4b9 100644 --- a/src/notifications/channels/teams.js +++ b/src/notifications/channels/teams.js @@ -24,7 +24,8 @@ export function convertSlackToTeams(slackPayload) { facts: [] }); } - if (block.type === 'fields' && block.fields) { + // Fields ride on a section block; 'fields' is tolerated for older payloads. + if ((block.type === 'section' || block.type === 'fields') && block.fields) { const currentSection = sections[sections.length - 1] || { facts: [] }; if (!sections.includes(currentSection)) { sections.push(currentSection); diff --git a/src/notifications/channels/text.js b/src/notifications/channels/text.js index adad2663..2814c08f 100644 --- a/src/notifications/channels/text.js +++ b/src/notifications/channels/text.js @@ -23,7 +23,8 @@ export function slackPayloadToText(slackPayload) { if (block.type === 'section' && block.text?.text) { lines.push(stripMrkdwn(block.text.text)); } - if (block.type === 'fields' && Array.isArray(block.fields)) { + // Fields ride on a section block; 'fields' is tolerated for older payloads. + if ((block.type === 'section' || block.type === 'fields') && Array.isArray(block.fields)) { for (const field of block.fields) { const parts = String(field.text ?? '').split('\n'); const name = stripMrkdwn(parts[0]); diff --git a/src/notifications/index.js b/src/notifications/index.js index 14e3a2ea..1c20acef 100644 --- a/src/notifications/index.js +++ b/src/notifications/index.js @@ -89,7 +89,8 @@ export function formatSlackStageMessage(runId, stageId, status, details = {}) { }, }, { - type: 'fields', + // Block Kit has no 'fields' block type — fields live on a section block. + type: 'section', fields, }, ]; @@ -146,7 +147,8 @@ export function formatSlackTaskReportMessage(runId, taskId, trace) { }, }, { - type: 'fields', + // Block Kit has no 'fields' block type — fields live on a section block. + type: 'section', fields: [ { type: 'mrkdwn', text: `*Run ID:*\n${runId}` }, { type: 'mrkdwn', text: `*Task:*\n${taskId}` },