Skip to content

Commit a87a3d9

Browse files
author
Guy Bedford
authored
actions: automatically reformat changelog (#789)
1 parent c755bde commit a87a3d9

File tree

5 files changed

+2534
-2145
lines changed

5 files changed

+2534
-2145
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
with:
2626
cache: 'yarn'
2727
- run: yarn install --immutable
28-
- run: npm run check-changelog
28+
- run: npm run format-changelog
2929

3030
check-docusaurus:
3131
if: github.ref != 'refs/heads/main'

.github/workflows/release-please.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ jobs:
5454
- run: npm run build:files
5555
working-directory: ./documentation/app
5656

57+
- run: npm run format-changelog
58+
5759
- name: Committing and push changes
5860
run: |
5961
git config user.name "${GITHUB_ACTOR}"

ci/format-changelog.js

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#!/usr/bin/env node
2+
3+
import { readFileSync, writeFileSync } from "node:fs";
4+
import path from "node:path";
5+
import process from "node:process";
6+
import { unified } from "unified";
7+
import remarkParse from "remark-parse";
8+
import remarkStringify from "remark-stringify";
9+
10+
let markdownString;
11+
const fileFromArgs = process.argv[2];
12+
const filePath = path.join(process.cwd(), fileFromArgs);
13+
try {
14+
markdownString = readFileSync(filePath, "utf-8");
15+
} catch (err) {
16+
console.log(
17+
`Could not read or maybe even find your markdown file. \nCheck your file path, name, extension. \nMake sure you type 'npm run check -- pathtofile.md'\nError from Node.js: ${err}`
18+
);
19+
process.exit(1);
20+
}
21+
22+
let ast;
23+
try {
24+
ast = getAst(markdownString);
25+
} catch (err) {
26+
console.log(`Could not parse the markdown into a syntax tree: ${err}`);
27+
process.exit(1);
28+
}
29+
30+
try {
31+
const result = format(ast, filePath);
32+
33+
if (result.changed) {
34+
console.log("Updated markdown");
35+
}
36+
if (result.correct) {
37+
console.log("Looks like the markdown was good enough");
38+
process.exit(0);
39+
} else {
40+
console.log(`There was a problem with the markdown...\n ${result.reason}`);
41+
process.exit(1);
42+
}
43+
} catch (err) {
44+
console.log(
45+
`I must have made a mistake or not handled an error, soz\n${err}`
46+
);
47+
process.exit(1);
48+
}
49+
50+
function getAst(markdownString) {
51+
const tree = unified()
52+
.use(remarkParse)
53+
.parse(markdownString);
54+
return tree;
55+
}
56+
57+
function format(ast, path) {
58+
try {
59+
let changed = false;
60+
const content = ast.children;
61+
if (!content.length) {
62+
return {
63+
correct: false,
64+
reason: "Empty file maybeee",
65+
};
66+
}
67+
68+
// heading 1 is optional so if it's there just get rid of it and check the rest
69+
if (content[0].type === "heading" && content[0].depth === 1) {
70+
content.splice(0, 1);
71+
}
72+
73+
// now we may have removed the h1, the new 'first' item should be a h2
74+
// check first item is a heading
75+
if (content[0].type !== "heading" || content[0].depth !== 2) {
76+
return {
77+
correct: false,
78+
reason:
79+
"There should be a level 2 heading at the top, or immediately after the level 1 heading if you have one. If you have a level 1 heading, make sure there is no text between that and the level 2 heading",
80+
};
81+
}
82+
83+
for (let i = 0; i < content.length; i++) {
84+
// checks on all ## headings
85+
const item = content[i];
86+
if (item.type === "heading" && item.depth === 2) {
87+
// check correct amount of text is at heading 2
88+
if (
89+
item.children.length === 2 &&
90+
item.children[0].type === "link" &&
91+
item.children[0].children.length === 1 &&
92+
item.children[0].children[0].type === "text" &&
93+
item.children[1].type === "text"
94+
) {
95+
const link = item.children[0];
96+
item.children = [item.children[1]];
97+
item.children[0].value = link.children[0].value + item.children[0].value;
98+
changed = true;
99+
} else if (item.children.length !== 1) {
100+
console.log(
101+
`${path}:${item.position.start.line}:${item.position.start.column}`
102+
);
103+
return {
104+
correct: false,
105+
reason:
106+
"Level 2 headings should say version and date, e.g. 1.9.2 (2023-02-10) and contain no other markdown",
107+
};
108+
}
109+
110+
const heading2 = item.children[0];
111+
const heading2Text = heading2.value;
112+
113+
// check heading 2 text can be split into exactly 2 parts at the point of a space
114+
let textParts;
115+
try {
116+
textParts = heading2Text.split(" ");
117+
} catch (err) {
118+
console.log(
119+
`${path}:${heading2.position.start.line}:${heading2.position.start.column}`
120+
);
121+
return {
122+
correct: false,
123+
reason: `Level 2 headings should contain a space. We expect one between the semantic version number and the date. Error message: ${err}`,
124+
};
125+
}
126+
127+
if (textParts.length > 2) {
128+
console.log(
129+
`${path}:${heading2.position.start.line}:${heading2.position.start.column}`
130+
);
131+
return {
132+
correct: false,
133+
reason:
134+
"Level 2 headings should only contain one space. We expect one between the semantic version number and the date",
135+
};
136+
}
137+
138+
// check first part of header 2 is a semantic version number
139+
const expectedSemanticVersion = textParts[0];
140+
if (
141+
!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test(
142+
expectedSemanticVersion
143+
)
144+
) {
145+
console.log(
146+
`${path}:${heading2.position.start.line}:${heading2.position.start.column}`
147+
);
148+
return {
149+
correct: false,
150+
reason:
151+
"First part of level 2 headings should be a semantic version, e.g. 1.9.2",
152+
};
153+
}
154+
155+
// check second part of header 2 is a date in format YYYY-MM-DD
156+
const expectedDate = textParts[1];
157+
if (
158+
!/^\((\d{4,5}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])\))$/.test(
159+
expectedDate
160+
)
161+
) {
162+
console.log(
163+
`${path}:${heading2.position.start.line}:${heading2.position.start.column}`
164+
);
165+
return {
166+
correct: false,
167+
reason:
168+
"Second part of level 2 headings should be a hypen-separated date in the format YYYY-MM-DD",
169+
};
170+
}
171+
172+
// check it is followed by at least one level 3 heading
173+
if (
174+
!content[i + 1] ||
175+
content[i + 1].type !== "heading" ||
176+
content[i + 1].depth !== 3
177+
) {
178+
console.log(
179+
`${path}:${item.position.start.line}:${item.position.start.column}`
180+
);
181+
return {
182+
correct: false,
183+
reason:
184+
"Level 2 headings must be followed by at least one level 3 heading",
185+
};
186+
}
187+
}
188+
189+
// checks on all ### headings
190+
if (item.type === "heading" && item.depth === 3) {
191+
// check it only uses one of the fixed options for change types
192+
if (
193+
item.children.length !== 1 ||
194+
item.children[0].type !== "text" ||
195+
![
196+
"Added",
197+
"Changed",
198+
"Deprecated",
199+
"Removed",
200+
"Fixed",
201+
"Security",
202+
].includes(item.children[0].value)
203+
) {
204+
console.log(
205+
`${path}:${item.children[0].position.start.line}:${item.children[0].position.start.column}`
206+
);
207+
return {
208+
correct: false,
209+
reason: `Level 3 headings should only be one of 'Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security'`,
210+
};
211+
}
212+
213+
// check that there is something other than a heading following it, which we have to presume describes the change
214+
if (!content[i + 1] || content[i + 1].type === "heading") {
215+
console.log(
216+
`${path}:${item.position.start.line}:${item.position.start.column}`
217+
);
218+
return {
219+
correct: false,
220+
reason:
221+
"Level 3 headings must be followed by something other than the next heading - you should use text to describe the change",
222+
};
223+
}
224+
}
225+
}
226+
227+
if (changed) {
228+
// ...work around convoluted API...
229+
const wat = { data () {} };
230+
remarkStringify.call(wat);
231+
const output = wat.compiler(ast);
232+
writeFileSync(path, output, 'utf8');
233+
}
234+
235+
return { correct: true, changed };
236+
} catch (err) {
237+
console.error("Must be an error in my checks: ", err);
238+
process.exit(1);
239+
}
240+
}

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@
3333
"build:debug": "DEBUG=true make -j8 -C runtime/js-compute-runtime && cp runtime/js-compute-runtime/*.wasm .",
3434
"build:starlingmonkey": "./runtime/fastly/build-release.sh",
3535
"build:starlingmonkey:debug": "./runtime/fastly/build-debug.sh",
36-
"check-changelog": "cae-release-notes-format-checker CHANGELOG.md"
36+
"format-changelog": "node ci/format-changelog.js CHANGELOG.md"
3737
},
3838
"devDependencies": {
3939
"@jakechampion/cli-testing-library": "^1.0.0",
4040
"brittle": "^3.2.1",
41-
"cae-release-notes-format-checker": "^1.0.2",
4241
"eslint": "^8.40.0",
4342
"get-bin-path": "^9.0.0",
43+
"remark-parse": "^11.0.0",
44+
"remark-stringify": "^11.0.0",
4445
"tsd": "^0.28.1",
45-
"typescript": "^5.0"
46+
"typescript": "^5.0",
47+
"unified": "^11.0.0"
4648
},
4749
"dependencies": {
4850
"@bytecodealliance/jco": "^0.10.0",

0 commit comments

Comments
 (0)