Add contributors title and statistics to the changelog script

Puts the contributors under a "Contributors" title on a single comma
separated line, and prints commits, merged pull requests, files changed
and line additions and deletions since the last release after them.

The compare API lists at most 300 files, so the file and line counts
come from the diff media type of the same comparison instead. To fetch
it, githubFetch now lets the caller override the Accept header and
checks the Content-Type against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 5 committed 2026-09-13 16:29:38 +00:00
1 parent a28e2bf9f6
commit 0f4671afd0
2 files changed
+74 -5

No files matched your search

+17 -2
View File
@@ -11,8 +11,8 @@ export async function githubFetch(
init = {
...init,
headers: {
...init.headers as Record<string, string> ?? {},
"Accept": "application/vnd.github+json",
...init.headers as Record<string, string> ?? {},
}
};
if (process.env.GH_TOKEN) {
@@ -24,7 +24,9 @@ export async function githubFetch(
console.log(response.headers);
throw new Error(`GitHub replied with ${response.status} ${response.statusText}: ${await response.text()}`);
}
if (!response.headers.get("Content-Type")?.startsWith("application/json")) {
const accept = init.headers!["Accept"];
const contentType = accept === "application/vnd.github+json" ? "application/json" : accept;
if (!response.headers.get("Content-Type")?.startsWith(contentType)) {
throw new Error(
`GitHub replied with ${response.status} ${response.statusText}: ${response.headers.get("Content-Type")}`
);
@@ -41,6 +43,15 @@ export async function githubFetchJson<T>(
return await response.json();
}
export async function githubFetchText(
path: string,
query: Record<string, string> = {},
init: RequestInit & { headers?: Record<string, string> } = {},
): Promise<string> {
const response = await githubFetch(path, query, init);
return await response.text();
}
export async function githubFetchJsonPaginated<T>(
path: string,
query: Record<string, string> = {},
@@ -117,6 +128,10 @@ export interface Release {
updated_at?: string | null,
}
export interface Comparison {
total_commits: number,
}
export interface Commit {
sha: string,
commit: {
+57 -3
View File
@@ -1,12 +1,22 @@
import {
type Comparison,
type Release,
type Issue,
githubFetchJson,
githubFetchJsonPaginated,
githubFetchText,
} from "./github_api.mts";
type Changelog = Record<string, string[]>;
type Statistics = {
commits: number,
pullRequests: number,
filesChanged: number,
lineAdditions: number,
lineDeletions: number,
};
const repository = "clusterio/clusterio";
const branch = "master";
@@ -187,10 +197,41 @@ async function fetchIssuesUpdatedSince(since: string) {
);
}
async function fetchStatistics(lastRelease: Release, pullRequests: Issue[]): Promise<Statistics> {
const path = `/repos/${repository}/compare/${lastRelease.tag_name}...${branch}`;
const comparison = await githubFetchJson<Comparison>(path, { per_page: "1" });
// The JSON response lists at most 300 files, the diff has all of them.
const diff = await githubFetchText(path, {}, { headers: { "Accept": "application/vnd.github.diff" } });
let filesChanged = 0;
let lineAdditions = 0;
let lineDeletions = 0;
let inHunk = false; // Skips the ---/+++ file headers
for (const line of diff.split("\n")) {
if (line.startsWith("diff --git ")) {
filesChanged += 1;
inHunk = false;
} else if (line.startsWith("@@")) {
inHunk = true;
} else if (inHunk && line.startsWith("+")) {
lineAdditions += 1;
} else if (inHunk && line.startsWith("-")) {
lineDeletions += 1;
}
}
return {
commits: comparison.total_commits,
pullRequests: pullRequests.length,
filesChanged,
lineAdditions,
lineDeletions,
};
}
function printMarkdown(
changelog: Changelog,
issues: Issue[],
users: Issue["user"][],
statistics: Statistics,
refText: (issue: Issue) => string,
userText: (user: Issue["user"]) => string,
) {
@@ -216,8 +257,20 @@ function printMarkdown(
}
}
console.log("Contributors");
console.log();
console.log(`Many thanks to the following for contributing to this release:`);
console.log(users.map(userText).join("\n"))
console.log(users.map(userText).join(", "));
const format = (value: number) => value.toLocaleString("en-US");
console.log();
console.log("Statistics");
console.log();
console.log(`Commits: ${format(statistics.commits)}`);
console.log(`Pull Requests: ${format(statistics.pullRequests)}`);
console.log(`Files Changed: ${format(statistics.filesChanged)}`);
console.log(`Line Additions: ${format(statistics.lineAdditions)}`);
console.log(`Line Deletions: ${format(statistics.lineDeletions)}`);
}
async function main() {
@@ -232,17 +285,18 @@ async function main() {
const changelog = changelogFromPullRequests(pullRequests, issues);
const users = [...new Map(pullRequests.map(issue => [issue.user.login, issue.user])).values()]
const statistics = await fetchStatistics(lastRelease, pullRequests);
console.log();
console.log("=== Github Release Markdown ===");
printMarkdown(changelog, issues, users,
printMarkdown(changelog, issues, users, statistics,
issue => `${repository}#${issue.number}`,
user => `@${user.login}`,
);
console.log();
console.log("=== Discord / Changelog Markdown ===");
printMarkdown(changelog, issues, users,
printMarkdown(changelog, issues, users, statistics,
issue => `[#${issue.number}](<${issue.html_url}>)`,
user => `[@${user.login}](<${user.html_url}>)`
);