From cb6aea44b5a10dce9ccd4d9fe3ce93971fd86fcb Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Fri, 23 Feb 2024 01:19:50 +0100 Subject: [PATCH] Script for generating changelogs, new template for PR's --- .github/PULL_REQUEST_TEMPLATE.md | 16 ++ tools/changelog.main.kts | 256 +++++++++++++++++++++++++++++++ tools/commit-to-pr-mapping.txt | 1 + 3 files changed, 273 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 tools/changelog.main.kts create mode 100644 tools/commit-to-pr-mapping.txt diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..7b59668978 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +_[Optional]_ Fixes $linkToIssue + +Properly describe the issue fixed. + +Describe it from the user perspective if it affects functionality (adds a feature, fixes a bug, changes behavior, improves performance). + +Add labels for the changelog: +- no labels - don't add to the changelog +- _changelog: normal_ - add +- _changelog: highlight_ - add and highlight +- _changelog: prerelease fix_ - add _(prerelease fix)_ to the title in a prerelease changelog, don’t include in a stable changelog +- _ios, desktop, web, android, common_ - affected platforms + +## Testing + +Describe how you tested your changes. diff --git a/tools/changelog.main.kts b/tools/changelog.main.kts new file mode 100644 index 0000000000..250e98bf50 --- /dev/null +++ b/tools/changelog.main.kts @@ -0,0 +1,256 @@ +/** + * Script for creating a changelog. Call + * ``` + * kotlin changelog.main.kts v1.6.0-rc02 release/1.6.0 + * ``` + * where v1.6.0-rc02 - the first commit + * where release/1.6.0 - the last commit + */ + +/** + * Run from command line: + * 1. Download https://github.com/JetBrains/kotlin/releases/tag/v1.9.22 and add `bin` to PATH + * 2. Call `kotlin ` + * + * Run from IntelliJ: + * 1. Right click on the script + * 2. More Run/Debug + * 3. Modify Run Configuration... + * 4. Clear all "Before launch" tasks (you can edit the system-wide template as well) + * 5. OK + */ + +@file:Repository("https://repo1.maven.org/maven2/") +@file:DependsOn("com.google.code.gson:gson:2.10.1") + +import com.google.gson.Gson +import java.io.File +import java.io.IOException +import java.net.URL +import java.util.concurrent.TimeUnit + +val firstCommit = args.getOrNull(0) ?: error("Please call this way: kotlin changelog.main.kts ") +val lastCommit = args.getOrNull(1) ?: error("Please call this way: kotlin changelog.main.kts ") + +// commits that don't have a link to a PR (a link should be something like " (#454)") +val commitToPRLinkMapping = File("commit-to-pr-mapping.txt").readLines().associate { + val splits = it.split(" ") + val commit = splits[0] + val prLink = splits[1] + commit to prLink +} + +val entries = entriesForRepo("JetBrains/compose-multiplatform-core") + + entriesForRepo("JetBrains/compose-multiplatform") + +fun List.ofType(type: Type) = + filter { it.type == type }.sortedByDescending { it.platforms.sumOf { it.sorting } } + +val highlighted = entries.ofType(Type.Highlighted) +val normal = entries.ofType(Type.Normal) +val prereleaseFixes = entries.ofType(Type.PrereleaseFix) +val unknown = entries.ofType(Type.Unknown) + +println( + buildString { + append("_Changes since ${commitToVersion(firstCommit)}_\n") + append("\n") + + if (highlighted.isNotEmpty()) + append(highlighted.joinToString("\n") { it.format() }).append("\n") + + if (normal.isNotEmpty()) + append(normal.joinToString("\n") { it.format() }).append("\n") + + if (prereleaseFixes.isNotEmpty()) + append(prereleaseFixes.joinToString("\n") { it.format() }).append("\n") + + if (unknown.isNotEmpty()) { + append("\nUnknown changes for review:\n") + append(unknown.joinToString("\n") { it.format() }).append("\n") + } + } +) + +/** + * Transforms v1.6.0-beta01 to 1.6.0-beta01 + */ +fun commitToVersion(commit: String) = + if (commit.startsWith("v") && commit.contains(".")) { + commit.removePrefix("v") + } else { + commit + } + +fun ChangelogEntry.format() = buildString { + append("- ") + if (type == Type.Highlighted) append("**") + if (type == Type.PrereleaseFix) append("_(prerelease fix)_ ") + append("[$title]($link)") + if (type == Type.Highlighted) append("**") + platforms.forEach { + append(" $it") + } +} + +/** + * @param repo Example: + * JetBrains/compose-multiplatform-core + */ +fun entriesForRepo(repo: String): List { + val pullNumberToPull = (1..5) + .flatMap { + request>("https://api.github.com/repos/$repo/pulls?state=closed&per_page=100&page=$it").toList() + } + .associateBy { it.number } + + fun prForCommit(commit: GitHubCompareResponse.CommitEntry): GitHubPullEntry? { + val repoNumber = repoNumberForCommit(commit) + return pullNumberToPull[repoNumber] + } + + fun changelogEntryFor( + commit: GitHubCompareResponse.CommitEntry, + pullRequest: GitHubPullEntry? + ): ChangelogEntry { + return if (pullRequest != null) { + val prTitle = pullRequest.title + val prNumber = pullRequest.number + ChangelogEntry( + prTitle, + "https://github.com/$repo/pull/$prNumber", + typeOf(pullRequest), + platformsOf(pullRequest) + ) + } else { + val commitSha = commit.sha + val commitTitle = commit.commit.message.substringBefore("\n") + ChangelogEntry(commitTitle, "https://github.com/$repo/commit/$commitSha", Type.Unknown, emptyList()) + } + } + + return request("https://api.github.com/repos/$repo/compare/$firstCommit...$lastCommit") + .commits + .map { changelogEntryFor(it, prForCommit(it)) } +} + +/** + * Extract the PR number from the commit. + */ +fun repoNumberForCommit(commit: GitHubCompareResponse.CommitEntry): Int? { + val commitTitle = commit.commit.message.substringBefore("\n") + // check title similar to `Fix import android flavors with compose resources (#4319)` + return if (commitTitle.contains(" (#")) { + commitTitle.substringAfter(" (#").substringBefore(")").toIntOrNull() + } else { + commitToPRLinkMapping[commit.sha]?.substringAfterLast("/")?.toIntOrNull() + } +} + +fun typeOf(pullRequest: GitHubPullEntry): Type { + val labels = pullRequest.labels.mapTo(mutableSetOf()) { it.name.lowercase() } + return when { + labels.contains("changelog: highlight") -> Type.Highlighted + labels.contains("changelog: normal") -> Type.Normal + labels.contains("changelog: prerelease fix") -> Type.PrereleaseFix + else -> Type.Unknown + } +} + +fun platformsOf(pullRequest: GitHubPullEntry) = pullRequest.labels.mapNotNull { + when (it.name.lowercase()) { + "ios" -> Platform.IOS + "android" -> Platform.Android + "desktop" -> Platform.Desktop + "web" -> Platform.Web + "common" -> Platform.Common + else -> null + } +} + +data class ChangelogEntry( + val title: String, + val link: String, + val type: Type, + val platforms: List, +) + +enum class Type { Highlighted, Normal, PrereleaseFix, Unknown } + +enum class Platform(val title: String, val sorting: Int) { + Common("Common", 0x11111), + IOS("iOS", 0x01000), + Android("Android", 0x00100), + Desktop("Desktop", 0x00010), + Web("Web", 0x00001); + + override fun toString() = title +} + +// example https://api.github.com/repos/JetBrains/compose-multiplatform-core/compare/v1.6.0-rc02...release/1.6.0 +data class GitHubCompareResponse(val commits: List) { + data class CommitEntry(val sha: String, val commit: Commit) + data class Commit(val message: String) +} + +// example https://api.github.com/repos/JetBrains/compose-multiplatform-core/pulls?state=closed +data class GitHubPullEntry(val number: Int, val title: String, val body: String, val labels: List