Skip to content
Open
Show file tree
Hide file tree
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
42 changes: 38 additions & 4 deletions steps/slack-notify/slackNotify.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@
);
};

// chat.update rejects `text` over 4,000 characters (`msg_too_long`), and a long
// release changelog gets there. The body is cut at the last line break that fits,
// so no mrkdwn link is split, and the cut is marked. A short closing paragraph
// survives the cut: release flags such as the DB-migration warning live there.
const MAX_TEXT_LENGTH = 4000;
const MAX_KEPT_TAIL_LENGTH = 500;
const TRUNCATION_MARK = '\n… (truncated)';

const truncateText = (text, limit = MAX_TEXT_LENGTH) => {
if (text.length <= limit) {
return text;
}
const trimmed = text.trimEnd();
if (trimmed.length <= limit) {
return trimmed;
}
const tailStart = trimmed.lastIndexOf('\n\n');
const tail =
tailStart > 0 && trimmed.length - tailStart <= MAX_KEPT_TAIL_LENGTH
? trimmed.slice(tailStart)
: '';
const room = limit - TRUNCATION_MARK.length - tail.length;
const lineEnd = trimmed.lastIndexOf('\n', room);
return `${trimmed.slice(0, lineEnd > 0 ? lineEnd : room)}${TRUNCATION_MARK}${tail}`;
};

// Posts a new message, or edits an existing one when `updateTs` is set
// (chat.update) — used to reflect lifecycle status (started → cancelled →
// completed) in the release's head message rather than only as thread replies.
Expand Down Expand Up @@ -79,7 +105,7 @@
req.end();
});

module.exports = { isValidSlackTs, markdownToMrkdwn };
module.exports = { isValidSlackTs, markdownToMrkdwn, truncateText };

// Standalone runner
if (require.main === module) {
Expand All @@ -103,7 +129,15 @@

console.log('Sending Slack notification.');

sendMessage(token, channel, markdownToMrkdwn(message), threadTs, updateTs)
const text = markdownToMrkdwn(message);
const fittedText = truncateText(text);
if (fittedText.length < text.trimEnd().length) {
console.log(
`::warning::Slack message truncated from ${text.length} to ${fittedText.length} characters.`,
);
}

sendMessage(token, channel, fittedText, threadTs, updateTs)
.then((ts) => {
if (!isValidSlackTs(ts)) {
console.error('Slack returned an invalid ts.');
Expand All @@ -117,8 +151,8 @@
console.log(`::set-output name=ts::${ts}`);
}
})
.catch(() => {
console.error('Failed to send Slack message.');
.catch((error) => {
console.error(`Failed to send Slack message: ${error.message}`);
process.exit(1);
});
}
32 changes: 31 additions & 1 deletion steps/slack-notify/slackNotify.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { markdownToMrkdwn } = require('./slackNotify');
const { markdownToMrkdwn, truncateText } = require('./slackNotify');

test('converts markdown bold to mrkdwn and leaves mrkdwn bold alone', () => {
assert.equal(
Expand All @@ -17,3 +17,33 @@ test('converts links, headers and list items', () => {
'*Features*\n• <https://x.y/z|APP-1: title>',
);
});

test('keeps text within the limit unchanged', () => {
assert.equal(truncateText('a\nb', 3), 'a\nb');
});

test('cuts over-limit text at the last line break that fits and marks it', () => {
const line = '• <https://x.y/z|APP-1: title>';
const text = Array.from({ length: 200 }, () => line).join('\n');
const truncated = truncateText(text);

assert.ok(truncated.length <= 4000);
assert.ok(truncated.endsWith('\n… (truncated)'));
const kept = truncated.slice(0, -'\n… (truncated)'.length).split('\n');
assert.ok(kept.every((keptLine) => keptLine === line));
});

test('keeps a short closing paragraph after the cut', () => {
const list = Array.from({ length: 200 }, () => '• item').join('\n');
const truncated = truncateText(`${list}\n\n⚠️ *Contains DB migrations*\n`, 100);

assert.ok(truncated.length <= 100);
assert.ok(truncated.endsWith('\n… (truncated)\n\n⚠️ *Contains DB migrations*'));
});

test('hard-cuts a single over-limit line', () => {
const truncated = truncateText('x'.repeat(5000));

assert.equal(truncated.length, 4000);
assert.ok(truncated.endsWith('\n… (truncated)'));
});
Loading