Skip to content

Commit 45d286f

Browse files
add: enhance nightly build workflow by adding a step to generate release notes (#1747)
* feat: Add translation check workflow for pull requests * Introduced a new GitHub Actions workflow to check for changes in translation files & a new workflow for labeling based upon the changed files on pull requests. * The workflow detects changes in JSON files located in the src/lang directory and runs a lint check if any changes are found. * Added a Workflow to label Pull requests based on their changed files. * update: enhance nightly build workflow by adding a step to generate release notes and updating conditions for releasing nightly versions. The new step checks for the existence of a nightly tag and captures release notes if required. * feat: enhance release notification workflow by adding a required release body input and integrating it into the Discord webhook message. Introduced a new script for generating release notes based on commit history. * fix: multi-lines for the Release notes CI * chore: fmt
1 parent d5b2ec9 commit 45d286f

File tree

3 files changed

+269
-1
lines changed

3 files changed

+269
-1
lines changed

.github/workflows/community-release-notifier.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ on:
1212
required: true
1313
description: "release URL"
1414
type: 'string'
15+
body:
16+
required: true
17+
description: "Release Body"
18+
type: 'string'
19+
default: ''
1520
secrets:
1621
DISCORD_WEBHOOK_RELEASE_NOTES:
1722
description: 'Discord Webhook for Notifying Releases to Discord'
@@ -30,6 +35,7 @@ jobs:
3035
stringToTruncate: |
3136
📢 Acode [${{ github.event.release.tag_name || inputs.tag_name }}](<${{ github.event.release.url || inputs.url }}>) was just Released 🎉!
3237
38+
${{ github.event.release.body || inputs.body }}
3339
3440
- name: Discord Webhook Action (Publishing)
3541
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0

.github/workflows/nightly-build.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
outputs:
6666
release_output_url: ${{ steps.release.outputs.url }}
6767
updated_version: ${{ steps.update-version.outputs.UPDATED_VERSION}}
68+
RELEASE_NOTES: ${{ env.RELEASE_NOTES }}
6869
steps:
6970
- name: Fast Fail if secrets are missing
7071
if: ${{ env.KEYSTORE_CONTENT == '' || env.BUILD_JSON_CONTENT == '' }}
@@ -201,6 +202,7 @@
201202
- name: Check Nightly Tag and Force Update
202203
#if: github.event_name == 'push' && contains(github.event.ref, 'tags/nightly') == false
203204
if: ${{ ! inputs.skip_tagging_and_releases }}
205+
id: check-nightly-tag-force-update
204206
run: |
205207
# Check if the nightly tag exists and get the commit it points to
206208
if git show-ref --quiet refs/tags/nightly; then
@@ -223,10 +225,22 @@
223225
echo "Nightly tag already points to this commit. Skipping update."
224226
fi
225227
228+
229+
- name: Generate Release Notes (Experimental)
230+
if: ${{ success() && env.releaseRequired == 'true' }}
231+
id: gen-release-notes
232+
continue-on-error: true
233+
run: |
234+
RELEASE_NOTES=$(node utils/scripts/generate-release-notes.js ${{ github.repository_owner }} Acode ${{ github.sha }} --format md --from-tag ${{ env.TAG_COMMIT }} --important-only --quiet --changelog-only)
235+
{
236+
echo "RELEASE_NOTES<<EOF"
237+
echo "$RELEASE_NOTES"
238+
echo "EOF"
239+
} >> $GITHUB_ENV
226240
- name: Release Nightly Version
227241
# Only run this step, if not called from another workflow. And a previous step is successful with releasedRequired=true
228242
id: release
229-
if: ${{ ! inputs.skip_tagging_and_releases && success() && env.releaseRequired == 'true' && !inputs.is_PR }}
243+
if: ${{ ! inputs.skip_tagging_and_releases && steps.check-nightly-tag-force-update.outcome == 'success' && env.releaseRequired == 'true' && !inputs.is_PR }}
230244
uses: softprops/action-gh-release@v2
231245
with:
232246
prerelease: true
@@ -240,6 +254,8 @@
240254
241255
[Compare Changes](https://github.com/${{ github.repository }}/compare/${{ env.TAG_COMMIT }}...${{ github.sha }})
242256
257+
${{ env.RELEASE_NOTES }}
258+
243259
- name: Update Last Comment by bot (If ran in PR)
244260
if: inputs.is_PR
245261
uses: marocchino/sticky-pull-request-comment@v2
@@ -261,5 +277,6 @@
261277
with:
262278
tag_name: ${{ needs.build.outputs.updated_version }}
263279
url: ${{ needs.build.outputs.release_output_url }}
280+
body: ${{ needs.build.outputs.RELEASE_NOTES }}
264281
secrets:
265282
DISCORD_WEBHOOK_RELEASE_NOTES: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#!/usr/bin/env node
2+
/**
3+
* ✨ @ UnschooledGamer (baked With AI, Modified by @ UnschooledGamer) ~ 2025.
4+
*
5+
* GitHub Release Notes Generator
6+
*
7+
* Features:
8+
* - Auto categorizes commits by type
9+
* - Optional compact "plain" output to save space
10+
* - Option to include only important tags (feat, fix, refactor, perf)
11+
* - Option to use only merge commits
12+
*
13+
* Usage:
14+
* GITHUB_TOKEN=<token> node generate-release-notes.js <owner> <repo> <current_tag> [options]
15+
*
16+
* Options:
17+
* --plain Output minimal Markdown (no emojis, compact)
18+
* --important-only Include only features, fixes, refactors, and perf
19+
* --merge-only Include only merge commits
20+
* --help Show usage
21+
* --format [md/json] Output Format
22+
* --fromTag v1.11.0 The From/Previous Tag
23+
* --quiet Suppress output to stdout
24+
*/
25+
26+
const args = process.argv.slice(2);
27+
28+
function getArgValue(flag) {
29+
const idx = args.indexOf(flag);
30+
return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-")
31+
? args[idx + 1]
32+
: null;
33+
}
34+
if (args.includes("--help") || args.length < 3) {
35+
console.log(`
36+
Usage: GITHUB_TOKEN=<token> node generate-release-notes.js <owner> <repo> <tag> [options]
37+
✨ @ UnschooledGamer (baked With AI, Modified by @ UnschooledGamer) ~ 2025
38+
39+
Options:
40+
--plain Compact, no emojis (saves space)
41+
--important-only Only include Features, Fixes, Refactors, Perf
42+
--merge-only Include only merge commits
43+
--help Show this help message
44+
--format [md/json] Output Format
45+
--from-tag v1.11.0 The From/Previous Tag
46+
--quiet Suppress output to stdout
47+
--stdout-only Output to stdout only
48+
--changelog-only Output changelog only
49+
`);
50+
process.exit(0);
51+
}
52+
53+
const [owner, repo, currentTag, previousTagArg] = args;
54+
const token = process.env.GITHUB_TOKEN;
55+
if (!token) {
56+
console.error("❌ Missing GITHUB_TOKEN environment variable.");
57+
process.exit(1);
58+
}
59+
60+
const flags = {
61+
plain: args.includes("--plain"),
62+
importantOnly: args.includes("--important-only"),
63+
mergeOnly: args.includes("--merge-only"),
64+
quiet: args.includes("--quiet") || args.includes("--stdout-only"),
65+
format: getArgValue("--format") || "md",
66+
fromTag: getArgValue("--from-tag"),
67+
changelogOnly: args.includes("--changelog-only"),
68+
};
69+
70+
function log(...msg) {
71+
if (!flags.quiet) console.error(...msg);
72+
}
73+
74+
const headers = {
75+
Authorization: `token ${token}`,
76+
Accept: "application/vnd.github+json",
77+
"User-Agent": "release-notes-script",
78+
};
79+
80+
async function getPreviousTag() {
81+
const res = await fetch(
82+
`https://api.github.com/repos/${owner}/${repo}/tags`,
83+
{ headers },
84+
);
85+
const tags = await res.json();
86+
if (!Array.isArray(tags) || tags.length < 2) return null;
87+
return tags[1].name;
88+
}
89+
90+
async function getCommits(previousTag, currentTag) {
91+
const url = `https://api.github.com/repos/${owner}/${repo}/compare/${previousTag}...${currentTag}`;
92+
const res = await fetch(url, { headers });
93+
if (!res.ok) throw new Error(`Failed to fetch commits: ${res.status}`);
94+
const data = await res.json();
95+
return data.commits || [];
96+
}
97+
98+
function categorizeCommits(commits, { mergeOnly, importantOnly }) {
99+
const sections = {
100+
feat: [],
101+
fix: [],
102+
perf: [],
103+
refactor: [],
104+
docs: [],
105+
chore: [],
106+
test: [],
107+
add: [],
108+
revert: [],
109+
update: [],
110+
other: [],
111+
};
112+
113+
for (const c of commits) {
114+
const msg = c.commit.message.split("\n")[0];
115+
const isMerge =
116+
msg.startsWith("Merge pull request") || msg.startsWith("Merge branch");
117+
118+
if (mergeOnly && !isMerge) continue;
119+
120+
const type =
121+
Object.keys(sections).find(
122+
(k) =>
123+
msg.toLowerCase().startsWith(`${k}:`) ||
124+
msg.toLowerCase().startsWith(`${k} `),
125+
) || "other";
126+
127+
if (
128+
importantOnly &&
129+
!["feat", "fix", "refactor", "perf", "add", "revert", "update"].includes(
130+
type,
131+
)
132+
)
133+
continue;
134+
135+
const author = c.author?.login
136+
? `[${c.author.login}](https://github.com/${c.author.login})`
137+
: "unknown";
138+
139+
const entry = `- ${msg} (${c.sha.slice(0, 7)}) by ${author}`;
140+
sections[type].push(entry);
141+
}
142+
143+
return sections;
144+
}
145+
146+
const emojis = {
147+
feat: flags.plain ? "" : "✨ ",
148+
fix: flags.plain ? "" : "🐞 ",
149+
perf: flags.plain ? "" : "⚡ ",
150+
refactor: flags.plain ? "" : "🔧 ",
151+
docs: flags.plain ? "" : "📝 ",
152+
chore: flags.plain ? "" : "🧹 ",
153+
test: flags.plain ? "" : "🧪 ",
154+
other: flags.plain ? "" : "📦 ",
155+
revert: flags.plain ? "" : "⏪ ",
156+
add: flags.plain ? "" : "➕ ",
157+
update: flags.plain ? "" : "🔄 ",
158+
};
159+
160+
function formatMarkdown(tag, prevTag, sections, { plain }) {
161+
const lines = [
162+
flags.changelogOnly
163+
? ""
164+
: `Changes since [${prevTag}](https://github.com/${owner}/${repo}/releases/tag/${prevTag})`,
165+
"",
166+
];
167+
168+
for (const [type, list] of Object.entries(sections)) {
169+
if (list.length === 0) continue;
170+
const header = plain
171+
? `## ${type}`
172+
: `## ${emojis[type]}${type[0].toUpperCase() + type.slice(1)}`;
173+
lines.push(header, "", list.join("\n"), "");
174+
}
175+
176+
// Compact single-line mode for super small output
177+
// if (plain) {
178+
// const compact = Object.entries(sections)
179+
// .filter(([_, list]) => list.length)
180+
// .map(([type, list]) => `${type.toUpperCase()}: ${list.length} commits`)
181+
// .join(" | ");
182+
// lines.push(`\n_Summary: ${compact}_`);
183+
// }
184+
185+
return lines.join("\n");
186+
}
187+
188+
function formatJSON(tag, prevTag, sections, plain = true) {
189+
const lines = [
190+
"",
191+
flags.changelogOnly
192+
? ""
193+
: `Changes since [${prevTag}](https://github.com/${owner}/${repo}/releases/tag/${prevTag})`,
194+
"",
195+
];
196+
197+
// todo: split into function
198+
for (const [type, list] of Object.entries(sections)) {
199+
if (list.length === 0) continue;
200+
const header = plain
201+
? `## ${type}`
202+
: `## ${emojis[type]}${type[0].toUpperCase() + type.slice(1)}`;
203+
lines.push(header, "", list.join("\n"), "");
204+
}
205+
return JSON.stringify(
206+
{
207+
release: tag,
208+
previous: prevTag,
209+
sections: Object.fromEntries(
210+
Object.entries(sections).filter(([_, v]) => v.length),
211+
),
212+
notes: lines.join("\n"),
213+
},
214+
null,
215+
2,
216+
);
217+
}
218+
219+
async function main() {
220+
log(`🔍 Generating release notes for ${owner}/${repo} @ ${currentTag}...`);
221+
222+
const prevTag = flags.fromTag || (await getPreviousTag());
223+
if (!prevTag) {
224+
console.error("No previous tag found. Use --from-tag to specify one.");
225+
process.exit(1);
226+
}
227+
228+
const commits = await getCommits(prevTag, currentTag);
229+
if (!commits.length) {
230+
console.error("No commits found.");
231+
process.exit(1);
232+
}
233+
const categorized = categorizeCommits(commits, flags);
234+
let output;
235+
236+
if (flags.format === "json") {
237+
output = formatJSON(currentTag, prevTag, categorized);
238+
} else {
239+
output = formatMarkdown(currentTag, prevTag, categorized, flags);
240+
}
241+
242+
process.stdout.write(output + "\n");
243+
}
244+
245+
main().catch((err) => console.error(err));

0 commit comments

Comments
 (0)