From 8ca9da2ca8e3435ee9b0d387355c0fae255c16b0 Mon Sep 17 00:00:00 2001 From: Ryan Hullah Date: Mon, 11 Dec 2023 11:18:02 -0500 Subject: [PATCH] feat: added additional description formatting (#23) remove: removed HTML comments added by the 'Generate release notes' button feat: description trimming style: reduce consecutive whitespace/newlines into a minimum of 2 to allow separation in paragraphs style: parse common Github URLs to more appropriate display feat: added max_description option feat: added reduce_headings option --- README.md | 22 +++++----- action.yml | 8 ++++ index.js | 125 +++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 121 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 84e58ac..17076f6 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,18 @@ A GitHub action that parses a GitHub release and posts it to a Discord channel a ## Configuration -| Variable | Required | Default | Description | -|-----------------|----------|----------------------------------------------------------------------------------------------------------------|--------------------------------------------| -| webhook_url | ✔ | | Discord's webhook url. Use GH repo secrets.| -| color | ❌ | "2105893" | Decimal color value for embed. | -| username | ❌ | | String username for webhook. | -| avatar_url | ❌ | | String url to webhook avatar picture. | -| content | ❌ | | String content for webhook. | -| footer_title | ❌ | | String title for the webhook footer. | -| footer_icon_url | ❌ | | String url for the webhook footer picture. | -| footer_timestamp| ❌ | | Boolean to enable footer timestamp. | +| Variable | Required | Default | Description | +|-----------------|----------|-------------------------------------------------------------------------------------------------------|-------------------------------------------------| +| webhook_url | ✔ | | Discord's webhook url. Use GH repo secrets. | +| color | ❌ | "2105893" | Decimal color value for embed. | +| username | ❌ | | String username for webhook. | +| avatar_url | ❌ | | String url to webhook avatar picture. | +| content | ❌ | | String content for webhook. | +| footer_title | ❌ | | String title for the webhook footer. | +| footer_icon_url | ❌ | | String url for the webhook footer picture. | +| footer_timestamp| ❌ | | Boolean to enable footer timestamp. | +| max_description | ❌ | "4096" | Max length for the description. | +| reduce_headings | ❌ | false | Converts H3 to bold, h2 to bold & underline. | ## Example Usage diff --git a/action.yml b/action.yml index e59516a..fe0e570 100644 --- a/action.yml +++ b/action.yml @@ -27,6 +27,14 @@ inputs: footer_timestamp: description: Timestamp for the footer. required: false + max_description: + description: Max length for the description. + required: false + default: '4096' + reduce_headings: + description: Converts H3 to bold, h2 to bold & underline. + required: false + default: 'false' runs: using: 'node16' main: 'index.js' diff --git a/index.js b/index.js index e14ce96..6025134 100644 --- a/index.js +++ b/index.js @@ -4,45 +4,119 @@ import fetch from 'node-fetch'; /** * Stylizes a markdown body into an appropriate embed message style. - * H3s converted to bold and underlined. - * H2s converted to bold. - * Redundant whitespace and newlines removed. - * @param description - * @returns {*} + * Remove HTML comments (commonly added by 'Generate release notes' button) + * Better URL linking for common Github links: PRs, Issues, Compare + * Redundant whitespace and newlines removed, keeping at max 2 to provide space between paragraphs + * Trim leading/trailing whitespace + * If reduce_headings: + * H3s converted to bold and underlined + * H2s converted to bold + * @param {string} description */ const formatDescription = (description) => { - return description - .replace(/### (.*?)\n/g,function (substring) { - const newString = substring.slice(4).replace(/(\r\n|\n|\r)/gm, "") - return `**__${newString}__**` + let edit = description + .replace(//gs, '') + .replace( + new RegExp( + "https://github.com/(.+)/(.+)/(issues|pull|commit|compare)/(\\S+)", + "g" + ), + (match, user, repo, type, id) => { + return `[${getTypePrefix(type) + id}](${match})` + } + ) + .replace(/\n\s*\n/g, (ws) => { + const nlCount = (ws.match(/\n/g) || []).length + return nlCount >= 2 ? '\n\n' : '\n' }) - .replace(/## (.*?)\n/g,function (substring) { - const newString = substring.slice(3).replace(/(\r\n|\n|\r)/gm, "") - return `**${newString}**` - }) - .replace(/\n\s*\n/g, '\n') + .trim() + + if (core.getBooleanInput('reduce_headings')) { + edit = edit + .replace(/^###\s+(.+)$/gm, '**__$1__**') + .replace(/^##\s+(.+)$/gm, '**$1**') + } + + return edit +} + +/** + * Get a prefix to use for Github link display + * @param {'issues' | 'pull' | 'commit' | 'compare'} type + */ +function getTypePrefix (type) { + switch (type) { + case 'issues': + return 'Issue #' + case 'pull': + return 'PR #' + case 'commit': + return 'Commit #' + case 'compare': + return '' + default: + return '#' + } +} + +/** + * Gets the max description length if set to a valid number, + * otherwise the default of 4096 + */ +function getMaxDescription () { + try { + const max = core.getInput('max_description') + if (typeof max === 'string' && max.length) { + // 4096 is max for Embed Description + // https://discord.com/developers/docs/resources/channel#embed-object-embed-limits + return Math.min(parseInt(max, 10), 4096) + } + } catch (err) { + core.warning(`max_description not a valid number: ${err}`) + } + return 4096 } /** * Get the context of the action, returns a GitHub Release payload. - * @returns {Promise<{html_url, body: (*|string), name: string}>} */ -async function getContext () { +function getContext () { const payload = github.context.payload; return { - body: payload.release.body.length < 1500 - ? payload.release.body - : payload.release.body.substring(0, 1500) + ` ([...](${payload.release.html_url}))`, - name: payload.release.name, + body: payload.release.body, + name: payload.release.name, html_url: payload.release.html_url } } +/** + * + * @param {string} str + * @param {number} maxLength + * @param {string=} url + */ +function limit(str, maxLength, url) { + if (str.length <= maxLength) + return str + let replacement = '…' + if (url) { + replacement = `([${replacement}](${url}))` + } + maxLength = maxLength - replacement.length + str = str.substring(0, maxLength) + + const lastWhitespace = str.search(/[^\s]*$/) + if (lastWhitespace > -1) { + str = str.substring(0, lastWhitespace) + } + + return str + replacement +} + /** * Handles the action. * Get inputs, creates a stylized response webhook, and sends it to the channel. - * @returns {Promise} */ async function run () { const webhookUrl = core.getInput('webhook_url'); @@ -56,22 +130,25 @@ async function run () { if (!webhookUrl) return core.setFailed('webhook_url not set. Please set it.'); - const {body, html_url, name} = await getContext(); + const {body, html_url, name} = getContext(); const description = formatDescription(body); let embedMsg = { - title: name, + title: limit(name, 256), url: html_url, color: color, description: description, footer: {} } - if (footerTitle != '') embedMsg.footer.text = footerTitle; + if (footerTitle != '') embedMsg.footer.text = limit(footerTitle, 2048); if (footerIconUrl != '') embedMsg.footer.icon_url = footerIconUrl; if (footerTimestamp == 'true') embedMsg.timestamp = new Date().toISOString(); + let embedSize = embedMsg.title.length + (embedMsg.footer?.text?.length ?? 0) + embedMsg.description = limit(embedMsg.description, Math.min(getMaxDescription(), 6000 - embedSize), embedMsg.url) + let requestBody = { embeds: [embedMsg] }