Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/workflows/release-complete.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ jobs:
slack-bot-token: ${{ fromJSON(steps.op.outputs.secrets).SLACK_BOT_TOKEN }}
slack-channel-id: ${{ fromJSON(steps.op.outputs.secrets).SLACK_CHANNEL_ID }}
update-ts: ${{ steps.thread.outputs.ts }}
cut-link: "[the release](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.tag }})"
message: |
🎉 *Release ${{ inputs.tag }} — Completed (${{ inputs.env }})* ✅${{ fromJSON(steps.op.outputs.secrets).SLACK_CODEOWNERS_GROUP_ID != '' && format(' <!subteam^{0}>', fromJSON(steps.op.outputs.secrets).SLACK_CODEOWNERS_GROUP_ID) || '' }}
${{ inputs.url != '' && format('*{0}:* {1}', inputs.env, inputs.url) || '' }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-pr-cancel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ jobs:
slack-bot-token: ${{ fromJSON(steps.op.outputs.secrets).SLACK_BOT_TOKEN }}
slack-channel-id: ${{ fromJSON(steps.op.outputs.secrets).SLACK_CHANNEL_ID }}
update-ts: ${{ steps.thread.outputs.ts }}
cut-link: "[the PR](${{ github.event.pull_request.html_url }})"
message: |
🛑 *${{ github.event.pull_request.title }} — Cancelled*${{ fromJSON(steps.op.outputs.secrets).SLACK_CODEOWNERS_GROUP_ID != '' && format(' <!subteam^{0}>', fromJSON(steps.op.outputs.secrets).SLACK_CODEOWNERS_GROUP_ID) || '' }}
*PR:* [${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }})
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-pr-refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ jobs:
slack-bot-token: ${{ fromJSON(steps.op.outputs.secrets).SLACK_BOT_TOKEN }}
slack-channel-id: ${{ fromJSON(steps.op.outputs.secrets).SLACK_CHANNEL_ID }}
update-ts: ${{ steps.markers.outputs.ts }}
cut-link: "[the PR](${{ github.event.pull_request.html_url }})"
message: |
🚀 *${{ github.event.pull_request.title }} — Started*${{ fromJSON(steps.op.outputs.secrets).SLACK_CODEOWNERS_GROUP_ID != '' && format(' <!subteam^{0}>', fromJSON(steps.op.outputs.secrets).SLACK_CODEOWNERS_GROUP_ID) || '' }}
*PR:* ${{ github.event.pull_request.html_url }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-start.yml
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ jobs:
slack-bot-token: ${{ fromJSON(steps.op.outputs.secrets).SLACK_BOT_TOKEN }}
slack-channel-id: ${{ fromJSON(steps.op.outputs.secrets).SLACK_CHANNEL_ID }}
update-ts: ${{ steps.slack.outputs.ts }}
cut-link: "[the PR](${{ steps.pr.outputs.url }})"
message: |
🚀 *Release ${{ steps.branch.outputs.label }} — Started*${{ fromJSON(steps.op.outputs.secrets).SLACK_CODEOWNERS_GROUP_ID != '' && format(' <!subteam^{0}>', fromJSON(steps.op.outputs.secrets).SLACK_CODEOWNERS_GROUP_ID) || '' }}
*PR:* ${{ steps.pr.outputs.url }}
Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces.json
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,10 @@
},
"slack-notify": {
"inputs": {
"cut-link": {
"required": false,
"default": ""
},
"message": {
"required": true
},
Expand Down
5 changes: 5 additions & 0 deletions steps/slack-notify/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inputs:
description: Slack message ts to EDIT (chat.update) instead of posting a new message
required: false
default: ""
cut-link:
description: "Markdown link to the full text, e.g. `[the PR](url)`: a message over Slack's 4,000 characters is cut and ends with `… the rest is in <link>` (`… (truncated)` without one)"
required: false
default: ""

outputs:
ts:
Expand All @@ -35,6 +39,7 @@ runs:
SLACK_CHANNEL_ID: ${{ inputs.slack-channel-id }}
SLACK_THREAD_TS: ${{ inputs.thread-ts }}
SLACK_UPDATE_TS: ${{ inputs.update-ts }}
SLACK_CUT_LINK: ${{ inputs.cut-link }}
MESSAGE: ${{ inputs.message }}
run: |
set -euo pipefail
Expand Down
24 changes: 15 additions & 9 deletions steps/slack-notify/slackNotify.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,25 @@ const markdownToMrkdwn = (text) => {
.replace(/\*\*(?=\S)([^*\n]+?)(?<=\S)\*\*/g, '*$1*')
// Convert headers: ## Header -> *Header*
.replace(/^#{1,6}\s+(.+)$/gm, '*$1*')
// Convert list items: - item -> • item
.replace(/^-\s+/gm, '• ')
// Convert list items, either marker, any nesting: - item / * item -> • item
.replace(/^(\s*)[-*]\s+/gm, '$1• ')
);
};

// 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.
// so no mrkdwn link is split, and the cut is marked — with a link to the full text
// (the PR, the GitHub release) when the caller passes one. 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) => {
const truncationMark = (cutLink) =>
cutLink ? `\n… the rest is in ${cutLink}` : TRUNCATION_MARK;

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

// Sanitize untrusted values before logging to avoid log-forging/control chars.
Expand Down Expand Up @@ -113,7 +118,7 @@ const sendMessage = (token, channel, text, threadTs, updateTs) =>
req.end();
});

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

// Standalone runner
if (require.main === module) {
Expand All @@ -138,7 +143,8 @@ if (require.main === module) {
console.log('Sending Slack notification.');

const text = markdownToMrkdwn(message);
const fittedText = truncateText(text);
const cutLink = markdownToMrkdwn(process.env.SLACK_CUT_LINK || '').trim();
const fittedText = truncateText(text, MAX_TEXT_LENGTH, truncationMark(cutLink));
if (fittedText.length < text.trimEnd().length) {
console.log(
`::warning::Slack message truncated from ${text.length} to ${fittedText.length} characters.`,
Expand Down
20 changes: 19 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, truncateText } = require('./slackNotify');
const { markdownToMrkdwn, truncateText, truncationMark } = require('./slackNotify');

test('converts markdown bold to mrkdwn and leaves mrkdwn bold alone', () => {
assert.equal(
Expand All @@ -18,6 +18,13 @@ test('converts links, headers and list items', () => {
);
});

test('converts star bullets and nested items, and leaves bold alone', () => {
assert.equal(
markdownToMrkdwn('* **APP-1332**: repair tool ([c91d3a5](https://x.y/c))\n * nested\n*Header*'),
'• *APP-1332*: repair tool (<https://x.y/c|c91d3a5>)\n • nested\n*Header*',
);
});

test('keeps text within the limit unchanged', () => {
assert.equal(truncateText('a\nb', 3), 'a\nb');
});
Expand Down Expand Up @@ -47,3 +54,14 @@ test('hard-cuts a single over-limit line', () => {
assert.equal(truncated.length, 4000);
assert.ok(truncated.endsWith('\n… (truncated)'));
});

test('marks the cut with a link to the full text when given one', () => {
const mark = truncationMark('<https://x.y/pull/1|the PR>');
assert.equal(mark, '\n… the rest is in <https://x.y/pull/1|the PR>');
assert.equal(truncationMark(''), '\n… (truncated)');

const list = Array.from({ length: 200 }, () => '• item').join('\n');
const truncated = truncateText(list, 100, mark);
assert.ok(truncated.length <= 100);
assert.ok(truncated.endsWith(mark));
});
Loading