@@ -3,176 +3,201 @@ import github from '@actions/github';
3
3
import fetch from 'node-fetch' ;
4
4
5
5
/**
6
- * Stylizes a markdown body into an appropriate embed message style.
7
- * Remove Carriage Return character to reduce size
8
- * Remove HTML comments (commonly added by 'Generate release notes' button)
9
- * Better URL linking for common Github links: PRs, Issues, Compare
10
- * Redundant whitespace and newlines removed, keeping at max 2 to provide space between paragraphs
11
- * Trim leading/trailing whitespace
12
- * If reduce_headings:
13
- * H3s converted to bold and underlined
14
- * H2s converted to bold
15
- * @param {string } description
6
+ * Removes carriage return characters.
7
+ * @param {string } text The input text.
8
+ * @returns {string } The text without carriage return characters.
16
9
*/
17
- const formatDescription = ( description ) => {
18
- let edit = description
19
- . replace ( / \r / g, '' )
20
- . replace ( / < ! - - .* ?- - > / gs, '' )
21
- . replace (
22
- new RegExp (
23
- "https://github.com/(.+)/(.+)/(issues|pull|commit|compare)/(\\S+)" ,
24
- "g"
25
- ) ,
26
- ( match , user , repo , type , id ) => {
27
- return `[${ getTypePrefix ( type ) + id } ](${ match } )`
28
- }
29
- )
30
- . replace ( / \n \s * \n / g, ( ws ) => {
31
- const nlCount = ( ws . match ( / \n / g) || [ ] ) . length
32
- return nlCount >= 2 ? '\n\n' : '\n'
33
- } )
34
- . replace ( / @ ( \S + ) / g, ( match , name ) => { return `[@${ name } ](https://github.com/${ name } )` } )
35
- . trim ( )
10
+ const removeCarriageReturn = ( text ) => text . replace ( / \r / g, '' ) ;
36
11
37
- if ( core . getBooleanInput ( 'reduce_headings' ) ) {
38
- edit = edit
39
- . replace ( / ^ # # # \s + ( .+ ) $ / gm, '**__$1__**' )
40
- . replace ( / ^ # # \s + ( .+ ) $ / gm, '**$1**' )
41
- }
12
+ /**
13
+ * Removes HTML comments.
14
+ * @param {string } text The input text.
15
+ * @returns {string } The text without HTML comments.
16
+ */
17
+ const removeHTMLComments = ( text ) => text . replace ( / < ! - - .* ?- - > / gs, '' ) ;
42
18
43
- return edit
44
- }
19
+ /**
20
+ * Reduces redundant newlines and spaces.
21
+ * Keeps a maximum of 2 newlines to provide spacing between paragraphs.
22
+ * @param {string } text The input text.
23
+ * @returns {string } The text with reduced newlines.
24
+ */
25
+ const reduceNewlines = ( text ) => text . replace ( / \n \s * \n / g, ( ws ) => {
26
+ const nlCount = ( ws . match ( / \n / g) || [ ] ) . length ;
27
+ return nlCount >= 2 ? '\n\n' : '\n' ;
28
+ } ) ;
29
+
30
+ /**
31
+ * Converts @mentions to GitHub profile links.
32
+ * @param {string } text The input text.
33
+ * @returns {string } The text with @mentions converted to links.
34
+ */
35
+ const convertMentionsToLinks = ( text ) => text . replace ( / @ ( \S + ) / g, ( match , name ) => `[@${ name } ](https://github.com/${ name } )` ) ;
45
36
46
37
/**
47
- * Get a prefix to use for Github link display
48
- * @param {'issues' | 'pull' | 'commit' | 'compare' } type
38
+ * Reduces headings to a smaller format if 'reduce_headings' is enabled.
39
+ * Converts H3 to bold+underline, H2 to bold.
40
+ * @param {string } text The input text.
41
+ * @returns {string } The text with reduced heading sizes.
49
42
*/
50
- function getTypePrefix ( type ) {
51
- switch ( type ) {
52
- case 'issues' :
53
- return 'Issue #'
54
- case 'pull' :
55
- return 'PR #'
56
- case 'commit' :
57
- return 'Commit #'
58
- case 'compare' :
59
- return ''
60
- default :
61
- return '#'
43
+ const reduceHeadings = ( text ) => text
44
+ . replace ( / ^ # # # \s + ( .+ ) $ / gm, '**__$1__**' ) // Convert H3 to bold + underline
45
+ . replace ( / ^ # # \s + ( .+ ) $ / gm, '**$1**' ) ; // Convert H2 to bold
46
+
47
+ /**
48
+ * Stylizes a markdown body into an appropriate embed message style.
49
+ * @param {string } description The description to format.
50
+ * @returns {string } The formatted description.
51
+ */
52
+ const formatDescription = ( description ) => {
53
+ let edit = removeCarriageReturn ( description ) ;
54
+ edit = removeHTMLComments ( edit ) ;
55
+ edit = reduceNewlines ( edit ) ;
56
+ edit = convertMentionsToLinks ( edit ) ;
57
+ edit = edit . trim ( ) ;
58
+
59
+ if ( core . getBooleanInput ( 'reduce_headings' ) ) {
60
+ edit = reduceHeadings ( edit ) ;
62
61
}
63
- }
62
+
63
+ return edit ;
64
+ } ;
64
65
65
66
/**
66
- * Gets the max description length if set to a valid number,
67
- * otherwise the default of 4096
67
+ * Gets the max description length, defaulting to 4096 if not set or invalid.
68
+ * @returns { number } The max description length.
68
69
*/
69
- function getMaxDescription ( ) {
70
+ const getMaxDescription = ( ) => {
70
71
try {
71
- const max = core . getInput ( 'max_description' )
72
- if ( typeof max === 'string' && max . length ) {
73
- // 4096 is max for Embed Description
74
- // https://discord.com/developers/docs/resources/channel#embed-object-embed-limits
75
- return Math . min ( parseInt ( max , 10 ) , 4096 )
72
+ const max = core . getInput ( 'max_description' ) ;
73
+ if ( max && ! isNaN ( max ) ) {
74
+ return Math . min ( parseInt ( max , 10 ) , 4096 ) ;
76
75
}
77
76
} catch ( err ) {
78
- core . warning ( `max_description not a valid number : ${ err } ` )
77
+ core . warning ( `Invalid max_description : ${ err } ` ) ;
79
78
}
80
- return 4096
81
- }
79
+ return 4096 ;
80
+ } ;
82
81
83
82
/**
84
- * Get the context of the action, returns a GitHub Release payload.
83
+ * Get the context of the action, returning a GitHub Release payload.
84
+ * @returns {object } The context with release details.
85
85
*/
86
- function getContext ( ) {
87
- const payload = github . context . payload ;
88
-
86
+ const getContext = ( ) => {
87
+ const { release } = github . context . payload ;
89
88
return {
90
- body : payload . release . body ,
91
- name : payload . release . name ,
92
- html_url : payload . release . html_url
93
- }
94
- }
89
+ body : release . body ,
90
+ name : release . name ,
91
+ html_url : release . html_url
92
+ } ;
93
+ } ;
95
94
96
95
/**
97
- *
98
- * @param {string } str
99
- * @param {number } maxLength
100
- * @param {string } [url]
101
- * @param {boolean } [clipAtLine=false]
96
+ * Limits the string to a maximum length, optionally adding a URL or clipping at a newline.
97
+ * @param {string } str The string to limit.
98
+ * @param {number } maxLength The maximum allowed length.
99
+ * @param {string } [url] Optional URL for linking the truncated text.
100
+ * @param {boolean } [clipAtLine=false] Whether to clip at the nearest newline.
101
+ * @returns {string } The limited string.
102
102
*/
103
- function limit ( str , maxLength , url , clipAtLine ) {
104
- clipAtLine ??= false
105
- if ( str . length <= maxLength )
106
- return str
107
- let replacement = clipAtLine ? '\n…' : '…'
108
- if ( url ) {
109
- replacement = `${ clipAtLine ? '\n' : '' } ([…](${ url } ))`
110
- }
111
- maxLength = maxLength - replacement . length
112
- str = str . substring ( 0 , maxLength )
103
+ const limitString = ( str , maxLength , url , clipAtLine = false ) => {
104
+ if ( str . length <= maxLength ) return str ;
113
105
114
- const lastNewline = str . search ( new RegExp ( `[^${ clipAtLine ? '\n' : '\s' } ]*$` ) )
106
+ const replacement = url
107
+ ? `${ clipAtLine ? '\n' : '' } ([…](${ url } ))`
108
+ : ( clipAtLine ? '\n…' : '…' ) ;
109
+
110
+ maxLength -= replacement . length ;
111
+ str = str . substring ( 0 , maxLength ) ;
112
+
113
+ const lastNewline = str . search ( new RegExp ( `[^${ clipAtLine ? '\n' : '\s' } ]*$` ) ) ;
115
114
if ( lastNewline > - 1 ) {
116
- str = str . substring ( 0 , lastNewline )
115
+ str = str . substring ( 0 , lastNewline ) ;
117
116
}
118
117
119
- return str + replacement
120
- }
118
+ return str + replacement ;
119
+ } ;
121
120
122
121
/**
123
- * Handles the action.
124
- * Get inputs, creates a stylized response webhook, and sends it to the channel.
122
+ * Builds the embed message for the Discord webhook.
123
+ * @param {string } name The title or name of the release.
124
+ * @param {string } html_url The URL of the release.
125
+ * @param {string } description The formatted description of the release.
126
+ * @returns {object } The embed message to send in the webhook.
125
127
*/
126
- async function run ( ) {
127
- const webhookUrl = core . getInput ( 'webhook_url' ) ;
128
- const color = core . getInput ( 'color' ) ;
129
- const username = core . getInput ( 'username' ) ;
130
- const avatarUrl = core . getInput ( 'avatar_url' ) ;
131
- const content = core . getInput ( 'content' ) ;
132
- const footerTitle = core . getInput ( 'footer_title' ) ;
133
- const footerIconUrl = core . getInput ( 'footer_icon_url' ) ;
134
- const footerTimestamp = core . getInput ( 'footer_timestamp' ) ;
135
-
136
- if ( ! webhookUrl ) return core . setFailed ( 'webhook_url not set. Please set it.' ) ;
128
+ const buildEmbedMessage = ( name , html_url , description ) => {
129
+ const embedMsg = {
130
+ title : limitString ( name , 256 ) ,
131
+ url : html_url ,
132
+ color : core . getInput ( 'color' ) ,
133
+ description : limitString ( description , Math . min ( getMaxDescription ( ) , 6000 - name . length ) ) ,
134
+ footer : { }
135
+ } ;
137
136
138
- const { body, html_url, name} = getContext ( ) ;
137
+ if ( core . getInput ( 'footer_title' ) ) {
138
+ embedMsg . footer . text = limitString ( core . getInput ( 'footer_title' ) , 2048 ) ;
139
+ }
140
+ if ( core . getInput ( 'footer_icon_url' ) ) {
141
+ embedMsg . footer . icon_url = core . getInput ( 'footer_icon_url' ) ;
142
+ }
143
+ if ( core . getInput ( 'footer_timestamp' ) === 'true' ) {
144
+ embedMsg . timestamp = new Date ( ) . toISOString ( ) ;
145
+ }
139
146
140
- const description = formatDescription ( body ) ;
147
+ return embedMsg ;
148
+ } ;
141
149
142
- let embedMsg = {
143
- title : limit ( name , 256 ) ,
144
- url : html_url ,
145
- color : color ,
146
- description : description ,
147
- footer : { }
150
+ /**
151
+ * Sends the webhook request to Discord.
152
+ * @param {string } webhookUrl The URL of the Discord webhook.
153
+ * @param {object } requestBody The payload to send in the webhook.
154
+ */
155
+ const sendWebhook = async ( webhookUrl , requestBody ) => {
156
+ try {
157
+ const response = await fetch ( `${ webhookUrl } ?wait=true` , {
158
+ method : 'POST' ,
159
+ body : JSON . stringify ( requestBody ) ,
160
+ headers : { 'Content-Type' : 'application/json' }
161
+ } ) ;
162
+ const data = await response . json ( ) ;
163
+ core . info ( JSON . stringify ( data ) ) ;
164
+ } catch ( err ) {
165
+ core . setFailed ( err . message ) ;
148
166
}
167
+ } ;
149
168
150
- if ( footerTitle != '' ) embedMsg . footer . text = limit ( footerTitle , 2048 ) ;
151
- if ( footerIconUrl != '' ) embedMsg . footer . icon_url = footerIconUrl ;
152
- if ( footerTimestamp == 'true' ) embedMsg . timestamp = new Date ( ) . toISOString ( ) ;
169
+ /**
170
+ * Builds the request body for the Discord webhook.
171
+ * @param {object } embedMsg The embed message to include in the request body.
172
+ * @returns {object } The request body for the webhook.
173
+ */
174
+ const buildRequestBody = ( embedMsg ) => {
175
+ return {
176
+ embeds : [ embedMsg ] ,
177
+ ...( core . getInput ( 'username' ) && { username : core . getInput ( 'username' ) } ) ,
178
+ ...( core . getInput ( 'avatar_url' ) && { avatar_url : core . getInput ( 'avatar_url' ) } ) ,
179
+ ...( core . getInput ( 'content' ) && { content : core . getInput ( 'content' ) } )
180
+ } ;
181
+ } ;
153
182
154
- let embedSize = embedMsg . title . length + ( embedMsg . footer ?. text ?. length ?? 0 )
155
- embedMsg . description = limit ( embedMsg . description , Math . min ( getMaxDescription ( ) , 6000 - embedSize ) , embedMsg . url , true )
156
183
157
- let requestBody = {
158
- embeds : [ embedMsg ]
159
- }
184
+ /**
185
+ * Main function to handle the action: get inputs, format the message, and send the webhook.
186
+ */
187
+ const run = async ( ) => {
188
+ const webhookUrl = core . getInput ( 'webhook_url' ) ;
189
+ if ( ! webhookUrl ) return core . setFailed ( 'webhook_url not set.' ) ;
190
+
191
+ const { body, html_url, name } = getContext ( ) ;
192
+ const description = formatDescription ( body ) ;
193
+
194
+ const embedMsg = buildEmbedMessage ( name , html_url , description ) ;
195
+
196
+ const requestBody = buildRequestBody ( embedMsg ) ;
160
197
161
- if ( username != '' ) requestBody . username = username ;
162
- if ( avatarUrl != '' ) requestBody . avatar_url = avatarUrl ;
163
- if ( content != '' ) requestBody . content = content ;
164
-
165
- const url = `${ webhookUrl } ?wait=true` ;
166
- fetch ( url , {
167
- method : 'post' ,
168
- body : JSON . stringify ( requestBody ) ,
169
- headers : { 'Content-Type' : 'application/json' }
170
- } )
171
- . then ( res => res . json ( ) )
172
- . then ( data => core . info ( JSON . stringify ( data ) ) )
173
- . catch ( err => core . info ( err ) )
174
- }
198
+ await sendWebhook ( webhookUrl , requestBody ) ;
199
+ } ;
175
200
176
201
run ( )
177
- . then ( ( ) => { core . info ( 'Action completed successfully' ) } )
178
- . catch ( err => { core . setFailed ( err . message ) } )
202
+ . then ( ( ) => core . info ( 'Action completed successfully' ) )
203
+ . catch ( err => core . setFailed ( err . message ) ) ;
0 commit comments