Files
factorio-clusterio/scripts/github_api.mts
T
bbassieandClaude Opus 5 0f4671afd0 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>
2026-09-13 16:29:38 +00:00

157 lines
3.7 KiB
TypeScript

export async function githubFetch(
path: string,
query: Record<string, string>= {},
init: RequestInit & { headers?: Record<string, string> } = {},
) {
const url = new URL("https://api.github.com/");
url.pathname = path;
for (const [name, value] of Object.entries(query)) {
url.searchParams.set(name, value);
}
init = {
...init,
headers: {
"Accept": "application/vnd.github+json",
...init.headers as Record<string, string> ?? {},
}
};
if (process.env.GH_TOKEN) {
init.headers!["Authorization"] = `Bearer ${process.env.GH_TOKEN}`;
}
console.log("Fetching", url.href);
const response = await fetch(url, init);
if (!response.ok) {
console.log(response.headers);
throw new Error(`GitHub replied with ${response.status} ${response.statusText}: ${await response.text()}`);
}
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")}`
);
}
return response;
}
export async function githubFetchJson<T>(
path: string,
query: Record<string, string> = {},
init: RequestInit & { headers?: Record<string, string> } = {},
): Promise<T> {
const response = await githubFetch(path, query, init);
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> = {},
init: RequestInit & { headers?: Record<string, string> } = {},
shouldContinue: (result: T[]) => boolean = () => true,
): Promise<T[]> {
const data: T[] = [];
let pagesRemaining = true;
query = {
...query,
per_page: "100",
};
while (pagesRemaining) {
const response = await githubFetch(path, query, init);
const responseItems = await response.json();
data.push(...responseItems);
const linkHeader = response.headers.get("Link");
pagesRemaining = linkHeader?.includes(`rel="next"`) ?? false;
if (pagesRemaining) {
pagesRemaining = shouldContinue(responseItems);
}
if (pagesRemaining) {
query = {
...query,
page: String((query.page ? (Number(query.page)) : 1) + 1),
}
}
}
return data;
}
// Minimal types that include only what is used and useful for our scripts. See
// https://docs.github.com/en/rest for complete reference.
export interface PullRequest {
number: number,
url: string,
html_url: string,
state: string,
title: string,
body: string | null,
created_at: string,
updated_at: string,
closed_at: string | null,
merged_at: string | null,
merge_commit_sha: string | null,
}
export interface Issue {
number: number,
title: string,
url: string,
html_url: string,
state: string,
body?: string | null,
user: {
login: string,
url: string,
html_url: string,
}
pull_request?: {
url: string,
html_url: string,
merged_at?: string | null,
},
}
export interface Release {
tag_name: string,
target_commitish: string,
name: string | null,
body: string | null,
created_at: string,
published_at: string | null,
updated_at?: string | null,
}
export interface Comparison {
total_commits: number,
}
export interface Commit {
sha: string,
commit: {
message: string,
},
author: null | {} | {
name?: null | string,
login: string,
type: string,
},
parents: {
sha: string,
}[],
}
export interface Reference {
ref: string,
object: {
type: string,
sha: string,
},
}