|
| 1 | +import com.fasterxml.jackson.annotation.JsonIgnoreProperties |
| 2 | +import com.fasterxml.jackson.annotation.JsonProperty |
| 3 | +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper |
1 | 4 | import org.gradle.internal.classpath.Instrumented.systemProperty |
2 | 5 | import org.jetbrains.changelog.Changelog |
3 | 6 | import org.jetbrains.changelog.markdownToHTML |
4 | 7 | import org.jetbrains.intellij.platform.gradle.TestFrameworkType |
| 8 | +import java.net.URL |
| 9 | +import java.time.LocalDate |
| 10 | +import java.time.OffsetDateTime |
| 11 | +import java.time.ZoneOffset |
| 12 | +import java.time.format.DateTimeFormatter |
5 | 13 | import java.util.Base64 |
6 | 14 |
|
7 | 15 | plugins { |
@@ -68,6 +76,7 @@ sourceSets { |
68 | 76 | dependencies { |
69 | 77 | implementation(libs.slf4j) |
70 | 78 | implementation(libs.logback) |
| 79 | + implementation(libs.jackson) |
71 | 80 |
|
72 | 81 | testImplementation(libs.junit) |
73 | 82 | testImplementation(libs.kotlinTest) |
@@ -190,6 +199,288 @@ tasks.register("encodeBase64") { |
190 | 199 | } |
191 | 200 | } |
192 | 201 |
|
| 202 | +tasks.register("updateChangelog") { |
| 203 | + group = "changelog" |
| 204 | + description = "Update CHANGELOG.md based on merged PRs since last release" |
| 205 | + |
| 206 | + @JsonIgnoreProperties(ignoreUnknown = true) |
| 207 | + data class Label( |
| 208 | + val id: Long = 0, |
| 209 | + val name: String = "", |
| 210 | + val color: String = "", |
| 211 | + val description: String = "", |
| 212 | + ) |
| 213 | + |
| 214 | + @JsonIgnoreProperties(ignoreUnknown = true) |
| 215 | + data class PullRequestItem( |
| 216 | + val title: String = "", |
| 217 | + @JsonProperty("html_url") |
| 218 | + val url: String = "", |
| 219 | + val number: Long = 0, |
| 220 | + var labelItems: List<String> = emptyList(), |
| 221 | + ) |
| 222 | + |
| 223 | + class VersionInfo( |
| 224 | + lastversion: String = "", |
| 225 | + ) { |
| 226 | + var lastMajor: Int = 0 |
| 227 | + var lastMinor: Int = 0 |
| 228 | + var lastPatch: Int = 0 |
| 229 | + |
| 230 | + var major: Int = 0 |
| 231 | + var minor: Int = 0 |
| 232 | + var patch: Int = 0 |
| 233 | + |
| 234 | + init { |
| 235 | + val lastVersions = lastversion.substringAfter("v").split(".") |
| 236 | + lastMajor = lastVersions[0].toInt() |
| 237 | + lastMinor = lastVersions[1].toInt() |
| 238 | + lastPatch = lastVersions[2].toInt() |
| 239 | + |
| 240 | + major = lastMajor |
| 241 | + minor = lastMinor |
| 242 | + patch = lastPatch |
| 243 | + } |
| 244 | + |
| 245 | + fun updateMajor() { |
| 246 | + if (major == lastMajor) { |
| 247 | + major++ |
| 248 | + minor = 0 |
| 249 | + patch = 0 |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + fun updateMinor() { |
| 254 | + if (minor == lastMinor && major == lastMajor) { |
| 255 | + minor++ |
| 256 | + patch = 0 |
| 257 | + } |
| 258 | + } |
| 259 | + |
| 260 | + fun updatePatch() { |
| 261 | + if (minor == lastMinor && major == lastMajor && patch == lastPatch) { |
| 262 | + patch++ |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | + fun getNewVersion() = "$major.$minor.$patch" |
| 267 | + } |
| 268 | + |
| 269 | + val releaseDate = |
| 270 | + if (project.hasProperty("releaseDate")) { |
| 271 | + project.property("releaseDate") as String |
| 272 | + } else { |
| 273 | + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) |
| 274 | + } |
| 275 | + val changelogFile = project.file("CHANGELOG.md") |
| 276 | + val rootDir = project.rootDir |
| 277 | + |
| 278 | + @Suppress("UNCHECKED_CAST") |
| 279 | + doLast { |
| 280 | + fun runCommand(command: String): String { |
| 281 | + val parts = command.split(" ") |
| 282 | + return ProcessBuilder(parts) |
| 283 | + .directory(rootDir) |
| 284 | + .redirectErrorStream(true) |
| 285 | + .start() |
| 286 | + .inputStream |
| 287 | + .bufferedReader() |
| 288 | + .readText() |
| 289 | + .trim() |
| 290 | + } |
| 291 | + |
| 292 | + val tagsOutput = runCommand("git tag --sort=-v:refname") |
| 293 | + val semverRegex = Regex("^v\\d+\\.\\d+\\.\\d+$") |
| 294 | + val tags = tagsOutput.lines().filter { semverRegex.matches(it) } |
| 295 | + if (tags.isEmpty()) { |
| 296 | + throw GradleException("Not Found Release Tag") |
| 297 | + } |
| 298 | + val lastTag = tags.first() |
| 299 | + println("Last release tag: $lastTag") |
| 300 | + |
| 301 | + val lastReleaseCommitDate = runCommand("git log -1 --format=%cI $lastTag").trim() |
| 302 | + val offsetTime = OffsetDateTime.parse(lastReleaseCommitDate) |
| 303 | + val lastReleaseCommitDateUtc = offsetTime.withOffsetSameInstant(ZoneOffset.UTC) |
| 304 | + println("Last release commit date: $lastReleaseCommitDateUtc") |
| 305 | + |
| 306 | + val githubToken = System.getenv("GITHUB_TOKEN") ?: throw GradleException("Not Setting GITHUB_TOKEN") |
| 307 | + val repo = System.getenv("GITHUB_REPOSITORY") ?: throw GradleException("Not Setting GITHUB_REPOSITORY") |
| 308 | + |
| 309 | + // https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests |
| 310 | + val apiPath = "https://api.github.com/search/issues" |
| 311 | + val sort = "sort:updated" |
| 312 | + val status = "is:closed+is:merged" |
| 313 | + val type = "type:pr" |
| 314 | + val base = "+base:main" |
| 315 | + val apiUrl = "$apiPath?q=repo:$repo+$type+$status+$sort+$base+merged:>$lastReleaseCommitDateUtc" |
| 316 | + val connection = |
| 317 | + URL(apiUrl).openConnection().apply { |
| 318 | + setRequestProperty("Authorization", "token $githubToken") |
| 319 | + setRequestProperty("Accept", "application/vnd.github.v3+json") |
| 320 | + } |
| 321 | + val response = connection.getInputStream().bufferedReader().readText() |
| 322 | + val mapper = jacksonObjectMapper() |
| 323 | + val json: Map<String, Any> = |
| 324 | + ( |
| 325 | + mapper.readValue(response, Map::class.java) as? Map<String, Any> |
| 326 | + ?: emptyList<Map<String, Any>>() |
| 327 | + ) as Map<String, Any> |
| 328 | + val items = |
| 329 | + (json["items"] as List<*>) |
| 330 | + .mapNotNull { item -> |
| 331 | + mapper.convertValue(item, Map::class.java) as Map<String, Any> |
| 332 | + } |
| 333 | + |
| 334 | + val prList = |
| 335 | + items.mapNotNull { pr -> |
| 336 | + val labelTemps = pr["labels"] as List<Map<String, Any>> |
| 337 | + val labels = |
| 338 | + labelTemps |
| 339 | + .mapNotNull { mapper.convertValue(it, Label::class.java) } |
| 340 | + .map { it.name } |
| 341 | + mapper.convertValue(pr, PullRequestItem::class.java)?.apply { |
| 342 | + labelItems = labels |
| 343 | + } |
| 344 | + } |
| 345 | + |
| 346 | + val categories = |
| 347 | + mapOf( |
| 348 | + "New Features" to listOf("feature", "enhancement"), |
| 349 | + "Bug Fixes" to listOf("fix", "bug", "bugfix"), |
| 350 | + "Maintenance" to listOf("ci", "chore", "perf", "refactor", "test", "security"), |
| 351 | + "Documentation" to listOf("doc"), |
| 352 | + "Dependency Updates" to listOf("dependencies"), |
| 353 | + ) |
| 354 | + |
| 355 | + val versionUpLabels = |
| 356 | + mapOf( |
| 357 | + "major" to listOf("major"), |
| 358 | + "minor" to listOf("minor", "feature", "enhancement"), |
| 359 | + "patch" to listOf("patch"), |
| 360 | + ) |
| 361 | + |
| 362 | + val categorized: MutableMap<String, MutableList<PullRequestItem>> = |
| 363 | + mutableMapOf( |
| 364 | + "New Features" to mutableListOf(), |
| 365 | + "Bug Fixes" to mutableListOf(), |
| 366 | + "Maintenance" to mutableListOf(), |
| 367 | + "Documentation" to mutableListOf(), |
| 368 | + "Dependency Updates" to mutableListOf(), |
| 369 | + "Other" to mutableListOf(), |
| 370 | + ) |
| 371 | + |
| 372 | + val versionInfo = VersionInfo(lastTag) |
| 373 | + var assigned: Boolean |
| 374 | + |
| 375 | + prList.forEach { pr -> |
| 376 | + assigned = false |
| 377 | + categories.forEach { (category, catLabels) -> |
| 378 | + val prLabels = pr.labelItems |
| 379 | + if (prLabels.any { it in catLabels }) { |
| 380 | + categorized[category]?.add(pr) |
| 381 | + versionUpLabels.forEach { (version, versionUpLabels) -> |
| 382 | + if (prLabels.any { it in versionUpLabels }) { |
| 383 | + assigned = true |
| 384 | + when (version) { |
| 385 | + "major" -> versionInfo.updateMajor() |
| 386 | + |
| 387 | + "minor" -> versionInfo.updateMinor() |
| 388 | + |
| 389 | + "patch" -> versionInfo.updatePatch() |
| 390 | + } |
| 391 | + } |
| 392 | + } |
| 393 | + } |
| 394 | + if (!assigned) { |
| 395 | + versionInfo.updatePatch() |
| 396 | + categorized["Other"]?.add(pr) |
| 397 | + } |
| 398 | + } |
| 399 | + } |
| 400 | + |
| 401 | + val newVersion = versionInfo.getNewVersion() |
| 402 | + val prLinks = mutableListOf<String>() |
| 403 | + val newEntry = StringBuilder() |
| 404 | + |
| 405 | + newEntry.append("## [$newVersion] - $releaseDate\n\n") |
| 406 | + categories.keys.forEach { category -> |
| 407 | + val hitItems = categorized[category] |
| 408 | + if (!hitItems.isNullOrEmpty()) { |
| 409 | + newEntry.append("### $category\n\n") |
| 410 | + hitItems.forEach { title -> |
| 411 | + newEntry.append("- ${title.title} ([#${title.number}])\n") |
| 412 | + prLinks.add("[#${title.number}]:${title.url}") |
| 413 | + } |
| 414 | + newEntry.append("\n") |
| 415 | + } |
| 416 | + } |
| 417 | + |
| 418 | + prLinks.forEach { link -> newEntry.append("$link\n") } |
| 419 | + |
| 420 | + val currentContent = if (changelogFile.exists()) changelogFile.readText() else "\n" |
| 421 | + val updatedContent = |
| 422 | + if (currentContent.contains("## [Unreleased]")) { |
| 423 | + currentContent.replace("## [Unreleased]", "## [Unreleased]\n\n$newEntry") |
| 424 | + } else { |
| 425 | + "## [Unreleased]\n\n$newEntry$currentContent" |
| 426 | + } |
| 427 | + val repoUrl = "https://github.com/domaframework/doma-tools-for-intellij" |
| 428 | + changelogFile.writeText(updatedContent) |
| 429 | + changelogFile.appendText("[$newVersion]: $repoUrl/compare/$lastTag...v$newVersion\n") |
| 430 | + |
| 431 | + val githubEnv = System.getenv("GITHUB_ENV") |
| 432 | + val envFile = File(githubEnv) |
| 433 | + envFile.appendText("NEW_VERSION=$newVersion\n") |
| 434 | + envFile.appendText("BRANCH=doc/changelog-update-$newVersion\n") |
| 435 | + |
| 436 | + println("Update CHANGELOG.md :newVersion $newVersion") |
| 437 | + } |
| 438 | +} |
| 439 | + |
| 440 | +tasks.register("checkExistChangelogPullRequest") { |
| 441 | + group = "changelog" |
| 442 | + description = "Check if a PR with the same name has already been created" |
| 443 | + |
| 444 | + val newBranch = |
| 445 | + if (project.hasProperty("newBranch")) { |
| 446 | + project.property("newBranch") as String |
| 447 | + } else { |
| 448 | + "doc/changelog-update" |
| 449 | + } |
| 450 | + |
| 451 | + @Suppress("UNCHECKED_CAST") |
| 452 | + doLast { |
| 453 | + println("Check PR with the same name has already been created $newBranch") |
| 454 | + |
| 455 | + val githubToken = System.getenv("GITHUB_TOKEN") ?: throw GradleException("Not Setting GITHUB_TOKEN") |
| 456 | + val repo = System.getenv("GITHUB_REPOSITORY") ?: throw GradleException("Not Setting GITHUB_REPOSITORY") |
| 457 | + |
| 458 | + // https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests |
| 459 | + val apiPath = "https://api.github.com/search/issues" |
| 460 | + val status = "is:open" |
| 461 | + val label = "label:changelog,skip-changelog" |
| 462 | + val branch = "base:main+head:$newBranch" |
| 463 | + val apiUrl = "$apiPath?q=repo:$repo+is:pr+$branch+$label+$status" |
| 464 | + val connection = |
| 465 | + URL(apiUrl).openConnection().apply { |
| 466 | + setRequestProperty("Authorization", "token $githubToken") |
| 467 | + setRequestProperty("Accept", "application/vnd.github.v3+json") |
| 468 | + } |
| 469 | + val response = connection.getInputStream().bufferedReader().readText() |
| 470 | + val mapper = jacksonObjectMapper() |
| 471 | + val json: Map<String, Any> = |
| 472 | + ( |
| 473 | + mapper.readValue(response, Map::class.java) as? Map<String, Any> |
| 474 | + ?: emptyList<Map<String, Any>>() |
| 475 | + ) as Map<String, Any> |
| 476 | + println("get response Json ${json["total_count"]}") |
| 477 | + val existChangelogPr = json["total_count"] != 0 |
| 478 | + |
| 479 | + val githubEnv = System.getenv("GITHUB_ENV") |
| 480 | + File(githubEnv).appendText("EXIST_CHANGELOG=$existChangelogPr\n") |
| 481 | + } |
| 482 | +} |
| 483 | + |
193 | 484 | intellijPlatformTesting { |
194 | 485 | runIde { |
195 | 486 | register("runIdeForUiTests") { |
|
0 commit comments