2021-06-18 11:10:33 +02:00
|
|
|
import {Octokit} from "@octokit/rest"
|
2022-04-28 14:36:24 +02:00
|
|
|
import {Option, program} from "commander"
|
2021-06-18 11:10:33 +02:00
|
|
|
import {fileURLToPath} from "url"
|
|
|
|
import path from "path"
|
|
|
|
import fs from "fs"
|
|
|
|
|
|
|
|
const wasRunFromCli = fileURLToPath(import.meta.url).startsWith(process.argv[1])
|
|
|
|
|
|
|
|
if (wasRunFromCli) {
|
2022-04-28 14:36:24 +02:00
|
|
|
program
|
2022-04-26 18:09:04 +02:00
|
|
|
.requiredOption('--releaseName <releaseName>', "Name of the release")
|
2021-06-28 13:03:54 +02:00
|
|
|
.requiredOption('--milestone <milestone>', "Milestone to reference")
|
|
|
|
.requiredOption('--tag <tag>', "The commit tag to reference")
|
2022-04-28 14:36:24 +02:00
|
|
|
.addOption(
|
|
|
|
new Option("--platform <platform>", 'Which platform to build')
|
|
|
|
.choices(["android", "ios", "desktop", "all"])
|
|
|
|
.default("all")
|
|
|
|
)
|
2021-06-18 11:10:33 +02:00
|
|
|
.option('--uploadFile <filePath>', "Path to a file to upload")
|
|
|
|
.option('--apkChecksum <checksum>', "Checksum for the APK")
|
2022-04-26 18:09:04 +02:00
|
|
|
.option('--toFile <toFile>', "If provided, the release notes will be written to the given file path. Implies `--dryRun`")
|
|
|
|
.option('--dryRun', "Don't make any changes to github")
|
|
|
|
.option('--format <format>', "Format to generate notes in", "github")
|
|
|
|
.action(async (options) => {
|
|
|
|
await createReleaseNotes(options)
|
|
|
|
})
|
|
|
|
.parseAsync(process.argv)
|
|
|
|
}
|
2021-06-18 11:10:33 +02:00
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
async function createReleaseNotes(
|
|
|
|
{
|
|
|
|
releaseName,
|
|
|
|
milestone,
|
|
|
|
tag,
|
|
|
|
platform,
|
|
|
|
uploadFile,
|
|
|
|
apkChecksum,
|
|
|
|
toFile,
|
|
|
|
dryRun,
|
|
|
|
format
|
|
|
|
}
|
|
|
|
) {
|
2021-06-18 11:10:33 +02:00
|
|
|
|
|
|
|
const releaseToken = process.env.GITHUB_TOKEN
|
|
|
|
|
|
|
|
if (!releaseToken) {
|
|
|
|
throw new Error("No GITHUB_TOKEN set!")
|
|
|
|
}
|
|
|
|
|
|
|
|
const octokit = new Octokit({
|
|
|
|
auth: releaseToken,
|
|
|
|
userAgent: 'tuta-github-release-v0.0.1'
|
|
|
|
})
|
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
let releaseNotes
|
|
|
|
|
|
|
|
const githubMilestone = await getMilestone(octokit, milestone)
|
|
|
|
const issues = await getIssuesForMilestone(octokit, githubMilestone)
|
|
|
|
const {bugs, other} = sortIssues(filterIssues(issues, platform))
|
|
|
|
|
|
|
|
if (format === "ios") {
|
|
|
|
releaseNotes = renderIosReleaseNotes(bugs, other)
|
|
|
|
} else {
|
|
|
|
releaseNotes = renderGithubReleaseNotes({
|
|
|
|
milestoneUrl: githubMilestone.html_url,
|
|
|
|
bugIssues: bugs,
|
|
|
|
otherIssues: other,
|
|
|
|
apkChecksum: apkChecksum
|
2021-06-18 11:10:33 +02:00
|
|
|
})
|
2022-04-26 18:09:04 +02:00
|
|
|
}
|
2021-06-18 11:10:33 +02:00
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
console.log("Release notes:")
|
|
|
|
console.log(releaseNotes)
|
2021-06-18 11:10:33 +02:00
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
if (!dryRun && !toFile) {
|
|
|
|
const draftResponse = await createReleaseDraft(octokit, releaseName, tag, releaseNotes)
|
2021-06-18 11:10:33 +02:00
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
const {upload_url, id} = draftResponse.data
|
|
|
|
|
|
|
|
if (uploadFile) {
|
|
|
|
console.log(`Uploading asset "${uploadFile}"`)
|
|
|
|
await uploadAsset(octokit, upload_url, id, uploadFile)
|
|
|
|
}
|
2021-06-18 11:10:33 +02:00
|
|
|
}
|
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
if (toFile) {
|
2022-05-11 10:55:59 +02:00
|
|
|
console.log(`writing release notes to ${toFile}`)
|
2022-05-11 10:48:15 +02:00
|
|
|
await fs.promises.writeFile(toFile, releaseNotes, "utf-8")
|
2022-04-26 18:09:04 +02:00
|
|
|
}
|
2021-06-18 11:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function getMilestone(octokit, milestoneName) {
|
2022-04-26 18:09:04 +02:00
|
|
|
const {data} = await octokit.issues.listMilestones({
|
|
|
|
owner: "tutao",
|
|
|
|
repo: "tutanota",
|
|
|
|
direction: "desc",
|
|
|
|
state: "all"
|
|
|
|
})
|
2021-06-18 11:10:33 +02:00
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
const milestone = data.find(m => m.title === milestoneName)
|
2021-06-18 11:10:33 +02:00
|
|
|
|
|
|
|
if (milestone) {
|
|
|
|
return milestone
|
|
|
|
} else {
|
2022-04-26 18:09:04 +02:00
|
|
|
const titles = data.map(m => m.title)
|
2021-06-18 11:10:33 +02:00
|
|
|
throw new Error(`No milestone named ${milestoneName} found. Milestones: ${titles.join(", ")}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
async function getIssuesForMilestone(octokit, milestone) {
|
2021-06-18 11:10:33 +02:00
|
|
|
const response = await octokit.issues.listForRepo({
|
|
|
|
owner: "tutao",
|
|
|
|
repo: "tutanota",
|
|
|
|
milestone: milestone.number,
|
|
|
|
state: "all"
|
|
|
|
})
|
|
|
|
return response.data
|
|
|
|
}
|
|
|
|
|
2022-02-23 14:02:45 +01:00
|
|
|
/**
|
|
|
|
* Filter the issues for the given platform.
|
|
|
|
* If an issue has no 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 paltform
|
|
|
|
*/
|
2021-06-18 11:10:33 +02:00
|
|
|
function filterIssues(issues, platform) {
|
2022-02-23 14:02:45 +01:00
|
|
|
|
|
|
|
const allPlatforms = ["android", "ios", "desktop"]
|
|
|
|
|
2021-06-18 11:10:33 +02:00
|
|
|
if (platform === "all") {
|
|
|
|
return issues
|
2022-02-23 14:02:45 +01:00
|
|
|
} else if (allPlatforms.includes(platform)) {
|
|
|
|
const otherPlatforms = allPlatforms.filter(p => p !== platform)
|
2021-06-29 15:50:50 +02:00
|
|
|
return issues.filter(issue =>
|
2022-02-23 14:02:45 +01:00
|
|
|
issue.labels.some(label => label.name === platform) ||
|
|
|
|
!issue.labels.some(label => otherPlatforms.includes(label.name))
|
|
|
|
)
|
2021-06-18 11:10:33 +02:00
|
|
|
} else {
|
|
|
|
throw new Error(`Invalid value "${platform}" for "platform"`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
/**
|
|
|
|
* Sort issues into bug issues and other issues
|
|
|
|
*/
|
2021-06-18 11:10:33 +02:00
|
|
|
function sortIssues(issues) {
|
|
|
|
const bugs = []
|
2022-04-26 18:09:04 +02:00
|
|
|
const other = []
|
2021-06-18 11:10:33 +02:00
|
|
|
for (const issue of issues) {
|
2022-05-11 10:40:54 +02:00
|
|
|
const isBug = issue.labels.find(l => l.name === "bug" || l.name === "dev bug")
|
2021-06-18 11:10:33 +02:00
|
|
|
if (isBug) {
|
|
|
|
bugs.push(issue)
|
|
|
|
} else {
|
2022-04-26 18:09:04 +02:00
|
|
|
other.push(issue)
|
2021-06-18 11:10:33 +02:00
|
|
|
}
|
|
|
|
}
|
2022-04-26 18:09:04 +02:00
|
|
|
return {bugs, other}
|
2021-06-18 11:10:33 +02:00
|
|
|
}
|
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
function renderGithubReleaseNotes({milestoneUrl, bugIssues, otherIssues, apkChecksum}) {
|
|
|
|
|
|
|
|
const whatsNewListRendered = otherIssues.map(issue => {
|
|
|
|
return ` - ${issue.title} #${issue.number}`
|
2021-06-18 11:10:33 +02:00
|
|
|
}).join("\n")
|
2022-04-26 18:09:04 +02:00
|
|
|
|
|
|
|
const bugsListRendered = bugIssues.map(issue => {
|
|
|
|
return ` - ${issue.title} #${issue.number}`
|
2021-06-18 11:10:33 +02:00
|
|
|
}).join("\n")
|
|
|
|
|
|
|
|
const milestoneUrlObject = new URL(milestoneUrl)
|
|
|
|
milestoneUrlObject.searchParams.append("closed", "1")
|
2022-04-26 18:09:04 +02:00
|
|
|
|
|
|
|
const apkSection = apkChecksum ? `# APK Checksum\nSHA256: ${apkChecksum}` : ""
|
|
|
|
|
|
|
|
return `
|
|
|
|
# What's new
|
2021-06-18 11:10:33 +02:00
|
|
|
${whatsNewListRendered}
|
|
|
|
|
|
|
|
# Bugfixes
|
|
|
|
${bugsListRendered}
|
|
|
|
|
|
|
|
# Milestone
|
|
|
|
${milestoneUrlObject.toString()}
|
|
|
|
|
2022-04-26 18:09:04 +02:00
|
|
|
${apkSection}
|
|
|
|
`.trim()
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderIosReleaseNotes(bugs, rest) {
|
|
|
|
return `
|
|
|
|
what's new:
|
|
|
|
${rest.map(issue => issue.title).join("\n")}
|
|
|
|
|
|
|
|
bugfixes:
|
|
|
|
${bugs.map(issue => issue.title).join("\n")}`.trim()
|
2021-06-18 11:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function createReleaseDraft(octokit, name, tag, body) {
|
|
|
|
return octokit.repos.createRelease({
|
|
|
|
owner: "tutao",
|
|
|
|
repo: "tutanota",
|
|
|
|
draft: true,
|
|
|
|
name,
|
|
|
|
tag_name: tag,
|
|
|
|
body,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async function uploadAsset(octokit, uploadUrl, releaseId, assetPath) {
|
|
|
|
const response = octokit.rest.repos.uploadReleaseAsset({
|
|
|
|
owner: "tutao",
|
|
|
|
repo: "tutanota",
|
|
|
|
release_id: releaseId,
|
|
|
|
data: await fs.promises.readFile(assetPath),
|
|
|
|
name: path.basename(assetPath),
|
|
|
|
upload_url: uploadUrl
|
|
|
|
});
|
|
|
|
|
|
|
|
if (response.status < 200 || response.status > 299) {
|
|
|
|
console.error(`Asset upload failed "${assetPath}. Response:"`, response)
|
|
|
|
}
|
|
|
|
}
|