Skip to content

Commit 10ced25

Browse files
chore: fmt
1 parent e77cfe9 commit 10ced25

1 file changed

Lines changed: 165 additions & 139 deletions

File tree

Lines changed: 165 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
22
/**
33
* ✨ @ UnschooledGamer (baked With AI, Modified by @ UnschooledGamer) ~ 2025.
4-
*
4+
*
55
* GitHub Release Notes Generator
66
*
77
* Features:
@@ -26,11 +26,13 @@
2626
const args = process.argv.slice(2);
2727

2828
function getArgValue(flag) {
29-
const idx = args.indexOf(flag);
30-
return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-") ? args[idx + 1] : null;
29+
const idx = args.indexOf(flag);
30+
return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-")
31+
? args[idx + 1]
32+
: null;
3133
}
3234
if (args.includes("--help") || args.length < 3) {
33-
console.log(`
35+
console.log(`
3436
Usage: GITHUB_TOKEN=<token> node generate-release-notes.js <owner> <repo> <tag> [options]
3537
✨ @ UnschooledGamer (baked With AI, Modified by @ UnschooledGamer) ~ 2025
3638
@@ -45,175 +47,199 @@ Options:
4547
--stdout-only Output to stdout only
4648
--changelog-only Output changelog only
4749
`);
48-
process.exit(0);
50+
process.exit(0);
4951
}
5052

5153
const [owner, repo, currentTag, previousTagArg] = args;
5254
const token = process.env.GITHUB_TOKEN;
5355
if (!token) {
54-
console.error("❌ Missing GITHUB_TOKEN environment variable.");
55-
process.exit(1);
56+
console.error("❌ Missing GITHUB_TOKEN environment variable.");
57+
process.exit(1);
5658
}
5759

5860
const flags = {
59-
plain: args.includes("--plain"),
60-
importantOnly: args.includes("--important-only"),
61-
mergeOnly: args.includes("--merge-only"),
62-
quiet: args.includes("--quiet") || args.includes("--stdout-only"),
63-
format: getArgValue("--format") || "md",
64-
fromTag: getArgValue("--from-tag"),
65-
changelogOnly: args.includes("--changelog-only"),
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"),
6668
};
6769

6870
function log(...msg) {
69-
if (!flags.quiet) console.error(...msg);
71+
if (!flags.quiet) console.error(...msg);
7072
}
7173

7274
const headers = {
73-
"Authorization": `token ${token}`,
74-
"Accept": "application/vnd.github+json",
75-
"User-Agent": "release-notes-script"
75+
Authorization: `token ${token}`,
76+
Accept: "application/vnd.github+json",
77+
"User-Agent": "release-notes-script",
7678
};
7779

7880
async function getPreviousTag() {
79-
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/tags`, { headers });
80-
const tags = await res.json();
81-
if (!Array.isArray(tags) || tags.length < 2) return null;
82-
return tags[1].name;
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;
8388
}
8489

8590
async function getCommits(previousTag, currentTag) {
86-
const url = `https://api.github.com/repos/${owner}/${repo}/compare/${previousTag}...${currentTag}`;
87-
const res = await fetch(url, { headers });
88-
if (!res.ok) throw new Error(`Failed to fetch commits: ${res.status}`);
89-
const data = await res.json();
90-
return data.commits || [];
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 || [];
9196
}
9297

9398
function categorizeCommits(commits, { mergeOnly, importantOnly }) {
94-
const sections = {
95-
feat: [],
96-
fix: [],
97-
perf: [],
98-
refactor: [],
99-
docs: [],
100-
chore: [],
101-
test: [],
102-
add: [],
103-
revert: [],
104-
update: [],
105-
other: [],
106-
};
107-
108-
for (const c of commits) {
109-
const msg = c.commit.message.split("\n")[0];
110-
const isMerge = msg.startsWith("Merge pull request") || msg.startsWith("Merge branch");
111-
112-
if (mergeOnly && !isMerge) continue;
113-
114-
const type = Object.keys(sections).find(k => msg.toLowerCase().startsWith(`${k}:`) || msg.toLowerCase().startsWith(`${k} `)) || "other";
115-
116-
if (importantOnly && !["feat", "fix", "refactor", "perf", "add", "revert", "update"].includes(type)) continue;
117-
118-
const author = c.author?.login
119-
? `[${c.author.login}](https://github.com/${c.author.login})`
120-
: "unknown";
121-
122-
const entry = `- ${msg} (${c.sha.slice(0, 7)}) by ${author}`;
123-
sections[type].push(entry);
124-
}
125-
126-
return sections;
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;
127144
}
128145

129146
const emojis = {
130-
feat: flags.plain ? "" : "✨ ",
131-
fix: flags.plain ? "" : "🐞 ",
132-
perf: flags.plain ? "" : "⚡ ",
133-
refactor: flags.plain ? "" : "🔧 ",
134-
docs: flags.plain ? "" : "📝 ",
135-
chore: flags.plain ? "" : "🧹 ",
136-
test: flags.plain ? "" : "🧪 ",
137-
other: flags.plain ? "" : "📦 ",
138-
revert: flags.plain ? "" : "⏪ ",
139-
add: flags.plain ? "" : "➕ ",
140-
update: flags.plain ? "" : "🔄 ",
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 ? "" : "🔄 ",
141158
};
142159

143160
function formatMarkdown(tag, prevTag, sections, { plain }) {
144-
145-
const lines = [
146-
flags.changelogOnly ? "" : `Changes since [${prevTag}](https://github.com/${owner}/${repo}/releases/tag/${prevTag})`,
147-
"",
148-
];
149-
150-
for (const [type, list] of Object.entries(sections)) {
151-
if (list.length === 0) continue;
152-
const header = plain ? `## ${type}` : `## ${emojis[type]}${type[0].toUpperCase() + type.slice(1)}`;
153-
lines.push(header, "", list.join("\n"), "");
154-
}
155-
156-
// Compact single-line mode for super small output
157-
// if (plain) {
158-
// const compact = Object.entries(sections)
159-
// .filter(([_, list]) => list.length)
160-
// .map(([type, list]) => `${type.toUpperCase()}: ${list.length} commits`)
161-
// .join(" | ");
162-
// lines.push(`\n_Summary: ${compact}_`);
163-
// }
164-
165-
return lines.join("\n");
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");
166186
}
167187

168188
function formatJSON(tag, prevTag, sections, plain = true) {
169-
170-
const lines = [
171-
"",
172-
flags.changelogOnly ? "" : `Changes since [${prevTag}](https://github.com/${owner}/${repo}/releases/tag/${prevTag})`,
173-
"",
174-
];
175-
176-
// todo: split into function
177-
for (const [type, list] of Object.entries(sections)) {
178-
if (list.length === 0) continue;
179-
const header = plain ? `## ${type}` : `## ${emojis[type]}${type[0].toUpperCase() + type.slice(1)}`;
180-
lines.push(header, "", list.join("\n"), "");
181-
}
182-
return JSON.stringify({
183-
release: tag,
184-
previous: prevTag,
185-
sections: Object.fromEntries(
186-
Object.entries(sections).filter(([_, v]) => v.length)
187-
),
188-
notes: lines.join("\n")
189-
}, null, 2);
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+
);
190217
}
191218

192-
193219
async function main() {
194-
log(`🔍 Generating release notes for ${owner}/${repo} @ ${currentTag}...`);
195-
196-
const prevTag = flags.fromTag || await getPreviousTag();
197-
if (!prevTag) {
198-
console.error("No previous tag found. Use --from-tag to specify one.");
199-
process.exit(1);
200-
}
201-
202-
const commits = await getCommits(prevTag, currentTag);
203-
if(!commits.length) {
204-
console.error("No commits found.");
205-
process.exit(1);
206-
}
207-
const categorized = categorizeCommits(commits, flags);
208-
let output;
209-
210-
if (flags.format === "json") {
211-
output = formatJSON(currentTag, prevTag, categorized);
212-
} else {
213-
output = formatMarkdown(currentTag, prevTag, categorized, flags);
214-
}
215-
216-
process.stdout.write(output + "\n");
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");
217243
}
218244

219-
main().catch(err => console.error(err));
245+
main().catch((err) => console.error(err));

0 commit comments

Comments
 (0)