Skip to content

Commit eb7e23a

Browse files
authored
doc: Changelogs (#611)
Automatically generates changelogs based on git history
1 parent b1d8756 commit eb7e23a

9 files changed

Lines changed: 164 additions & 6 deletions

File tree

.github/workflows/check-uids.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout
16-
uses: actions/checkout@v2
16+
uses: actions/checkout@v4
1717
- name: Setup Godot
1818
uses: chickensoft-games/setup-godot@v1
1919
with:

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout
16-
uses: actions/checkout@v2
16+
uses: actions/checkout@v4
1717
- name: Lint
1818
run: sh/lint.sh
1919
test:

.github/workflows/publish-site.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ jobs:
1818
runs-on: ubuntu-latest
1919
steps:
2020
- name: Checkout
21-
uses: actions/checkout@v2
21+
uses: actions/checkout@v6
22+
with:
23+
fetch-depth: 0
2224
- name: Setup Pages
2325
uses: actions/configure-pages@v3
2426
- name: Setup Python
@@ -47,5 +49,7 @@ jobs:
4749
run: sh/setup-plantuml.sh
4850
- name: Generate API docs
4951
run: sh/apidocs.sh
52+
- name: Generate changelog
53+
run: sh/changelog.ts > docs/changelog.md
5054
- name: Build
5155
run: PLANTUML_URL=http://localhost:8080/ sh/push-docs.sh

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: Checkout
18-
uses: actions/checkout@v2
18+
uses: actions/checkout@v4
1919
- name: Setup Godot
2020
uses: chickensoft-games/setup-godot@v1
2121
with:

.github/workflows/validate-site.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout
16-
uses: actions/checkout@v2
16+
uses: actions/checkout@v6
17+
with:
18+
fetch-depth: 0
19+
- name: Validate checkout
20+
run: git branch -a && git tag && git log --format=oneline
1721
- name: Setup Python
1822
uses: actions/setup-python@v5
1923
- name: Setup Bun
@@ -41,6 +45,8 @@ jobs:
4145
run: sh/setup-plantuml.sh
4246
- name: Generate API docs
4347
run: sh/apidocs.sh
48+
- name: Generate changelog
49+
run: sh/changelog.ts > docs/changelog.md
4450
- name: Build
4551
run: PLANTUML_URL=http://localhost:8080/ NOPUSH=yes sh/push-docs.sh
4652

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ docs/class-reference/*
3232
apidocs/
3333
classdb.json
3434

35+
# Changelog - generated by sh/changelog.ts
36+
docs/changelog.md
37+
3538
# Only for local use
3639
sh/ensure-uids.sh
3740

docs/.gdignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ markdown_extensions:
3030
nav:
3131
- 'index.md'
3232
- 'upgrading.md'
33+
- 'changelog.md'
3334
- 'made-with-netfox.md'
3435
- 'faq.md'
3536
- 'external-tutorials.md'

sh/changelog.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env bun
2+
3+
import { $ } from "bun";
4+
5+
const ERR_NO_TAGS = 1;
6+
7+
const REPO_LINK = "https://github.com/foxssake/netfox";
8+
const LATEST_REF = "HEAD";
9+
10+
interface Semver {
11+
prefix: string | undefined;
12+
major: number;
13+
minor: number;
14+
patch: number;
15+
suffix: string | undefined;
16+
}
17+
18+
interface Commit {
19+
hash: string;
20+
description: string;
21+
}
22+
23+
function parseSemver(version: string): Semver | undefined {
24+
const pattern = /(v?)(\d+)\.(\d+)\.(\d+)([\.-].*)?/;
25+
const matches = pattern.exec(version);
26+
if (matches === null) return undefined;
27+
28+
const result = {
29+
prefix: matches[1],
30+
major: parseInt(matches[2]),
31+
minor: parseInt(matches[3]),
32+
patch: parseInt(matches[4]),
33+
suffix: matches[5],
34+
} as Semver;
35+
36+
if (result.suffix === "") result.suffix = undefined;
37+
38+
return result;
39+
}
40+
41+
function compareSemver(a: Semver, b: Semver): number {
42+
if (a.major != b.major) return a.major - b.major;
43+
if (a.minor != b.minor) return a.minor - b.minor;
44+
if (a.patch != b.patch) return a.patch - b.patch;
45+
if (a.suffix != b.suffix) {
46+
if (a.suffix === undefined) return 1; // a has no suffix, therefore it's more mature
47+
if (b.suffix === undefined) return -1; // b has no suffix, therefore it's more mature
48+
if (a.suffix !== undefined && b.suffix !== undefined)
49+
return a.suffix.localeCompare(b.suffix);
50+
}
51+
return 0;
52+
}
53+
54+
function stringifySemver(version: Semver): string {
55+
return `${version.prefix ?? ""}${version.major}.${version.minor}.${version.patch}${version.suffix ?? ""}`;
56+
}
57+
58+
function parseOnelineCommit(line: string): Commit | undefined {
59+
const pattern = /([\w\d]+) (.+)/;
60+
const matches = pattern.exec(line);
61+
if (matches === null) return undefined;
62+
63+
return {
64+
hash: matches[1],
65+
description: matches[2],
66+
};
67+
}
68+
69+
function renderCommitDescription(description: string): string {
70+
const pattern = /(.*)\(#([\d]+)\)$/;
71+
const matches = pattern.exec(description);
72+
if (matches === null) return description;
73+
74+
const body = matches[1];
75+
const pullId = matches[2];
76+
const link = `${REPO_LINK}/pull/${pullId}`;
77+
return `${body}([#${pullId}](${link}))`;
78+
}
79+
80+
function renderCommit(commit: Commit): string {
81+
const link = `${REPO_LINK}/commit/${commit.hash}`;
82+
const body = renderCommitDescription(commit.description);
83+
return `${body} [🔗](${link})`;
84+
}
85+
86+
function renderRelease(tag: string, commits: Commit[]): string {
87+
const title = tag === LATEST_REF ? "Latest" : tag;
88+
const body = commits.map((it) => "* " + renderCommit(it)).join("\n");
89+
return `## ${title}\n${body}`;
90+
}
91+
92+
async function main(): Promise<number> {
93+
const versions = (await Array.fromAsync($`git tag`.quiet().lines()))
94+
.filter((it) => it.startsWith("v"))
95+
.map((it) => parseSemver(it))
96+
.filter((it) => it !== undefined)
97+
.sort(compareSemver)
98+
.reverse()
99+
.map(stringifySemver);
100+
101+
if (versions.length == 0) {
102+
console.error("No version tags found!");
103+
return ERR_NO_TAGS;
104+
}
105+
106+
versions.unshift(LATEST_REF);
107+
108+
const logIntervals = versions.map((it, idx) => [
109+
it,
110+
versions.at(idx + 1) ?? "",
111+
]);
112+
113+
const logs = await Promise.all(
114+
logIntervals
115+
.map(([from, to]) =>
116+
to !== ""
117+
? $`git log --format=oneline ${to}..${from}`.quiet().lines()
118+
: $`git log --format=oneline ${from}`.quiet().lines(),
119+
)
120+
.map((it) => Array.fromAsync(it))
121+
.map((it) =>
122+
it.then((lines) =>
123+
lines
124+
.map(parseOnelineCommit)
125+
.filter((commit) => commit !== undefined),
126+
),
127+
),
128+
);
129+
130+
const releaseNotes = versions.map((version, idx) => [
131+
version,
132+
logs.at(idx) ?? [],
133+
]) as [string, Commit[]][];
134+
135+
console.log("# Changelog\n");
136+
console.log(
137+
releaseNotes
138+
.map(([version, commits]) => renderRelease(version, commits))
139+
.join("\n\n"),
140+
);
141+
142+
return 0;
143+
}
144+
145+
process.exit(await main());

0 commit comments

Comments
 (0)