From 0a85beccee76aa44b1f289fa97cd149f5ebd4308 Mon Sep 17 00:00:00 2001 From: xterao Date: Tue, 11 Mar 2025 13:04:31 +0900 Subject: [PATCH] Add: Update CHANGELOG.md Action --- .github/workflows/update_changelog.yml | 79 +++++++ build.gradle.kts | 291 +++++++++++++++++++++++++ gradle/libs.versions.toml | 1 + 3 files changed, 371 insertions(+) create mode 100644 .github/workflows/update_changelog.yml diff --git a/.github/workflows/update_changelog.yml b/.github/workflows/update_changelog.yml new file mode 100644 index 00000000..48b90a26 --- /dev/null +++ b/.github/workflows/update_changelog.yml @@ -0,0 +1,79 @@ +name: Update Changelog PR + +on: + push: + branches: + - 'main' + - 'releases/**' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + update-changelog: + runs-on: ubuntu-latest + if: startsWith(github.event.head_commit.message, 'Merge pull request') + steps: + + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.release.tag_name }} + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run Gradle updateChangelog + run: | + ./gradlew updateChangelog -PreleaseDate=$(date +'%Y-%m-%d') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Gradle checkExistChangelogPullRequest + run: | + ./gradlew checkExistChangelogPullRequest -PnewBranch=$BRANCH + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Push ChangeLog + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.email "action@github.com" + git config user.name "GitHub Action" + git checkout -b $BRANCH + git add CHANGELOG.md + git commit -am "Changelog update - $NEW_VERSION" + git push --set-upstream --force-with-lease origin $BRANCH + + - name: Create Pull Request + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if ${{ env.EXIST_CHANGELOG == 'false' }} ; then + gh label create "$LABEL" \ + --description "Pull requests with release changelog update" \ + --force \ + || true + + gh pr create \ + --title "Changelog update - \`$NEW_VERSION\`" \ + --body "Current pull request contains patched \`CHANGELOG.md\` for the \`$NEW_VERSION\` version.Please merge this Pull Request once you have completed all the changes you want included in the latest version." \ + --label changelog,skip-changelog \ + --head $BRANCH \ + --draft + fi \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8ecc6a7a..01f5c08a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,15 @@ +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.gradle.internal.classpath.Instrumented.systemProperty import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import java.net.URL +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter import java.util.Base64 plugins { @@ -68,6 +76,7 @@ sourceSets { dependencies { implementation(libs.slf4j) implementation(libs.logback) + implementation(libs.jackson) testImplementation(libs.junit) testImplementation(libs.kotlinTest) @@ -190,6 +199,288 @@ tasks.register("encodeBase64") { } } +tasks.register("updateChangelog") { + group = "changelog" + description = "Update CHANGELOG.md based on merged PRs since last release" + + @JsonIgnoreProperties(ignoreUnknown = true) + data class Label( + val id: Long = 0, + val name: String = "", + val color: String = "", + val description: String = "", + ) + + @JsonIgnoreProperties(ignoreUnknown = true) + data class PullRequestItem( + val title: String = "", + @JsonProperty("html_url") + val url: String = "", + val number: Long = 0, + var labelItems: List = emptyList(), + ) + + class VersionInfo( + lastversion: String = "", + ) { + var lastMajor: Int = 0 + var lastMinor: Int = 0 + var lastPatch: Int = 0 + + var major: Int = 0 + var minor: Int = 0 + var patch: Int = 0 + + init { + val lastVersions = lastversion.substringAfter("v").split(".") + lastMajor = lastVersions[0].toInt() + lastMinor = lastVersions[1].toInt() + lastPatch = lastVersions[2].toInt() + + major = lastMajor + minor = lastMinor + patch = lastPatch + } + + fun updateMajor() { + if (major == lastMajor) { + major++ + minor = 0 + patch = 0 + } + } + + fun updateMinor() { + if (minor == lastMinor && major == lastMajor) { + minor++ + patch = 0 + } + } + + fun updatePatch() { + if (minor == lastMinor && major == lastMajor && patch == lastPatch) { + patch++ + } + } + + fun getNewVersion() = "$major.$minor.$patch" + } + + val releaseDate = + if (project.hasProperty("releaseDate")) { + project.property("releaseDate") as String + } else { + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + } + val changelogFile = project.file("CHANGELOG.md") + val rootDir = project.rootDir + + @Suppress("UNCHECKED_CAST") + doLast { + fun runCommand(command: String): String { + val parts = command.split(" ") + return ProcessBuilder(parts) + .directory(rootDir) + .redirectErrorStream(true) + .start() + .inputStream + .bufferedReader() + .readText() + .trim() + } + + val tagsOutput = runCommand("git tag --sort=-v:refname") + val semverRegex = Regex("^v\\d+\\.\\d+\\.\\d+$") + val tags = tagsOutput.lines().filter { semverRegex.matches(it) } + if (tags.isEmpty()) { + throw GradleException("Not Found Release Tag") + } + val lastTag = tags.first() + println("Last release tag: $lastTag") + + val lastReleaseCommitDate = runCommand("git log -1 --format=%cI $lastTag").trim() + val offsetTime = OffsetDateTime.parse(lastReleaseCommitDate) + val lastReleaseCommitDateUtc = offsetTime.withOffsetSameInstant(ZoneOffset.UTC) + println("Last release commit date: $lastReleaseCommitDateUtc") + + val githubToken = System.getenv("GITHUB_TOKEN") ?: throw GradleException("Not Setting GITHUB_TOKEN") + val repo = System.getenv("GITHUB_REPOSITORY") ?: throw GradleException("Not Setting GITHUB_REPOSITORY") + + // https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests + val apiPath = "https://api.github.com/search/issues" + val sort = "sort:updated" + val status = "is:closed+is:merged" + val type = "type:pr" + val base = "+base:main" + val apiUrl = "$apiPath?q=repo:$repo+$type+$status+$sort+$base+merged:>$lastReleaseCommitDateUtc" + val connection = + URL(apiUrl).openConnection().apply { + setRequestProperty("Authorization", "token $githubToken") + setRequestProperty("Accept", "application/vnd.github.v3+json") + } + val response = connection.getInputStream().bufferedReader().readText() + val mapper = jacksonObjectMapper() + val json: Map = + ( + mapper.readValue(response, Map::class.java) as? Map + ?: emptyList>() + ) as Map + val items = + (json["items"] as List<*>) + .mapNotNull { item -> + mapper.convertValue(item, Map::class.java) as Map + } + + val prList = + items.mapNotNull { pr -> + val labelTemps = pr["labels"] as List> + val labels = + labelTemps + .mapNotNull { mapper.convertValue(it, Label::class.java) } + .map { it.name } + mapper.convertValue(pr, PullRequestItem::class.java)?.apply { + labelItems = labels + } + } + + val categories = + mapOf( + "New Features" to listOf("feature", "enhancement"), + "Bug Fixes" to listOf("fix", "bug", "bugfix"), + "Maintenance" to listOf("ci", "chore", "perf", "refactor", "test", "security"), + "Documentation" to listOf("doc"), + "Dependency Updates" to listOf("dependencies"), + ) + + val versionUpLabels = + mapOf( + "major" to listOf("major"), + "minor" to listOf("minor", "feature", "enhancement"), + "patch" to listOf("patch"), + ) + + val categorized: MutableMap> = + mutableMapOf( + "New Features" to mutableListOf(), + "Bug Fixes" to mutableListOf(), + "Maintenance" to mutableListOf(), + "Documentation" to mutableListOf(), + "Dependency Updates" to mutableListOf(), + "Other" to mutableListOf(), + ) + + val versionInfo = VersionInfo(lastTag) + var assigned: Boolean + + prList.forEach { pr -> + assigned = false + categories.forEach { (category, catLabels) -> + val prLabels = pr.labelItems + if (prLabels.any { it in catLabels }) { + categorized[category]?.add(pr) + versionUpLabels.forEach { (version, versionUpLabels) -> + if (prLabels.any { it in versionUpLabels }) { + assigned = true + when (version) { + "major" -> versionInfo.updateMajor() + + "minor" -> versionInfo.updateMinor() + + "patch" -> versionInfo.updatePatch() + } + } + } + } + if (!assigned) { + versionInfo.updatePatch() + categorized["Other"]?.add(pr) + } + } + } + + val newVersion = versionInfo.getNewVersion() + val prLinks = mutableListOf() + val newEntry = StringBuilder() + + newEntry.append("## [$newVersion] - $releaseDate\n\n") + categories.keys.forEach { category -> + val hitItems = categorized[category] + if (!hitItems.isNullOrEmpty()) { + newEntry.append("### $category\n\n") + hitItems.forEach { title -> + newEntry.append("- ${title.title} ([#${title.number}])\n") + prLinks.add("[#${title.number}]:${title.url}") + } + newEntry.append("\n") + } + } + + prLinks.forEach { link -> newEntry.append("$link\n") } + + val currentContent = if (changelogFile.exists()) changelogFile.readText() else "\n" + val updatedContent = + if (currentContent.contains("## [Unreleased]")) { + currentContent.replace("## [Unreleased]", "## [Unreleased]\n\n$newEntry") + } else { + "## [Unreleased]\n\n$newEntry$currentContent" + } + val repoUrl = "https://github.com/domaframework/doma-tools-for-intellij" + changelogFile.writeText(updatedContent) + changelogFile.appendText("[$newVersion]: $repoUrl/compare/$lastTag...v$newVersion\n") + + val githubEnv = System.getenv("GITHUB_ENV") + val envFile = File(githubEnv) + envFile.appendText("NEW_VERSION=$newVersion\n") + envFile.appendText("BRANCH=doc/changelog-update-$newVersion\n") + + println("Update CHANGELOG.md :newVersion $newVersion") + } +} + +tasks.register("checkExistChangelogPullRequest") { + group = "changelog" + description = "Check if a PR with the same name has already been created" + + val newBranch = + if (project.hasProperty("newBranch")) { + project.property("newBranch") as String + } else { + "doc/changelog-update" + } + + @Suppress("UNCHECKED_CAST") + doLast { + println("Check PR with the same name has already been created $newBranch") + + val githubToken = System.getenv("GITHUB_TOKEN") ?: throw GradleException("Not Setting GITHUB_TOKEN") + val repo = System.getenv("GITHUB_REPOSITORY") ?: throw GradleException("Not Setting GITHUB_REPOSITORY") + + // https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests + val apiPath = "https://api.github.com/search/issues" + val status = "is:open" + val label = "label:changelog,skip-changelog" + val branch = "base:main+head:$newBranch" + val apiUrl = "$apiPath?q=repo:$repo+is:pr+$branch+$label+$status" + val connection = + URL(apiUrl).openConnection().apply { + setRequestProperty("Authorization", "token $githubToken") + setRequestProperty("Accept", "application/vnd.github.v3+json") + } + val response = connection.getInputStream().bufferedReader().readText() + val mapper = jacksonObjectMapper() + val json: Map = + ( + mapper.readValue(response, Map::class.java) as? Map + ?: emptyList>() + ) as Map + println("get response Json ${json["total_count"]}") + val existChangelogPr = json["total_count"] != 0 + + val githubEnv = System.getenv("GITHUB_ENV") + File(githubEnv).appendText("EXIST_CHANGELOG=$existChangelogPr\n") + } +} + intellijPlatformTesting { runIde { register("runIdeForUiTests") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f5899c6..0f0db0bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ kotlinTest = { group = "org.jetbrains.kotlin", name = "kotlin-test" } domacore = { module = "org.seasar.doma:doma-core", version.ref = "doma" } google-java-format = { module = "com.google.googlejavaformat:google-java-format", version = "1.25.2" } ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version = "1.5.0" } +jackson = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version = "2.18.3" } [plugins] changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }