diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..dbe6df65f0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +## Proposed Changes + +[Optional] Fixes $linkToTheIssue + +[Optional] Fixes "Description of the issue" + +[Optional] RelNote [Section\Subsection] Adds a feature + + - + - + - + +## Testing + +Describe how you tested your changes + +[Optional] This should be tested by QA diff --git a/tools/changelog.main.kts b/tools/changelog.main.kts new file mode 100644 index 0000000000..7056ef7f21 --- /dev/null +++ b/tools/changelog.main.kts @@ -0,0 +1,301 @@ +/** + * 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 ") +val token = args.getOrNull(2) + +// TODO automate or pass as arguments +// Exclude cherry-picked commits to the previous versions +val excludeFirstCommit = "v1.6.0-rc01" +val excludeLastCommit = "v1.6.2" + +if (token == null) { + println("To increase the rate limit, specify token (https://github.com/settings/tokens): kotlin changelog.main.kts TOKEN") +} + +// 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("\n# CHANGELOG") + +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()) + } + } + + val commits = fetchPagedUntilEmpty { page -> + request("https://api.github.com/repos/$repo/compare/$firstCommit...$lastCommit?per_page=1000&page=$page") + .commits + }.toSet() + + fun GitHubCompareResponse.CommitEntry.commitId() = repoNumberForCommit(this)?.toString() ?: sha + + val excludedCommitIds = fetchPagedUntilEmpty { page -> + request("https://api.github.com/repos/$repo/compare/$excludeFirstCommit...$excludeLastCommit?per_page=1000&page=$page") + .commits + .map { it.commitId() } + }.toSet() + + // Exclude cherry-picks with the same message (they have a different SHA, we can compare by it) + val nonCherrypickedCommits = commits.filter { it.commitId() !in excludedCommitIds } + + return nonCherrypickedCommits + .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") + val prLink = commitToPRLinkMapping[commit.sha] + return when { + prLink != null -> prLink.substringAfterLast("/").toIntOrNull() + // check title similar to `Fix import android flavors with compose resources (#4319)` + commitTitle.contains(" (#") -> commitTitle.substringAfter(" (#").substringBefore(")").toIntOrNull() + else -> null + } +} + +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.Normal + } +} + +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