Skip to content

Commit 21adf20

Browse files
committed
chore(repo): Improve Slack notification formatter
1 parent 0627ba9 commit 21adf20

File tree

2 files changed

+126
-52
lines changed

2 files changed

+126
-52
lines changed

.github/workflows/release-prod.yml

+3-14
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,17 @@ jobs:
5252
inputs: { version: clerkjsVersion }
5353
})
5454
55-
- name: Generate notification
55+
- name: Generate notification payload
5656
id: notification
5757
if: steps.changesets.outputs.published == 'true'
58-
run: message=$(node scripts/notify.mjs '${{ steps.changesets.outputs.publishedPackages }}' '${{ github.event.number }}') && echo ::set-output name=message::${message//$'\n'/'%0A'}
58+
run: payload=$(node scripts/notify.mjs '${{ steps.changesets.outputs.publishedPackages }}' '${{ github.event.pusher.username }}') && echo ::set-output name=payload::${payload//$'\n'/'%0A'}
5959

6060
- name: Send commit log to Slack
6161
id: slack
6262
if: steps.changesets.outputs.published == 'true'
6363
uses: slackapi/slack-github-action@v1.18.0
6464
with:
65-
payload: |
66-
{
67-
"blocks": [
68-
{
69-
"type": "section",
70-
"text": {
71-
"type": "mrkdwn",
72-
"text": ${{ steps.notification.outputs.message }}
73-
}
74-
}
75-
]
76-
}
65+
payload: ${{ steps.notification.outputs.payload }}
7766
env:
7867
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CHANGELOG_WEBHOOK_URL }}
7968
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

scripts/notify.mjs

+123-38
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,104 @@ const { GITHUB_REF = 'main' } = process.env;
66
const baseUrl = new URL(`https://github.com/clerkinc/javascript/blob/${GITHUB_REF}/`);
77

88
/**
9-
* @returns {Promise<Map<string,{ version: string, path: string }>>}
9+
* @typedef {Object} PackageData
10+
* @property {string} name
11+
* @property {string} version
12+
* @property {string} changelogUrl
1013
*/
11-
async function createPackageMap() {
14+
15+
/**
16+
* @typedef {Object} Pusher
17+
* @property {string} username
18+
* @property {string} avatarUrl
19+
* @property {string} profileUrl
20+
*/
21+
22+
/**
23+
* @typedef {Object} ChangelogData
24+
* @property {PackageData[]} packageData
25+
* @property {string} releasePrUrl
26+
* @property {Pusher} pusher
27+
*/
28+
29+
/**
30+
* @typedef {Object} Formatter
31+
* @property {(data: ChangelogData) => string} generateChangelog
32+
*/
33+
34+
/**
35+
* Slack is using their own Markdown format, see:
36+
* https://api.slack.com/reference/surfaces/formatting#basics
37+
* https://app.slack.com/block-kit-builder
38+
* @type {Formatter}
39+
*/
40+
const slackFormatter = {
41+
generateChangelog: ({ packageData, releasePrUrl, pusher }) => {
42+
const markdown = text => ({ type: 'section', text: { type: 'mrkdwn', text } });
43+
const divider = () => ({ type: 'divider' });
44+
const header = text => ({ type: 'header', text: { type: 'plain_text', text } });
45+
const context = (imgUrl, text) => ({
46+
type: 'context',
47+
elements: [
48+
...(imgUrl ? [{ type: 'image', image_url: imgUrl, alt_text: 'avatar' }] : []),
49+
{ type: 'mrkdwn', text },
50+
],
51+
});
52+
const blocks = [];
53+
54+
blocks.push(header(`Javascript SDKs - Stable Release - ${new Date().toLocaleDateString('en-US')}`));
55+
56+
let body = '';
57+
for (const { name, version, changelogUrl } of packageData) {
58+
body += `• <${changelogUrl}|Changelog> - \`${name}@${version}\`\n`;
59+
}
60+
61+
blocks.push(markdown(`All release PRs for this day can be found <${releasePrUrl}|here>.\nReleased packages:\n`));
62+
blocks.push(markdown(body));
63+
// blocks.push(divider());
64+
blocks.push(markdown('\n'));
65+
blocks.push(context(pusher.avatarUrl, `<${pusher.profileUrl}|*${pusher.username}*> triggered this release.`));
66+
67+
return JSON.stringify({ blocks });
68+
},
69+
};
70+
71+
/**
72+
* @type {Record<string, Formatter>}
73+
*/
74+
const formatters = {
75+
slack: slackFormatter,
76+
};
77+
78+
const run = async () => {
79+
const releasedPackages = JSON.parse(process.argv[2]);
80+
const packageToPathMap = await createPackageToPathMap();
81+
const packageData = createPackageData(releasedPackages, packageToPathMap);
82+
const releasePrUrl = createReleasePrUrl();
83+
const pusher = createPusher(process.argv[3]);
84+
const data = { packageData, releasePrUrl, pusher };
85+
86+
// TODO: Add support for more formatters
87+
const formatter = formatters['slack'];
88+
if (!formatter) {
89+
throw new Error('Invalid formatter, supported formatters are: ' + Object.keys(formatters).join(', '));
90+
}
91+
console.log(formatter.generateChangelog(data));
92+
};
93+
94+
run();
95+
96+
/*
97+
* @returns {Pusher}
98+
*/
99+
const createPusher = username => {
100+
return { username, avatarUrl: `https://github.com/${username}.png`, profileUrl: `https://github.com/${username}` };
101+
};
102+
103+
/**
104+
* @returns {Promise<Map<string,string>>}
105+
*/
106+
async function createPackageToPathMap() {
12107
const map = new Map();
13108
const packagesRoot = new URL('../packages/', import.meta.url);
14109
const packages = await glob(['*/package.json', '*/*/package.json'], { cwd: fileURLToPath(packagesRoot) });
@@ -17,46 +112,36 @@ async function createPackageMap() {
17112
const packageJsonPath = fileURLToPath(new URL(pkg, packagesRoot));
18113
const packageJson = fs.readJSONSync(packageJsonPath);
19114
if (!packageJson.private && packageJson.version) {
20-
const version = packageJson.version;
21-
const path = `./packages/${pkg.replace('/package.json', '')}`;
22-
map.set(packageJson.name, { version, path });
115+
map.set(packageJson.name, `./packages/${pkg.replace('/package.json', '')}`);
23116
}
24117
}),
25118
);
26119
return map;
27120
}
28121

29-
const releasedPackages = JSON.parse(process.argv[2]);
30-
const prNumber = process.argv[3];
31-
32-
// console.debug({ releasedPackages, prNumber });
33-
34-
const packageMap = await createPackageMap();
35-
const packages = releasedPackages.map(({ name, version }) => {
36-
const pkg = packageMap.get(name);
37-
if (!pkg) {
38-
throw new Error(`Unable to find entrypoint for "${name}"!`);
39-
}
40-
const url = new URL(`${pkg.path}/CHANGELOG.md#${version.replace(/\./g, '')}`, baseUrl).toString();
41-
return { name, version, url };
42-
});
43-
44-
// Slack is using their own Markdown format, see:
45-
// https://api.slack.com/reference/surfaces/formatting#basics
46-
// https://app.slack.com/block-kit-builder
47-
let message = '';
48-
message += `*Javascript SDKs - Stable Release - ${new Date().toLocaleDateString('en-US')}*\n\n`;
49-
for (const { name, version, url } of packages) {
50-
message += `• \`${name}@${version}\` - <${url}|release notes>\n`;
51-
}
52-
53-
// TODO: Get PR number using the GitHub API
54-
// if (prNumber) {
55-
// message += `\nView <https://github.com/clerkinc/javascript/pull/${prNumber}|release PR>`;
56-
// }
57-
58-
message += `\nView <https://github.com/clerkinc/javascript/pulls?q=is%3Apr+is%3Aclosed+Version+Packages+in%3Atitle+merged%3A${new Date()
59-
.toISOString()
60-
.slice(0, 10)}|all release PRs for this day>`;
122+
/**
123+
* @returns {PackageData[]}
124+
*/
125+
const createPackageData = (releasedPackages, packageToPathMap) => {
126+
return releasedPackages.map(({ name, version }) => {
127+
const relativePath = packageToPathMap.get(name);
128+
if (!relativePath) {
129+
throw new Error(`Not found: "${relativePath}"!`);
130+
}
131+
const changelogUrl = new URL(`${relativePath}/CHANGELOG.md#${version.replace(/\./g, '')}`, baseUrl).toString();
132+
return { name, version, changelogUrl };
133+
});
134+
};
61135

62-
console.log(JSON.stringify(message));
136+
/**
137+
* @returns {string}
138+
*/
139+
const createReleasePrUrl = () => {
140+
// TODO: Get PR number using the GitHub API
141+
// if (prNumber) {
142+
// message += `\nView <https://github.com/clerkinc/javascript/pull/${prNumber}|release PR>`;
143+
// }
144+
return `https://github.com/clerkinc/javascript/pulls?q=is%3Apr+is%3Aclosed+Version+Packages+in%3Atitle+merged%3A${new Date()
145+
.toISOString()
146+
.slice(0, 10)}`;
147+
};

0 commit comments

Comments
 (0)