Skip to content

Commit 04b582d

Browse files
feat: auto generate release notes for next release (#996)
* Added generation of release notes * added tests * updated documentation * Delete next release notes
1 parent 487063b commit 04b582d

34 files changed

Lines changed: 688 additions & 97 deletions

.github/pull_request_template.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ Fixes #
99

1010
- [ ] Documented
1111
- [ ] Unit tested
12-
- [ ] release_notes.md updated
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: "Generate release notes for next commit"
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
generateReleaseNotes:
8+
runs-on: macos-latest
9+
10+
steps:
11+
- uses: actions/checkout@v2
12+
13+
- name: Gradle Build flankScripts and add it to PATH
14+
run: |
15+
./flank-scripts/bash/buildFlankScripts.sh
16+
echo "::add-path::./flank-scripts/bash"
17+
18+
- name: Set next release tag variable
19+
run: |
20+
TAG=$(flankScripts ci nextReleaseTag --token=${{ secrets.GITHUB_TOKEN }})
21+
echo "::set-env name=NEXT_RELEASE_TAG::$(echo $TAG)";
22+
23+
- name: Append release note
24+
run: |
25+
flankScripts ci generateReleaseNotes --token=${{ secrets.GITHUB_TOKEN }}
26+
27+
- name: Commit files and create Pull request
28+
id: pr
29+
uses: peter-evans/create-pull-request@v3
30+
with:
31+
commit-message: "[Automatic PR] Generate release notes"
32+
signoff: false
33+
branch: 'release/${{ env.NEXT_RELEASE_TAG }}'
34+
title: 'chore: release notes for ${{ env.NEXT_RELEASE_TAG }}'
35+
body: "Auto generated release notes for `${{ env.NEXT_RELEASE_TAG }}` by @${{ github.actor }}"
36+
labels: |
37+
automated pr
38+
release
39+
reviewers: bootstraponline,jan-gogo,pawelpasterz,adamfilipow92,piotradamczyk5,Sloox
40+
draft: false

flank-scripts/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,29 @@ If you need help with available commands or arguments you could always use optio
1919

2020
## Available commands and options
2121

22+
### CI
23+
To show all available commands for ci use:
24+
`flankScripts ci`
25+
26+
Available commands are:
27+
- `generateReleaseNotes` Command to generate release notes and append them to `release_notes.md`
28+
- `nextReleaseTag` Print next release tag
29+
30+
#### `generateReleaseNotes`
31+
Command to generate release notes and append them to `release_notes.md`
32+
33+
| Option | Description |
34+
|---------------------- |--------------------------------------------------------- |
35+
| --token | Git token |
36+
| --release-notes-file | Path to release_notes.md (default `./release_notes.md`) |
37+
38+
#### `nextReleaseTag`
39+
Print next release tag
40+
41+
| Option | Description |
42+
|----------------- |----------------- |
43+
| `--token` | Git token |
44+
2245
### Release
2346
The release process was described in [document](../docs/release_process.md).
2447
To show all available commands for release use:

flank-scripts/src/main/kotlin/flank/scripts/Main.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package flank.scripts
22

33
import com.github.ajalt.clikt.core.CliktCommand
44
import com.github.ajalt.clikt.core.subcommands
5+
import flank.scripts.ci.CiCommand
56
import flank.scripts.release.ReleaseCommand
67

78
class Main : CliktCommand(name = "flankScripts") {
@@ -10,6 +11,7 @@ class Main : CliktCommand(name = "flankScripts") {
1011
}
1112

1213
fun main(args: Array<String>) {
13-
Main().subcommands(ReleaseCommand())
14-
.main(args)
14+
Main()
15+
.subcommands(ReleaseCommand(), CiCommand())
16+
.main(args)
1517
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package flank.scripts.ci
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.core.subcommands
5+
import flank.scripts.ci.nexttag.NextReleaseTagCommand
6+
import flank.scripts.ci.releasenotes.GenerateReleaseNotesCommand
7+
8+
class CiCommand : CliktCommand(help = "Contains command related to CI", name = "ci") {
9+
10+
init {
11+
subcommands(GenerateReleaseNotesCommand(), NextReleaseTagCommand())
12+
}
13+
14+
@Suppress("EmptyFunctionBlock")
15+
override fun run() {}
16+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package flank.scripts.ci.nexttag
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.parameters.options.option
5+
import com.github.ajalt.clikt.parameters.options.required
6+
import com.github.kittinunf.result.Result
7+
import flank.scripts.github.getLatestReleaseTag
8+
import kotlin.system.exitProcess
9+
import kotlinx.coroutines.runBlocking
10+
11+
class NextReleaseTagCommand : CliktCommand(help = "Print next release tag", name = "nextReleaseTag") {
12+
13+
private val token by option(help = "Git Token").required()
14+
15+
override fun run() {
16+
runBlocking {
17+
getNextReleaseTagCommand(token)
18+
}
19+
}
20+
}
21+
22+
private suspend fun getNextReleaseTagCommand(token: String) {
23+
when (val result = getLatestReleaseTag(token)) {
24+
is Result.Success -> println(generateNextReleaseTag(result.value.tag))
25+
is Result.Failure -> {
26+
println(result.error)
27+
exitProcess(1)
28+
}
29+
}
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package flank.scripts.ci.nexttag
2+
3+
import java.time.LocalDate
4+
import java.time.Year
5+
import java.time.format.DateTimeFormatter
6+
7+
fun generateNextReleaseTag(previousReleaseTag: String): String {
8+
val (year, month, number) = previousReleaseTag.trimStart('v').split('.')
9+
return if (isNextReleaseInCurrentMonth(
10+
year,
11+
month
12+
)
13+
) "v$year.$month.${number.toInt() + 1}" else currentMonthFirstTag()
14+
}
15+
16+
private fun isNextReleaseInCurrentMonth(year: String, month: String) =
17+
DateTimeFormatter.ofPattern("yy").format(Year.now()) == year &&
18+
DateTimeFormatter.ofPattern("MM").format(LocalDate.now()) == month
19+
20+
private fun currentMonthFirstTag() = "v${DateTimeFormatter.ofPattern("yy.MM").format(LocalDate.now())}.0"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package flank.scripts.ci.releasenotes
2+
3+
import flank.scripts.utils.markdownH2
4+
import java.io.File
5+
6+
fun File.appendReleaseNotes(messages: List<String>, releaseTag: String) {
7+
appendToReleaseNotes(
8+
messages = messages,
9+
releaseTag = releaseTag
10+
)
11+
}
12+
13+
private fun File.appendToReleaseNotes(messages: List<String>, releaseTag: String) {
14+
val currentFileLines = readLines()
15+
val newLines = mutableListOf<String>().apply {
16+
add(releaseTag.markdownH2())
17+
addAll(messages)
18+
add("")
19+
}
20+
21+
writeText((newLines + currentFileLines).joinToString(System.lineSeparator()).withNewLineAtTheEnd())
22+
}
23+
24+
private fun String.withNewLineAtTheEnd() = plus(System.lineSeparator())
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package flank.scripts.ci.releasenotes
2+
3+
import flank.scripts.utils.markdownBold
4+
5+
fun String.mapPrTitle() = when {
6+
startsWith("feat") -> replaceFirst("feat", "New feature".markdownBold())
7+
startsWith("fix") -> replaceFirst("fix", "Fix".markdownBold())
8+
startsWith("docs") -> replaceFirst("docs", "Documentation".markdownBold())
9+
startsWith("refactor") -> replaceFirst("refactor", "Refactor".markdownBold())
10+
startsWith("ci") -> replaceFirst("ci", "CI changes".markdownBold())
11+
startsWith("test") -> replaceFirst("test", "Tests update".markdownBold())
12+
startsWith("perf") -> replaceFirst("perf", "Performance upgrade".markdownBold())
13+
else -> null // we do not accept other prefix to have update in release notes
14+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package flank.scripts.ci.releasenotes
2+
3+
import com.github.kittinunf.result.Result
4+
import flank.scripts.github.getPrDetailsByCommit
5+
import flank.scripts.utils.markdownLink
6+
import flank.scripts.utils.runCommand
7+
import java.io.File
8+
import kotlinx.coroutines.async
9+
import kotlinx.coroutines.awaitAll
10+
import kotlinx.coroutines.runBlocking
11+
12+
fun generateReleaseNotes(latestReleaseTag: String, githubToken: String) = getCommitsSha(latestReleaseTag).getReleaseNotes(githubToken)
13+
14+
internal fun getCommitsSha(fromTag: String): List<String> {
15+
val outputFile = File.createTempFile("sha", ".log")
16+
17+
"git log --pretty=%H $fromTag..HEAD".runCommand(fileForOutput = outputFile)
18+
19+
return outputFile.readLines()
20+
}
21+
22+
private fun List<String>.getReleaseNotes(githubToken: String) = runBlocking {
23+
map { sha -> async { getPrDetailsByCommit(sha, githubToken) } }
24+
.awaitAll()
25+
.filterIsInstance<Result.Success<List<GithubPullRequest>>>()
26+
.map { it.value }
27+
.filter { it.isNotEmpty() }
28+
.mapNotNull { it.first().toReleaseNoteMessage() }
29+
}
30+
31+
private fun GithubPullRequest.toReleaseNoteMessage() =
32+
title.mapPrTitle()?.let { title ->
33+
"- ${markdownLink(number.toString(), htmlUrl)} $title ${assignees.format()}"
34+
}
35+
36+
private fun List<GithubUser>.format() = "(${joinToString { (login, url) -> markdownLink(login, url) }})"

0 commit comments

Comments
 (0)