From c0c34ae29e40b62941a1e8d39ace0214cf267617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Tue, 7 Apr 2026 10:06:55 +0200 Subject: [PATCH 1/2] fix: slack section limiting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/libs/slack/src/notify.ts | 44 ++++++++++++++++++------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/services/libs/slack/src/notify.ts b/services/libs/slack/src/notify.ts index 9c92716a58..3df1882b39 100644 --- a/services/libs/slack/src/notify.ts +++ b/services/libs/slack/src/notify.ts @@ -10,6 +10,7 @@ const log = getServiceLogger() // Slack message size limit (keeping it conservative at 30KB) const MAX_MESSAGE_SIZE = 30 * 1024 // 30KB in bytes const MAX_BLOCKS = 50 +const MAX_SECTION_TEXT = 2900 // Slack hard limit is 3000 chars per section block interface SlackBlock { type: string @@ -25,32 +26,37 @@ interface SlackBlock { } /** - * Build content blocks from either a simple string or an array of sections + * Build content blocks from either a simple string or an array of sections. + * Splits text that exceeds Slack's 3000 char per-section limit at line boundaries. */ function buildContentBlocks(content: string | SlackMessageSection[]): SlackBlock[] { if (typeof content === 'string') { - // Simple string content - single section - return [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: content, - }, - }, - ] + return [{ type: 'section', text: { type: 'mrkdwn', text: content } }] } - // Multiple sections - create a block for each section const blocks: SlackBlock[] = [] for (const section of content) { - blocks.push({ - type: 'section', - text: { - type: 'mrkdwn', - text: `*${section.title}*\n${section.text}`, - }, - }) + const fullText = `*${section.title}*\n${section.text}` + if (fullText.length <= MAX_SECTION_TEXT) { + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: fullText } }) + continue + } + + // Split at line boundaries into multiple blocks + const lines = fullText.split('\n') + let current = '' + for (const line of lines) { + const candidate = current ? `${current}\n${line}` : line + if (candidate.length > MAX_SECTION_TEXT && current) { + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: current } }) + current = line + } else { + current = candidate + } + } + if (current) { + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: current } }) + } } return blocks } From a4986c1da9b01b169841ad1b53689d9fc67d63a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Tue, 7 Apr 2026 10:15:24 +0200 Subject: [PATCH 2/2] fix: comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/libs/slack/src/notify.ts | 55 ++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/services/libs/slack/src/notify.ts b/services/libs/slack/src/notify.ts index 3df1882b39..a94887000c 100644 --- a/services/libs/slack/src/notify.ts +++ b/services/libs/slack/src/notify.ts @@ -25,37 +25,52 @@ interface SlackBlock { }> } +/** + * Split text into section blocks, respecting Slack's 3000 char per-section limit. + * Splits at line boundaries where possible; hard-truncates individual lines that + * are themselves longer than the limit. + */ +function splitIntoSectionBlocks(text: string): SlackBlock[] { + const blocks: SlackBlock[] = [] + const lines = text.split('\n') + let current = '' + + for (let line of lines) { + // Hard-truncate a single line that exceeds the limit on its own + if (line.length > MAX_SECTION_TEXT) { + line = `${line.slice(0, MAX_SECTION_TEXT - 1)}…` + } + + const candidate = current ? `${current}\n${line}` : line + if (candidate.length > MAX_SECTION_TEXT && current) { + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: current } }) + current = line + } else { + current = candidate + } + } + + if (current) { + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: current } }) + } + + return blocks +} + /** * Build content blocks from either a simple string or an array of sections. * Splits text that exceeds Slack's 3000 char per-section limit at line boundaries. */ function buildContentBlocks(content: string | SlackMessageSection[]): SlackBlock[] { if (typeof content === 'string') { - return [{ type: 'section', text: { type: 'mrkdwn', text: content } }] + return splitIntoSectionBlocks(content) } const blocks: SlackBlock[] = [] for (const section of content) { const fullText = `*${section.title}*\n${section.text}` - if (fullText.length <= MAX_SECTION_TEXT) { - blocks.push({ type: 'section', text: { type: 'mrkdwn', text: fullText } }) - continue - } - - // Split at line boundaries into multiple blocks - const lines = fullText.split('\n') - let current = '' - for (const line of lines) { - const candidate = current ? `${current}\n${line}` : line - if (candidate.length > MAX_SECTION_TEXT && current) { - blocks.push({ type: 'section', text: { type: 'mrkdwn', text: current } }) - current = line - } else { - current = candidate - } - } - if (current) { - blocks.push({ type: 'section', text: { type: 'mrkdwn', text: current } }) + for (const block of splitIntoSectionBlocks(fullText)) { + blocks.push(block) } } return blocks