-
Notifications
You must be signed in to change notification settings - Fork 546
/
Copy pathreleaseNotes.js
152 lines (129 loc) · 4.73 KB
/
releaseNotes.js
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
import { Octokit } from "@octokit/rest"
import { Option, program } from "commander"
import { fileURLToPath } from "node:url"
import fs from "node:fs"
import crypto from "node:crypto"
const wasRunFromCli = fileURLToPath(import.meta.url).startsWith(process.argv[1])
if (wasRunFromCli) {
program
.requiredOption("--milestone <milestone>", "Milestone name or milestone number to reference")
.addOption(
new Option("--platform <platform>", "label filter for the issues to include in the notes")
.choices(["android", "ios", "desktop", "web"])
.default("web"),
)
.action(async (options) => {
await renderReleaseNotes(options)
})
.parseAsync(process.argv)
}
async function renderReleaseNotes({ milestone, platform }) {
const octokit = new Octokit({
userAgent: "tuta-github-release-v0.0.1",
})
const githubMilestone = await getMilestone(octokit, milestone)
const issues = await getIssuesForMilestone(octokit, githubMilestone)
const { bugs, other } = sortIssues(filterIssues(issues, platform))
const releaseNotes =
platform === "ios"
? renderIosReleaseNotes(bugs, other)
: renderGithubReleaseNotes({
milestoneUrl: githubMilestone.html_url,
bugIssues: bugs,
otherIssues: other,
})
console.log(releaseNotes)
}
async function getMilestone(octokit, milestoneNameOrNumber) {
const { data } = await octokit.issues.listMilestones({
owner: "tutao",
repo: "tutanota",
direction: "desc",
state: "all",
})
const milestone = data.find((m) => m.title === milestoneNameOrNumber || String(m.number) === milestoneNameOrNumber)
if (milestone) {
return milestone
} else {
const titles = data.map((m) => `${m.title} (${m.number})`)
throw new Error(`No milestone ${milestoneNameOrNumber} found. Milestones:
${titles.join(",\n\t")}`)
}
}
async function getIssuesForMilestone(octokit, milestone) {
const response = await octokit.issues.listForRepo({
owner: "tutao",
repo: "tutanota",
milestone: milestone.number,
state: "all",
})
return response.data
}
/**
* Filter the issues for the given platform.
* If an issue has no platform label, then it will be included
* If an issue has a label for a different platform, it won't be included,
* _unless_ it also has the label for the specified platform.
*/
function filterIssues(issues, platform) {
const allPlatforms = new Set(["android", "ios", "desktop"])
// issues that have any of these labels will not be included in any release notes
const excludedLabels = new Set(["dev bug", "topic:usage test", "no-release-notes"])
issues = issues.filter((issue) => !issue.labels.some((label) => excludedLabels.has(label.name)))
if (platform === "web") {
// for the web app, we only want to include issues that don't have a platform label
return issues.filter((i) => areDisjoint(labelSet(i), allPlatforms))
} else if (allPlatforms.has(platform)) {
const otherPlatforms = new Set(allPlatforms)
otherPlatforms.delete(platform)
return issues.filter((issue) => issue.labels.some((label) => label.name === platform) || !issue.labels.some((label) => otherPlatforms.has(label.name)))
} else {
throw new Error(`Invalid value "${platform}" for "platform"`)
}
}
/**
* Sort issues into bug issues and other issues
*/
function sortIssues(issues) {
const bugs = []
const other = []
for (const issue of issues) {
const isBug = issue.labels.find((l) => l.name === "bug" || l.name === "dev bug")
if (isBug) {
bugs.push(issue)
} else {
other.push(issue)
}
}
return { bugs, other }
}
function renderGithubReleaseNotes({ milestoneUrl, bugIssues, otherIssues }) {
const whatsNewListRendered = otherIssues.length > 0 ? "# What's new\n" + otherIssues.map((issue) => ` - ${issue.title} #${issue.number}`).join("\n") : ""
const bugsListRendered = bugIssues.length > 0 ? "# Bugfixes\n" + bugIssues.map((issue) => ` - ${issue.title} #${issue.number}`).join("\n") : ""
const milestoneUrlObject = new URL(milestoneUrl)
milestoneUrlObject.searchParams.append("closed", "1")
return `
${whatsNewListRendered}
${bugsListRendered}
# Milestone
${milestoneUrlObject.toString()}
`.trim()
}
function renderIosReleaseNotes(bugs, rest) {
const whatsNewSection = rest.length > 0 ? "what's new:\n" + rest.map((issue) => issue.title).join("\n") : ""
const bugfixSection = bugs.length > 0 ? "\nbugfixes:\n" + bugs.map((issue) => "fixed " + issue.title).join("\n") : ""
return `${whatsNewSection}\n${bugfixSection}`.trim()
}
/**
* test whether two js sets have no elements in common
*/
function areDisjoint(setA, setB) {
return [...setA].filter((el) => setB.has(el)).length === 0
}
function labelSet(issue) {
return new Set(issue.labels.map((l) => l.name))
}
function hashFileSha256(filePath) {
const input = fs.readFileSync(filePath)
return crypto.createHash("sha256").update(input).digest("hex")
}