|
1 | 1 | import chalk from 'chalk'; |
| 2 | +import { request as httpsRequest } from 'https'; |
2 | 3 | import { ChangelogReleaseNotes } from './extract-release-notes'; |
3 | 4 |
|
4 | | -const { green, red, cyan } = chalk; |
| 5 | +const { cyan, green, red } = chalk; |
5 | 6 |
|
6 | 7 | type MattermostConfig = { |
7 | 8 | url: string; |
8 | 9 | channel: string; |
9 | | - username: string; |
10 | 10 | }; |
11 | 11 |
|
12 | 12 | function getMattermostConfig(): MattermostConfig | null { |
13 | 13 | const url = process.env['MATTERMOST_WEBHOOK_URL']; |
14 | 14 | const channel = process.env['MATTERMOST_CHANNEL']; |
15 | | - const username = process.env['MATTERMOST_USERNAME']; |
16 | 15 |
|
17 | | - if (!url || !channel || !username) { |
| 16 | + if (!url || !channel) { |
18 | 17 | return null; |
19 | 18 | } |
20 | 19 |
|
21 | | - return { url, channel, username }; |
| 20 | + return { url, channel }; |
22 | 21 | } |
23 | 22 |
|
| 23 | +// Use https.request (not fetch/undici) — the Mattermost webhook is behind a WAF |
| 24 | +// that blocks requests by TLS fingerprint / HTTP/2. The Node built-in HTTP stack |
| 25 | +// (HTTP/1.1 + standard TLS) is the same one @actions/http-client uses and passes |
| 26 | +// the WAF. Don't "modernize" this back to fetch without re-checking the WAF. |
24 | 27 | async function sendNotification(url: string, body: object): Promise<void> { |
25 | | - const headers: Record<string, string> = { 'Content-Type': 'application/json' }; |
26 | | - const requestOptions: RequestInit = { |
27 | | - method: 'POST', |
28 | | - headers, |
29 | | - body: JSON.stringify(body) |
| 28 | + const payload = JSON.stringify(body); |
| 29 | + const headers: Record<string, string> = { |
| 30 | + 'Content-Type': 'application/json; charset=utf-8', |
| 31 | + Accept: 'application/json', |
| 32 | + 'Content-Length': Buffer.byteLength(payload).toString() |
30 | 33 | }; |
31 | 34 |
|
32 | | - console.info(cyan('POST notification:'), { headers, body: JSON.stringify(body) }); |
33 | | - |
34 | | - const response = await fetch(url, requestOptions); |
35 | | - |
36 | | - if (!response.ok) { |
37 | | - throw new Error(`Failed to post notification: ${response.status} ${response.statusText}`); |
| 35 | + console.info(cyan(' [DEBUG] HTTP request:'), { |
| 36 | + method: 'POST', |
| 37 | + url, |
| 38 | + headers, |
| 39 | + body: payload |
| 40 | + }); |
| 41 | + |
| 42 | + const parsed = new URL(url); |
| 43 | + const { statusCode, statusMessage, responseBody } = await new Promise<{ |
| 44 | + statusCode: number; |
| 45 | + statusMessage: string; |
| 46 | + responseBody: string; |
| 47 | + }>((resolve, reject) => { |
| 48 | + const req = httpsRequest( |
| 49 | + { |
| 50 | + method: 'POST', |
| 51 | + hostname: parsed.hostname, |
| 52 | + port: parsed.port || undefined, |
| 53 | + path: parsed.pathname + parsed.search, |
| 54 | + headers |
| 55 | + }, |
| 56 | + (res) => { |
| 57 | + let data = ''; |
| 58 | + |
| 59 | + res.setEncoding('utf8'); |
| 60 | + res.on('data', (chunk: string) => { |
| 61 | + data += chunk; |
| 62 | + }); |
| 63 | + res.on('end', () => { |
| 64 | + resolve({ |
| 65 | + statusCode: res.statusCode ?? 0, |
| 66 | + statusMessage: res.statusMessage ?? '', |
| 67 | + responseBody: data |
| 68 | + }); |
| 69 | + }); |
| 70 | + } |
| 71 | + ); |
| 72 | + |
| 73 | + req.on('error', reject); |
| 74 | + req.write(payload); |
| 75 | + req.end(); |
| 76 | + }); |
| 77 | + |
| 78 | + console.info(cyan(` [DEBUG] HTTP response: ${statusCode} ${statusMessage}`)); |
| 79 | + |
| 80 | + if (statusCode < 200 || statusCode >= 300) { |
| 81 | + console.error(red(` ✘ Mattermost response body: ${responseBody}`)); |
| 82 | + throw new Error(`Failed to post notification: ${statusCode} ${statusMessage}`); |
38 | 83 | } |
39 | 84 | } |
40 | 85 |
|
@@ -67,8 +112,8 @@ export async function notify(releaseData: ChangelogReleaseNotes): Promise<void> |
67 | 112 | } |
68 | 113 |
|
69 | 114 | const body = { |
70 | | - channel: `${config.channel}`, |
71 | | - username: `${config.username}`, |
| 115 | + channel: config.channel, |
| 116 | + username: 'Wall-e', |
72 | 117 | text: `${releaseData.releaseTitle}\n${releaseData.releaseNotes}` |
73 | 118 | }; |
74 | 119 |
|
|
0 commit comments