mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
152 lines
4.7 KiB
JavaScript
152 lines
4.7 KiB
JavaScript
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")
|
|
}
|