| 
									
										
										
										
											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. | 
					
						
							| 
									
										
										
										
											2022-06-27 14:45:40 +02:00
										 |  |  |  * If an issue has no platform label, then it will be included | 
					
						
							| 
									
										
										
										
											2022-02-23 14:02:45 +01:00
										 |  |  |  * If an issue has a label for a different platform, it won't be included, | 
					
						
							| 
									
										
										
										
											2022-06-27 14:45:40 +02:00
										 |  |  |  * _unless_ it also has the label for the specified platform. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * issues that have the "dev bug" label won't be included in any case. | 
					
						
							| 
									
										
										
										
											2022-02-23 14:02:45 +01:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											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"] | 
					
						
							| 
									
										
										
										
											2022-06-27 14:45:40 +02:00
										 |  |  | 	issues = issues.filter(issue => !issue.labels.some(label => label.name === "dev bug")) | 
					
						
							| 
									
										
										
										
											2022-02-23 14:02:45 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } |