Browse Source

Changelog script. Read dependencies, support N/A (#5181)

Fixes
https://youtrack.jetbrains.com/issue/CMP-7203/Changelog-script.-Generate-Dependencies-section
Fixes
https://youtrack.jetbrains.com/issue/CMP-7137/Changelog-script-small-fixes

- Read dependencies from git repos
- Support N/A
- Supports any prefix. For example `- (experimental) Change`
- Try to fix the line start

Can be read commit by commit.
pull/2080/merge
Igor Demin 1 year ago committed by GitHub
parent
commit
e9597e7193
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      .github/PULL_REQUEST_TEMPLATE.md
  2. 218
      tools/changelog.main.kts

8
.github/PULL_REQUEST_TEMPLATE.md

@ -15,16 +15,18 @@ This should be tested by QA @@ -15,16 +15,18 @@ This should be tested by QA
## Release Notes
<!--
Optional, if omitted - won't be included in the changelog
If we definitely shouldn't add Release Notes, add only N/A.
Sections:
Or enumerate sections, subsections and all changes.
Possible sections:
- Highlights
- Known Issues
- Breaking Changes
- Features
- Fixes
Subsections:
Possible subsections:
- Multiple Platforms
- iOS
- Desktop

218
tools/changelog.main.kts

@ -28,13 +28,15 @@ @@ -28,13 +28,15 @@
@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.lang.ProcessBuilder.Redirect
import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets.UTF_8
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.text.substringAfterLast
//region ========================================== CONSTANTS =========================================
@ -73,7 +75,6 @@ val argsKeyToValue = args @@ -73,7 +75,6 @@ val argsKeyToValue = args
.associate { it.substringBefore("=") to it.substringAfter("=") }
val versionCommit = argsKeyless.getOrNull(0) ?: "HEAD"
val versionName = argsKeyless.getOrNull(1) ?: versionCommit
val token = argsKeyToValue["token"]
println("Note. The script supports optional arguments: kotlin changelog.main.kts [versionCommit] [versionName] [token=githubToken]")
@ -82,6 +83,34 @@ if (token == null) { @@ -82,6 +83,34 @@ if (token == null) {
}
println()
val androidxLibToVersion = androidxLibToVersion(versionCommit)
val androidxLibToRedirectingVersion = androidxLibToRedirectingVersion(versionCommit)
fun formatAndroidxLibVersion(libName: String) =
androidxLibToVersion[libName] ?: "PLACEHOLDER".also {
println("Can't find $libName version. Using PLACEHOLDER")
}
fun formatAndroidxLibRedirectingVersion(libName: String) =
androidxLibToRedirectingVersion[libName] ?: "PLACEHOLDER".also {
println("Can't find $libName redirecting version. Using PLACEHOLDER")
}
val versionCompose = formatAndroidxLibVersion("COMPOSE")
val versionComposeMaterial3Adaptive = formatAndroidxLibVersion("COMPOSE_MATERIAL3_ADAPTIVE")
val versionLifecycle = formatAndroidxLibVersion("LIFECYCLE")
val versionNavigation = formatAndroidxLibVersion("NAVIGATION")
val versionRedirectingCompose = formatAndroidxLibRedirectingVersion("compose")
val versionRedirectingComposeFoundation = formatAndroidxLibRedirectingVersion("compose.foundation")
val versionRedirectingComposeMaterial = formatAndroidxLibRedirectingVersion("compose.material")
val versionRedirectingComposeMaterial3 = formatAndroidxLibRedirectingVersion("compose.material3")
val versionRedirectingComposeMaterial3Adaptive = formatAndroidxLibRedirectingVersion("compose.material3.adaptive")
val versionRedirectingLifecycle = formatAndroidxLibRedirectingVersion("lifecycle")
val versionRedirectingNavigation = formatAndroidxLibRedirectingVersion("navigation")
val versionName = versionCompose
val currentChangelog = changelogFile.readText()
val previousChangelog =
if (currentChangelog.startsWith("# $versionName ")) {
@ -93,9 +122,10 @@ val previousChangelog = @@ -93,9 +122,10 @@ val previousChangelog =
val previousVersion = previousChangelog.substringAfter("# ").substringBefore(" (")
println()
println("Generating changelog between $previousVersion and $versionName")
val newChangelog = getChangelog("v$previousVersion", versionCommit, previousVersion, versionName)
val newChangelog = getChangelog("v$previousVersion", versionCommit, previousVersion)
changelogFile.writeText(
newChangelog + previousChangelog
@ -105,12 +135,12 @@ println() @@ -105,12 +135,12 @@ println()
println("CHANGELOG.md changed")
fun getChangelog(firstCommit: String, lastCommit: String, firstVersion: String, lastVersion: String): String {
fun getChangelog(firstCommit: String, lastCommit: String, firstVersion: String): String {
val entries = entriesForRepo("JetBrains/compose-multiplatform-core", firstCommit, lastCommit) +
entriesForRepo("JetBrains/compose-multiplatform", firstCommit, lastCommit)
return buildString {
appendLine("# $lastVersion (${currentChangelogDate()})")
appendLine("# $versionName (${currentChangelogDate()})")
appendLine()
appendLine("_Changes since ${firstVersion}_")
@ -140,16 +170,16 @@ fun getChangelog(firstCommit: String, lastCommit: String, firstVersion: String, @@ -140,16 +170,16 @@ fun getChangelog(firstCommit: String, lastCommit: String, firstVersion: String,
"""
## Dependencies
- Gradle Plugin `org.jetbrains.compose`, version `$lastVersion`. Based on Jetpack Compose libraries:
- [Runtime REDIRECT_PLACEHOLDER](https://developer.android.com/jetpack/androidx/releases/compose-runtime#REDIRECT_PLACEHOLDER)
- [UI REDIRECT_PLACEHOLDER](https://developer.android.com/jetpack/androidx/releases/compose-ui#REDIRECT_PLACEHOLDER)
- [Foundation REDIRECT_PLACEHOLDER](https://developer.android.com/jetpack/androidx/releases/compose-foundation#REDIRECT_PLACEHOLDER)
- [Material REDIRECT_PLACEHOLDER](https://developer.android.com/jetpack/androidx/releases/compose-material#REDIRECT_PLACEHOLDER)
- [Material3 REDIRECT_PLACEHOLDER](https://developer.android.com/jetpack/androidx/releases/compose-material3#REDIRECT_PLACEHOLDER)
- Lifecycle libraries `org.jetbrains.androidx.lifecycle:lifecycle-*:RELEASE_PLACEHOLDER`. Based on [Jetpack Lifecycle REDIRECT_PLACEHOLDER](https://developer.android.com/jetpack/androidx/releases/lifecycle#REDIRECT_PLACEHOLDER)
- Navigation libraries `org.jetbrains.androidx.navigation:navigation-*:RELEASE_PLACEHOLDER`. Based on [Jetpack Navigation REDIRECT_PLACEHOLDER](https://developer.android.com/jetpack/androidx/releases/navigation#REDIRECT_PLACEHOLDER)
- Material3 Adaptive libraries `org.jetbrains.compose.material3.adaptive:adaptive*:RELEASE_PLACEHOLDER`. Based on [Jetpack Material3 Adaptive REDIRECT_PLACEHOLDER](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive#REDIRECT_PLACEHOLDER)
- Gradle Plugin `org.jetbrains.compose`, version `$versionCompose`. Based on Jetpack Compose libraries:
- [Runtime $versionRedirectingCompose](https://developer.android.com/jetpack/androidx/releases/compose-runtime#$versionRedirectingCompose)
- [UI $versionRedirectingCompose](https://developer.android.com/jetpack/androidx/releases/compose-ui#$versionRedirectingCompose)
- [Foundation $versionRedirectingComposeFoundation](https://developer.android.com/jetpack/androidx/releases/compose-foundation#$versionRedirectingComposeFoundation)
- [Material $versionRedirectingComposeMaterial](https://developer.android.com/jetpack/androidx/releases/compose-material#$versionRedirectingComposeMaterial)
- [Material3 $versionRedirectingComposeMaterial3](https://developer.android.com/jetpack/androidx/releases/compose-material3#$versionRedirectingComposeMaterial3)
- Lifecycle libraries `org.jetbrains.androidx.lifecycle:lifecycle-*:$versionLifecycle`. Based on [Jetpack Lifecycle $versionRedirectingLifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle#$versionRedirectingLifecycle)
- Navigation libraries `org.jetbrains.androidx.navigation:navigation-*:$versionNavigation`. Based on [Jetpack Navigation $versionRedirectingNavigation](https://developer.android.com/jetpack/androidx/releases/navigation#$versionRedirectingNavigation)
- Material3 Adaptive libraries `org.jetbrains.compose.material3.adaptive:adaptive*:$versionComposeMaterial3Adaptive`. Based on [Jetpack Material3 Adaptive $versionRedirectingComposeMaterial3Adaptive](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive#$versionRedirectingComposeMaterial3Adaptive)
---
""".trimIndent()
@ -188,13 +218,26 @@ fun currentChangelogDate() = LocalDate.now().format(DateTimeFormatter.ofPattern( @@ -188,13 +218,26 @@ fun currentChangelogDate() = LocalDate.now().format(DateTimeFormatter.ofPattern(
* - [A new approach to implementation of `platformLayers`](link). Now extra layers (such as Dialogs and Popups) drawing is merged into a single screen size canvas.
*/
fun ChangelogEntry.format(): String {
return try {
tryFormat()
} catch (e: Exception) {
throw RuntimeException("Formatting error of ChangelogEntry. Message:\n$message", e)
}
}
fun ChangelogEntry.tryFormat(): String {
return if (link != null) {
val prefixRegex = "^[-\\s]*" // "- "
val tagRegex1 = "\\(.*\\)\\s*" // "(something) "
val tagRegex2 = "\\[.*\\]\\s*" // "[something] "
val tagRegex3 = "_.*_\\s*" // "_something_ "
val linkStartIndex = maxOf(
message.indexOfFirst { !it.isWhitespace() && it != '-' }.ifNegative { 0 },
message.endIndexOf("_(prerelease fix)_ ").ifNegative { 0 },
message.endIndexOf("(prerelease fix) ").ifNegative { 0 },
message.endIndexOfFirstGroup(Regex("($prefixRegex).*"))?.plus(1) ?: 0,
message.endIndexOfFirstGroup(Regex("($prefixRegex$tagRegex1).*"))?.plus(1) ?: 0,
message.endIndexOfFirstGroup(Regex("($prefixRegex$tagRegex2).*"))?.plus(1) ?: 0,
message.endIndexOfFirstGroup(Regex("($prefixRegex$tagRegex3).*"))?.plus(1) ?: 0,
)
val linkLastIndex = message.indexOfAny(listOf(". ", " (")).ifNegative { message.length }
val linkLastIndex = message.indexOfAny(listOf(". ", " ("), linkStartIndex).ifNegative { message.length }
val beforeLink = message.substring(0, linkStartIndex)
val inLink = message.substring(linkStartIndex, linkLastIndex).removeLinks()
@ -208,13 +251,8 @@ fun ChangelogEntry.format(): String { @@ -208,13 +251,8 @@ fun ChangelogEntry.format(): String {
fun Int.ifNegative(value: () -> Int): Int = if (this < 0) value() else this
fun String.endIndexOf(value: String): Int = indexOf(value).let {
if (it >= 0) {
it + value.length
} else {
it
}
}
fun String.endIndexOfFirstGroup(regex: Regex): Int? =
regex.find(this)?.groups?.toList()?.getOrNull(1)?.range?.endInclusive
/**
* Converts:
@ -243,9 +281,13 @@ fun GitHubPullEntry.extractReleaseNotes(link: String): List<ChangelogEntry> { @@ -243,9 +281,13 @@ fun GitHubPullEntry.extractReleaseNotes(link: String): List<ChangelogEntry> {
before?.trim()
}
if (relNoteBody?.trim()?.lowercase() == "n/a") return emptyList()
val list = mutableListOf<ChangelogEntry>()
var section: String? = null
var subsection: String? = null
var isFirstLine = true
var shouldPadLines = false
for (line in relNoteBody.orEmpty().split("\n")) {
// parse "### Section - Subsection"
@ -253,17 +295,30 @@ fun GitHubPullEntry.extractReleaseNotes(link: String): List<ChangelogEntry> { @@ -253,17 +295,30 @@ fun GitHubPullEntry.extractReleaseNotes(link: String): List<ChangelogEntry> {
val s = line.removePrefix("### ")
section = s.substringBefore("-", "").trim().normalizeSectionName().ifEmpty { null }
subsection = s.substringAfter("-", "").trim().normalizeSubsectionName().ifEmpty { null }
isFirstLine = true
shouldPadLines = false
} else if (section != null && line.isNotBlank()) {
val isTopLevel = line.startsWith("-")
val trimmedLine = line.trimEnd().removeSuffix(".")
var lineFixed = line
if (isFirstLine && !lineFixed.startsWith("-")) {
lineFixed = "- $lineFixed"
shouldPadLines = true
}
if (!isFirstLine && shouldPadLines) {
lineFixed = " $lineFixed"
}
lineFixed = lineFixed.trimEnd().removeSuffix(".")
val isTopLevel = lineFixed.startsWith("-")
list.add(
ChangelogEntry(
trimmedLine,
lineFixed,
section,
subsection,
link.takeIf { isTopLevel }
)
)
isFirstLine = false
}
}
@ -277,7 +332,7 @@ fun GitHubPullEntry.extractReleaseNotes(link: String): List<ChangelogEntry> { @@ -277,7 +332,7 @@ fun GitHubPullEntry.extractReleaseNotes(link: String): List<ChangelogEntry> {
fun entriesForRepo(repo: String, firstCommit: String, lastCommit: String): List<ChangelogEntry> {
val pulls = (1..5)
.flatMap {
request<Array<GitHubPullEntry>>("https://api.github.com/repos/$repo/pulls?state=closed&per_page=100&page=$it").toList()
requestJson<Array<GitHubPullEntry>>("https://api.github.com/repos/$repo/pulls?state=closed&per_page=100&page=$it").toList()
}
val pullNumberToPull = pulls.associateBy { it.number }
@ -310,8 +365,7 @@ fun entriesForRepo(repo: String, firstCommit: String, lastCommit: String): List< @@ -310,8 +365,7 @@ fun entriesForRepo(repo: String, firstCommit: String, lastCommit: String): List<
fun fetchCommits(firsCommitSha: String, lastCommitSha: String): CommitsResult {
lateinit var mergeBaseCommit: String
val commits = fetchPagedUntilEmpty { page ->
val result =
request<GitHubCompareResponse>("https://api.github.com/repos/$repo/compare/$firsCommitSha...$lastCommitSha?per_page=1000&page=$page")
val result = requestJson<GitHubCompareResponse>("https://api.github.com/repos/$repo/compare/$firsCommitSha...$lastCommitSha?per_page=1000&page=$page")
mergeBaseCommit = result.merge_base_commit.sha
result.commits
}
@ -336,6 +390,55 @@ fun repoTitleAndNumberForCommit(commit: GitHubCompareResponse.CommitEntry): Pair @@ -336,6 +390,55 @@ fun repoTitleAndNumberForCommit(commit: GitHubCompareResponse.CommitEntry): Pair
return title to number
}
/**
* Extract redirecting versions from core repo, file gradle.properties
*
* Example
* https://raw.githubusercontent.com/JetBrains/compose-multiplatform-core/v1.8.0%2Bdev1966/gradle.properties
* artifactRedirecting.androidx.graphics.version=1.0.1
*/
fun androidxLibToRedirectingVersion(commit: String): Map<String, String> {
val gradleProperties = githubContentOf("JetBrains/compose-multiplatform-core", "gradle.properties", commit)
val regex = Regex("artifactRedirecting\\.androidx\\.(.*)\\.version=(.*)")
return regex.findAll(gradleProperties).associate { result ->
result.groupValues[1].trim() to result.groupValues[2].trim()
}
}
/**
* Extract versions from CI config, file .teamcity/compose/Library.kt
*
* Example
* https://jetbrains.team/p/ui/repositories/compose-teamcity-config/files/8f8408ccd05a9188895969b1fa0243050716baad/.teamcity/compose/Library.kt?tab=source&line=37&lines-count=1
* Library.CORE_BUNDLE -> "1.1.0-alpha01"
*/
fun androidxLibToVersion(commit: String): Map<String, String> {
val repo = "ssh://git@git.jetbrains.team/ui/compose-teamcity-config.git"
val file = ".teamcity/compose/Library.kt"
val libraryKt = spaceContentOf(repo, file, commit)
return if (libraryKt.isBlank()) {
println("Can't clone $repo to know library versions. Please register your ssh key in https://jetbrains.team/m/me/authentication?tab=GitKeys")
emptyMap()
} else {
val regex = Regex("Library\\.(.*)\\s*->\\s*\"(.*)\"")
return regex.findAll(libraryKt).associate { result ->
result.groupValues[1].trim() to result.groupValues[2].trim()
}
}
}
fun githubContentOf(repo: String, path: String, commit: String): String {
val commitEncoded = URLEncoder.encode(commit, UTF_8)
return requestPlain("https://raw.githubusercontent.com/$repo/$commitEncoded/$path")
}
fun spaceContentOf(repoUrl: String, path: String, tagName: String): String {
return pipeProcess("git archive --remote=$repoUrl $tagName $path")
.pipeTo("tar -xO $path")
.readText()
}
data class ChangelogEntry(
val message: String,
val section: String?,
@ -362,37 +465,25 @@ data class GitHubPullEntry(val number: Int, val title: String, val body: String? @@ -362,37 +465,25 @@ data class GitHubPullEntry(val number: Int, val title: String, val body: String?
}
//region ========================================== UTILS =========================================
// from https://stackoverflow.com/a/41495542
fun String.runCommand(workingDir: File = File(".")) {
ProcessBuilder(*split(" ").toTypedArray())
.directory(workingDir)
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.start()
.waitFor(5, TimeUnit.MINUTES)
}
fun String.execCommand(workingDir: File = File(".")): String? {
try {
val parts = this.split("\\s".toRegex())
val proc = ProcessBuilder(*parts.toTypedArray())
.directory(workingDir)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.start()
proc.waitFor(60, TimeUnit.MINUTES)
return proc.inputStream.bufferedReader().readText()
} catch (e: IOException) {
e.printStackTrace()
return null
fun pipeProcess(command: String) = ProcessBuilder(command.split(" "))
.redirectOutput(Redirect.PIPE)
.redirectError(Redirect.PIPE)
.start()!!
fun Process.pipeTo(command: String): Process = pipeProcess(command).also {
inputStream.use { input ->
it.outputStream.use { out ->
input.copyTo(out)
}
}
}
inline fun <reified T> request(
url: String
): T = exponentialRetry {
fun Process.readText(): String = inputStream.bufferedReader().use { it.readText() }
inline fun <reified T> requestJson(url: String): T =
Gson().fromJson(requestPlain(url), T::class.java)
fun requestPlain(url: String): String = exponentialRetry {
println("Request $url")
val connection = URL(url).openConnection()
connection.setRequestProperty("User-Agent", "Compose-Multiplatform-Script")
@ -400,10 +491,7 @@ inline fun <reified T> request( @@ -400,10 +491,7 @@ inline fun <reified T> request(
connection.setRequestProperty("Authorization", "Bearer $token")
}
connection.getInputStream().use {
Gson().fromJson(
it.bufferedReader(),
T::class.java
)
it.bufferedReader().readText()
}
}

Loading…
Cancel
Save