Skip to content

Commit bbc375f

Browse files
feat: Implement Discord release notifier script
This commit introduces a new Node.js script `post-discord-release.js` that automatically posts release notes to a Discord channel via a webhook. Key features: - Fetches commits between the latest and previous tags. - Parses commit messages based on conventional commit types. - Groups commits into predefined categories (Enhancements, Bug Fixes, Refactors, etc.). - Cleans commit messages by removing conventional commit prefixes. - Converts PR numbers (e.g., #123) into clickable links. - Formats the release notes in Markdown for Discord. - Includes a download link to the GitHub release. - Skips certain commit types (e.g., `chore(changelog)`, `chore(deps)`). Signed-off-by: CreativeCodeCat <wayne6324@gmail.com>
1 parent 6434c45 commit bbc375f

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

post-discord-release.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env node
2+
3+
const https = require("https");
4+
const { execSync } = require("child_process");
5+
6+
const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;
7+
const REPO_URL = "https://github.com/DroidWorksStudio/mLauncher";
8+
9+
// Commit parsing rules
10+
const commitParsers = [
11+
{ message: /^chore\(changelog\):/i, skip: true },
12+
{ message: /^chore\(release\): prepare for/i, skip: true },
13+
{ message: /^chore\(deps.*\)/i, skip: true },
14+
{ message: /^chore\(change.*\)/i, skip: true },
15+
{ message: /^chore\(pr\)/i, skip: true },
16+
{ message: /^chore\(pull\)/i, skip: true },
17+
{ message: /^fixes/i, skip: true },
18+
{ message: /^feat|^perf|^style|^ui|^ux/i, group: "Enhancements" },
19+
{ message: /^fix|^bug|^hotfix|^emergency/i, group: "Bug Fixes" },
20+
{ message: /^refactor/i, group: "Refactors" },
21+
{ message: /^doc|^lang|^i18n/i, group: "Documentation & Language" },
22+
{ message: /^security/i, group: "Security" },
23+
{ message: /^revert/i, group: "Reverts" },
24+
{ message: /^build/i, group: "Build" },
25+
{ message: /^dependency|^deps/i, group: "Dependencies" },
26+
{ message: /^config|^configuration|^ci|^pipeline|^release|^version|^versioning/i, group: "Meta" },
27+
{ message: /^test/i, group: "Tests" },
28+
{ message: /^infra|^infrastructure|^ops/i, group: "Infrastructure & Ops" },
29+
{ message: /^chore|^housekeeping|^cleanup|^clean\(up\)/i, group: "Maintenance & Cleanup" },
30+
{ message: /^drop|^remove/i, group: "Feature Removals" },
31+
];
32+
33+
const GROUP_ORDER = commitParsers.filter((p) => !p.skip).map((p) => p.group);
34+
35+
function run(cmd) {
36+
return execSync(cmd, { encoding: "utf8" }).trim();
37+
}
38+
39+
function cleanMessage(message) {
40+
return message.replace(/^(feat|fix|bug|lang|i18n|doc|perf|refactor|style|ui|ux|security|revert|release|dependency|deps|build|ci|pipeline|chore|housekeeping|version|versioning|config|configuration|cleanup|clean\(up\)|drop|remove|hotfix|emergency|test|infra|infrastructure|ops|asset|content|exp|experiment|prototype)\s*(\(.+?\))?:\s*/i, "");
41+
}
42+
43+
function linkPR(message) {
44+
return message.replace(/\(#(\d+)\)/g, (_, num) => `([#${num}](${REPO_URL}/pull/${num}))`);
45+
}
46+
47+
function classifyCommit(message) {
48+
for (const parser of commitParsers) {
49+
if (parser.message.test(message)) {
50+
if (parser.skip) return null;
51+
return parser.group;
52+
}
53+
}
54+
return null;
55+
}
56+
57+
// Get latest tag
58+
const allTags = run("git tag --sort=-creatordate").split("\n");
59+
const latestTag = allTags[0];
60+
const previousTag = allTags[1];
61+
const range = previousTag ? `${previousTag}..${latestTag}` : latestTag;
62+
63+
// Get commits
64+
const rawCommits = run(`git log ${range} --pretty=format:"%h|%s"`).split("\n");
65+
const commits = rawCommits
66+
.map((line) => {
67+
const [hash, ...msgParts] = line.split("|");
68+
const message = msgParts.join("|").trim();
69+
const group = classifyCommit(message);
70+
if (!group) return null;
71+
return { group, message: linkPR(cleanMessage(message)), hash };
72+
})
73+
.filter(Boolean);
74+
75+
// Group commits
76+
const groups = {};
77+
for (const c of commits) {
78+
groups[c.group] = groups[c.group] || [];
79+
groups[c.group].push(`* ${c.message} ([${c.hash}](${REPO_URL}/commit/${c.hash}))`);
80+
}
81+
82+
// Build plain message
83+
let discordMessage = `## Multi Launcher ${latestTag}\n`;
84+
85+
for (const group of GROUP_ORDER) {
86+
if (!groups[group] || groups[group].length === 0) continue;
87+
discordMessage += `**${group}**\n${groups[group].join("\n")}\n\n`;
88+
}
89+
90+
// Fallback
91+
if (!commits.length) discordMessage += "No commits found.";
92+
93+
// Append download link
94+
discordMessage += `\n[Download Multi Launcher](${REPO_URL}/releases/tag/${latestTag})`;
95+
96+
// Send to Discord
97+
const payload = JSON.stringify({
98+
content: discordMessage,
99+
username: "Multi Launcher Updates!",
100+
avatar_url: "https://github.com/DroidWorksStudio/mLauncher/blob/main/fastlane/metadata/android/en-US/images/icon.png?raw=true",
101+
});
102+
103+
const url = new URL(WEBHOOK_URL);
104+
const options = {
105+
hostname: url.hostname,
106+
path: url.pathname + url.search,
107+
method: "POST",
108+
headers: {
109+
"Content-Type": "application/json",
110+
"Content-Length": payload.length,
111+
},
112+
};
113+
114+
const req = https.request(options, (res) => {
115+
let data = "";
116+
res.on("data", (chunk) => (data += chunk));
117+
res.on("end", () => {
118+
if (res.statusCode >= 200 && res.statusCode < 300) {
119+
console.log("✅ Release posted to Discord!");
120+
} else {
121+
console.error("Failed to post:", res.statusCode, data);
122+
}
123+
});
124+
});
125+
126+
req.on("error", (e) => console.error("Error:", e));
127+
req.write(payload);
128+
req.end();

0 commit comments

Comments
 (0)