-
Notifications
You must be signed in to change notification settings - Fork 326
/
Copy pathnotify.mjs
183 lines (160 loc) · 5.29 KB
/
notify.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import { fileURLToPath } from 'node:url';
import fs from 'fs-extra';
import { globby as glob } from 'globby';
const { GITHUB_REF = 'main' } = process.env;
const baseUrl = new URL(`https://github.com/clerk/javascript/blob/${GITHUB_REF}/`);
const getReleaseChannel = version => {
if (version?.includes('alpha')) {
return 'Alpha';
} else if (version?.includes('beta')) {
return 'Beta';
} else {
return 'Stable';
}
};
/**
* @typedef {Object} PackageData
* @property {string} name
* @property {string} version
* @property {string} changelogUrl
*/
/**
* @typedef {Object} Pusher
* @property {string} username
* @property {string} avatarUrl
* @property {string} profileUrl
*/
/**
* @typedef {Object} ChangelogData
* @property {PackageData[]} packageData
* @property {string} releasePrUrl
* @property {Pusher} pusher
*/
/**
* @typedef {Object} Formatter
* @property {(data: ChangelogData) => string} generateChangelog
*/
/**
* Slack is using their own Markdown format, see:
* https://api.slack.com/reference/surfaces/formatting#basics
* https://app.slack.com/block-kit-builder
* @type {Formatter}
*/
const slackFormatter = {
generateChangelog: ({ packageData, releasePrUrl, pusher }) => {
const markdown = text => ({ type: 'section', text: { type: 'mrkdwn', text } });
const header = text => ({ type: 'header', text: { type: 'plain_text', text } });
const context = (imgUrl, text) => ({
type: 'context',
elements: [
...(imgUrl ? [{ type: 'image', image_url: imgUrl, alt_text: 'avatar' }] : []),
{ type: 'mrkdwn', text },
],
});
const blocks = [];
const releaseChannel = getReleaseChannel(packageData?.[0]?.version);
blocks.push(header(`Javascript SDKs - ${releaseChannel} Release - ${new Date().toLocaleDateString('en-US')}`));
blocks.push(markdown(`All release PRs for this day can be found <${releasePrUrl}|here>.\nReleased packages:\n`));
createPackagesBody(packageData).forEach(body => {
blocks.push(markdown(body));
});
blocks.push(markdown('\n'));
blocks.push(context(pusher.avatarUrl, `<${pusher.profileUrl}|*${pusher.username}*> triggered this release.`));
return JSON.stringify({ blocks });
},
};
/**
* @property {PackageData[]} packageData
*/
const createPackagesBody = packageData => {
// The Slack API has a limitation of ~3000 characters per block and
// also there is a limit on the number of blocks that can be sent in a single message.
// So, we split the body into fragments of 10 packages each.
const fragments = [];
let body = '';
let count = 0;
for (const { name, version, changelogUrl } of packageData) {
body += `• <${changelogUrl}|Changelog> - \`${name}@${version}\`\n`;
count++;
if (count === 10) {
fragments.push(body);
body = '';
count = 0;
}
}
// This is the remaining
if (body) {
fragments.push(body);
}
return fragments;
};
/**
* @type {Record<string, Formatter>}
*/
const formatters = {
slack: slackFormatter,
};
const run = async () => {
const releasedPackages = JSON.parse(process.argv[2]);
const packageToPathMap = await createPackageToPathMap();
const packageData = createPackageData(releasedPackages, packageToPathMap);
const releasePrUrl = createReleasePrUrl();
const pusher = createPusher(process.argv[3]);
const data = { packageData, releasePrUrl, pusher };
// TODO: Add support for more formatters
const formatter = formatters['slack'];
if (!formatter) {
throw new Error('Invalid formatter, supported formatters are: ' + Object.keys(formatters).join(', '));
}
console.log(formatter.generateChangelog(data));
};
run();
/*
* @returns {Pusher}
*/
const createPusher = username => {
return { username, avatarUrl: `https://github.com/${username}.png`, profileUrl: `https://github.com/${username}` };
};
/**
* @returns {Promise<Map<string,string>>}
*/
async function createPackageToPathMap() {
const map = new Map();
const packagesRoot = new URL('../packages/', import.meta.url);
const packages = await glob(['*/package.json', '*/*/package.json'], { cwd: fileURLToPath(packagesRoot) });
await Promise.all(
packages.map(async pkg => {
const packageJsonPath = fileURLToPath(new URL(pkg, packagesRoot));
const packageJson = fs.readJSONSync(packageJsonPath);
if (!packageJson.private && packageJson.version) {
map.set(packageJson.name, `./packages/${pkg.replace('/package.json', '')}`);
}
}),
);
return map;
}
/**
* @returns {PackageData[]}
*/
const createPackageData = (releasedPackages, packageToPathMap) => {
return releasedPackages.map(({ name, version }) => {
const relativePath = packageToPathMap.get(name);
if (!relativePath) {
throw new Error(`Not found: "${relativePath}"!`);
}
const changelogUrl = new URL(`${relativePath}/CHANGELOG.md#${version.replace(/\./g, '')}`, baseUrl).toString();
return { name, version, changelogUrl };
});
};
/**
* @returns {string}
*/
const createReleasePrUrl = () => {
// TODO: Get PR number using the GitHub API
// if (prNumber) {
// message += `\nView <https://github.com/clerk/javascript/pull/${prNumber}|release PR>`;
// }
return `https://github.com/clerk/javascript/pulls?q=is%3Apr+is%3Aclosed+Version+Packages+in%3Atitle+merged%3A${new Date()
.toISOString()
.slice(0, 10)}`;
};