Skip to content

Commit 704962e

Browse files
authored
Merge pull request #34 from domaframework/ci/changelog-update-action
Add: Update CHANGELOG.md Action
2 parents 170f573 + 0a85bec commit 704962e

File tree

3 files changed

+371
-0
lines changed

3 files changed

+371
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Update Changelog PR
2+
3+
on:
4+
push:
5+
branches:
6+
- 'main'
7+
- 'releases/**'
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: write
12+
pull-requests: write
13+
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
update-changelog:
20+
runs-on: ubuntu-latest
21+
if: startsWith(github.event.head_commit.message, 'Merge pull request')
22+
steps:
23+
24+
- name: Checkout repository
25+
uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
ref: ${{ github.event.release.tag_name }}
29+
30+
- name: Setup Java
31+
uses: actions/setup-java@v4
32+
with:
33+
distribution: zulu
34+
java-version: 17
35+
36+
# Setup Gradle
37+
- name: Setup Gradle
38+
uses: gradle/actions/setup-gradle@v4
39+
40+
- name: Run Gradle updateChangelog
41+
run: |
42+
./gradlew updateChangelog -PreleaseDate=$(date +'%Y-%m-%d')
43+
env:
44+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45+
46+
- name: Run Gradle checkExistChangelogPullRequest
47+
run: |
48+
./gradlew checkExistChangelogPullRequest -PnewBranch=$BRANCH
49+
env:
50+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51+
52+
- name: Push ChangeLog
53+
env:
54+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55+
run: |
56+
git config user.email "[email protected]"
57+
git config user.name "GitHub Action"
58+
git checkout -b $BRANCH
59+
git add CHANGELOG.md
60+
git commit -am "Changelog update - $NEW_VERSION"
61+
git push --set-upstream --force-with-lease origin $BRANCH
62+
63+
- name: Create Pull Request
64+
env:
65+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66+
run: |
67+
if ${{ env.EXIST_CHANGELOG == 'false' }} ; then
68+
gh label create "$LABEL" \
69+
--description "Pull requests with release changelog update" \
70+
--force \
71+
|| true
72+
73+
gh pr create \
74+
--title "Changelog update - \`$NEW_VERSION\`" \
75+
--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." \
76+
--label changelog,skip-changelog \
77+
--head $BRANCH \
78+
--draft
79+
fi

build.gradle.kts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
2+
import com.fasterxml.jackson.annotation.JsonProperty
3+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
14
import org.gradle.internal.classpath.Instrumented.systemProperty
25
import org.jetbrains.changelog.Changelog
36
import org.jetbrains.changelog.markdownToHTML
47
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
513
import java.util.Base64
614

715
plugins {
@@ -68,6 +76,7 @@ sourceSets {
6876
dependencies {
6977
implementation(libs.slf4j)
7078
implementation(libs.logback)
79+
implementation(libs.jackson)
7180

7281
testImplementation(libs.junit)
7382
testImplementation(libs.kotlinTest)
@@ -190,6 +199,288 @@ tasks.register("encodeBase64") {
190199
}
191200
}
192201

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+
193484
intellijPlatformTesting {
194485
runIde {
195486
register("runIdeForUiTests") {

0 commit comments

Comments
 (0)