-
Notifications
You must be signed in to change notification settings - Fork 35
/
Copy pathchangelog.civet
299 lines (254 loc) · 8.28 KB
/
changelog.civet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
// Usage: `npm run changelog` or `civet build/changelog.civet`
// Or `yarn changelog --gh` to run authenticated via `gh` CLI tool
{ spawnSync } from child_process
{ readFileSync, writeFileSync } from fs
{ dirname, join } from path
{ fileURLToPath } from url
repoUrl := 'https://github.com/DanielXMoore/Civet'
// Change to project root directory, one up from `build` where this file is
process.chdir join (dirname fileURLToPath import.meta.url), '..'
function run(command: string, args: string[], options?: object): string
sub := spawnSync command, args, {
encoding: 'utf8'
maxBuffer: 32 * 1024 * 1024 // 32 MB
...options
}
if sub.error
console.error sub.error
process.exit 1
if sub.signal
console.error `${command} died from signal ${sub.signal}`
process.exit 1
if sub.status
console.error sub.stderr
process.exit sub.status
sub.stdout
// Find commits for each version of Civet
versionLogs := run 'git', [
'log'
'--topo-order'
'--reverse'
'--patch'
'--date=iso'
'--'
'package.json'
]
interface Version
version: string
commit?: string
date?: string?
tag?: string
prs?: string[]
changes?: string[]
versions: Version[] := []
let commit?: string, date?: string
for each line of versionLogs.split '\n'
if match := line.match /^commit ([0-9a-f]+)/
[, commit] = match
else if match := line.match /^Date:\s*(\S+)/
[, date] = match
else if [, version] := line.match /^\+ "version": "([^"]+)"/
if commit?
versions.push { commit, date, version }
else
console.warn `Version ${version} outside of a commit`
console.log `${versions#} versions, from ${versions.0.version} to ${versions.-1.version}`
commitToVersion: Record<string, Version> := {}
for each version of versions
commitToVersion[version.commit] = version if version.commit?
// Check for uncommitted local changes
let uncommitted: Version?
if [, version] := readFileSync('package.json', encoding: 'utf8').match /^ "version": "([^"]+)"/m
unless versions.find .version is version
console.log `+ uncommitted version ${version}`
now := new Date
versions.push uncommitted = {
version
date: `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
}
// Tag version commits
existingTag: Map<string, string> := new Map // tag -> commit
run 'git', ['tag', '--list', '--format=%(object) %(refname:short)', 'v\\*']
.split '\n'
.forEach (line) =>
[commit, tag] := line.split ' '
existingTag.set tag, commit
tags .= 0
function versionTag(version: Version): void
tag := `v${version.version}`
if existingTag.has tag
if version.commit is not existingTag.get tag
console.warn `Tag ${tag} already exists but points to ${existingTag.get tag} instead of ${version.commit}`
else if version.commit?
console.log `Tagging ${tag} -> ${version.commit}`
run 'git', ['tag', '-a', tag, '-m', tag, version.commit]
tags++
version.tag = tag
for each version of versions
versionTag version
// Find pull requests between each version
prLogs := run 'git', [
'log'
'--topo-order'
'--reverse'
// We can't just look at PR merge commits; also need to see version commits.
//'--grep'
//'Merge pull request #[0-9]'
]
let prs: string[] = []
let pr?: string
for each line of prLogs.split '\n'
if pr? and line is not like /^\s/
console.warn `No message for pull request #${pr}`
pr = undefined
if [, commit] := line.match /^commit ([0-9a-f]+)/
if commit in commitToVersion
commitToVersion[commit].prs = prs
prs = []
else if match := line.match /^ Merge pull request #([0-9]+) from (\S+)/
unless match.2 is like /\/dependabot\//
pr = match.1
else if pr and line is not like /^\s*$/
prs.push pr
pr = undefined
if prs#
if uncommitted?
uncommitted.prs = prs
else
versions.push { prs, version: 'Unreleased' }
// Look up current PR titles
interface PRRecord
number: number
title: string
body: string
type WithPages<T> = T & { numPages?: number }
function ghAPI(endpoint: string, fields: Record<string, string>, headers: Record<string, string>): Promise<WithPages<unknown>>
let json: (WithPages<unknown> & {
message?: string
documentation_url?: string
})?
if process.argv.includes '--gh'
args := [
'api'
endpoint
'--include' // header output
'--method', 'GET'
]
for key, value in fields
args.push '--raw-field', `${key}=${value}`
for key, value in headers
args.push '--header', `${key}: ${value}`
response := run 'gh', args
[header, body] := response.split /\r?\n\r?\n/, 2
json = body
|> .replace /^[^]*?\n\n/, '' // remove headers
|> JSON.parse
if match := header.match /(?:^|\n)Link:.*?<([^>]+)>; rel="last"/
if [, numPages] := match.1.match /[?&]page=([0-9]+)/
json!.numPages = Number numPages
else
url .= 'https://api.github.com'
url += '/' unless endpoint.startsWith '/'
url += endpoint
url += '?'
url += (
for key, value in fields
`${key}=${value}`
).join '&'
response := fetch url, headers
|> await
json = response.json()
|> await
if match := response.headers.get('link')?.match /<([^>]+)>; rel="last"/
if [, numPages] := match.1.match /&page=([0-9]+)/
json!.numPages = Number numPages
json = json!
if json.message?
console.error `API error: ${json.message} ${
if json.documentation_url then `[${json.documentation_url}]` else ''
}`
unless process.argv.includes '--gh'
console.log `Try authenticating with 'gh' CLI tool and run with --gh flag`
process.exit 1
json
function ghPR(page: number): Promise<WithPages<PRRecord[]>>
ghAPI '/repos/DanielXMoore/Civet/pulls',
state: 'closed'
per_page: '100'
page: page.toString()
,
Accept: 'application/vnd.github+json'
'X-GitHub-Api-Version': '2022-11-28'
|> await
|> as WithPages<PRRecord[]>
prData := await ghPR 1
if prData.numPages? > 1
await.all
for page of [2..prData.numPages]
async do prData.push ...await ghPR page
prMap: Record<string, PRRecord> := {}
for each pr of prData
prMap[pr.number] = pr
// Output
changelog .= '''\
# Civet Changelog
This changelog is generated automatically by [`build/changelog.civet`](build/changelog.civet).
For each version of Civet, it lists and links to all incorporated PRs,
as well as a full diff and commit list.
'''
function tagOrCommit(version: Version?): string?
return unless version?
version.tag ?? version.commit ?? '???'
total .= 0
versions.reverse()
for each version, i of versions
prevCommit := tagOrCommit versions[i+1]
details := [
version.date
if (version.tag? or version.commit?)? and prevCommit?
`[diff](${repoUrl}/compare/${prevCommit}...${tagOrCommit version})`
if version.tag?
`[commits](${repoUrl}/commits/${version.tag})`
else if version.commit?
`[commit](${repoUrl}/commit/${version.commit})`
].filter &?
detail := if details# then ` (${details.join ', '})` else ''
changelog += `## ${version.version}${detail}\n`
for each pr of version.prs ?? []
changelog += `* ${prMap[pr]?.title ?? ''} [[#${pr}](${repoUrl}/pull/${pr})]\n`
if match := prMap[pr]?.body?.match /^BREAKING CHANGE: ([^\r\n]+)/m
changelog += ` * ${match.0}\n`
total++
changelog += '\n'
writeFileSync 'CHANGELOG.md', changelog, encoding: 'utf8'
console.log `Wrote ${total} changes to CHANGELOG.md`
// Release commit
if uncommitted?
if process.argv.includes '--release'
console.log '---------------------'
run 'git', [
'commit'
'-a'
'--verbose'
'--dry-run'
], stdio: 'inherit'
readline from node:readline/promises
rl := readline.createInterface
input: process.stdin
output: process.stdout
answer := await rl.question
`Make new release commit for ${uncommitted.version}? `
rl.close()
if answer.toLowerCase().startsWith 'y'
run 'git', [
'commit'
'-a'
'-m', uncommitted.version
], stdio: 'inherit'
// Tag new commit
uncommitted.commit = run 'git', ['rev-parse', 'HEAD']
versionTag uncommitted
else
console.log `!! Don't forget to do a release commit with new package.json and CHANGELOG.md`
if tags
console.log `!! Don't forget to push ${tags} new tags with 'git push --follow-tags'`