Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto generate release notes for next release #996

Merged
merged 4 commits into from
Aug 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ Fixes #

- [ ] Documented
- [ ] Unit tested
- [ ] release_notes.md updated
40 changes: 40 additions & 0 deletions .github/workflows/release_notes_generation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: "Generate release notes for next commit"

on:
workflow_dispatch:

jobs:
generateReleaseNotes:
runs-on: macos-latest

steps:
- uses: actions/checkout@v2

- name: Gradle Build flankScripts and add it to PATH
run: |
./flank-scripts/bash/buildFlankScripts.sh
echo "::add-path::./flank-scripts/bash"

- name: Set next release tag variable
run: |
TAG=$(flankScripts ci nextReleaseTag --token=${{ secrets.GITHUB_TOKEN }})
echo "::set-env name=NEXT_RELEASE_TAG::$(echo $TAG)";

- name: Append release note
run: |
flankScripts ci generateReleaseNotes --token=${{ secrets.GITHUB_TOKEN }}

- name: Commit files and create Pull request
id: pr
uses: peter-evans/create-pull-request@v3
with:
commit-message: "[Automatic PR] Generate release notes"
signoff: false
branch: 'release/${{ env.NEXT_RELEASE_TAG }}'
title: 'chore: release notes for ${{ env.NEXT_RELEASE_TAG }}'
body: "Auto generated release notes for `${{ env.NEXT_RELEASE_TAG }}` by @${{ github.actor }}"
labels: |
automated pr
release
reviewers: bootstraponline,jan-gogo,pawelpasterz,adamfilipow92,piotradamczyk5,Sloox
draft: false
23 changes: 23 additions & 0 deletions flank-scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@ If you need help with available commands or arguments you could always use optio

## Available commands and options

### CI
To show all available commands for ci use:
`flankScripts ci`

Available commands are:
- `generateReleaseNotes` Command to generate release notes and append them to `release_notes.md`
- `nextReleaseTag` Print next release tag

#### `generateReleaseNotes`
Command to generate release notes and append them to `release_notes.md`

| Option | Description |
|---------------------- |--------------------------------------------------------- |
| --token | Git token |
| --release-notes-file | Path to release_notes.md (default `./release_notes.md`) |

#### `nextReleaseTag`
Print next release tag

| Option | Description |
|----------------- |----------------- |
| `--token` | Git token |

### Release
The release process was described in [document](../docs/release_process.md).
To show all available commands for release use:
Expand Down
6 changes: 4 additions & 2 deletions flank-scripts/src/main/kotlin/flank/scripts/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package flank.scripts

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import flank.scripts.ci.CiCommand
import flank.scripts.release.ReleaseCommand

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

fun main(args: Array<String>) {
Main().subcommands(ReleaseCommand())
.main(args)
Main()
.subcommands(ReleaseCommand(), CiCommand())
.main(args)
}
16 changes: 16 additions & 0 deletions flank-scripts/src/main/kotlin/flank/scripts/ci/CiCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package flank.scripts.ci

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import flank.scripts.ci.nexttag.NextReleaseTagCommand
import flank.scripts.ci.releasenotes.GenerateReleaseNotesCommand

class CiCommand : CliktCommand(help = "Contains command related to CI", name = "ci") {

init {
subcommands(GenerateReleaseNotesCommand(), NextReleaseTagCommand())
}

@Suppress("EmptyFunctionBlock")
override fun run() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package flank.scripts.ci.nexttag

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.kittinunf.result.Result
import flank.scripts.github.getLatestReleaseTag
import kotlin.system.exitProcess
import kotlinx.coroutines.runBlocking

class NextReleaseTagCommand : CliktCommand(help = "Print next release tag", name = "nextReleaseTag") {

private val token by option(help = "Git Token").required()

override fun run() {
runBlocking {
getNextReleaseTagCommand(token)
}
}
}

private suspend fun getNextReleaseTagCommand(token: String) {
when (val result = getLatestReleaseTag(token)) {
is Result.Success -> println(generateNextReleaseTag(result.value.tag))
is Result.Failure -> {
println(result.error)
exitProcess(1)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package flank.scripts.ci.nexttag

import java.time.LocalDate
import java.time.Year
import java.time.format.DateTimeFormatter

fun generateNextReleaseTag(previousReleaseTag: String): String {
val (year, month, number) = previousReleaseTag.trimStart('v').split('.')
return if (isNextReleaseInCurrentMonth(
year,
month
)
) "v$year.$month.${number.toInt() + 1}" else currentMonthFirstTag()
}

private fun isNextReleaseInCurrentMonth(year: String, month: String) =
DateTimeFormatter.ofPattern("yy").format(Year.now()) == year &&
DateTimeFormatter.ofPattern("MM").format(LocalDate.now()) == month

private fun currentMonthFirstTag() = "v${DateTimeFormatter.ofPattern("yy.MM").format(LocalDate.now())}.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package flank.scripts.ci.releasenotes

import flank.scripts.utils.markdownH2
import java.io.File

fun File.appendReleaseNotes(messages: List<String>, releaseTag: String) {
appendToReleaseNotes(
messages = messages,
releaseTag = releaseTag
)
}

private fun File.appendToReleaseNotes(messages: List<String>, releaseTag: String) {
val currentFileLines = readLines()
val newLines = mutableListOf<String>().apply {
add(releaseTag.markdownH2())
addAll(messages)
add("")
}

writeText((newLines + currentFileLines).joinToString(System.lineSeparator()).withNewLineAtTheEnd())
}

private fun String.withNewLineAtTheEnd() = plus(System.lineSeparator())
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package flank.scripts.ci.releasenotes

import flank.scripts.utils.markdownBold

fun String.mapPrTitle() = when {
startsWith("feat") -> replaceFirst("feat", "New feature".markdownBold())
startsWith("fix") -> replaceFirst("fix", "Fix".markdownBold())
startsWith("docs") -> replaceFirst("docs", "Documentation".markdownBold())
startsWith("refactor") -> replaceFirst("refactor", "Refactor".markdownBold())
startsWith("ci") -> replaceFirst("ci", "CI changes".markdownBold())
startsWith("test") -> replaceFirst("test", "Tests update".markdownBold())
startsWith("perf") -> replaceFirst("perf", "Performance upgrade".markdownBold())
else -> null // we do not accept other prefix to have update in release notes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package flank.scripts.ci.releasenotes

import com.github.kittinunf.result.Result
import flank.scripts.github.getPrDetailsByCommit
import flank.scripts.utils.markdownLink
import flank.scripts.utils.runCommand
import java.io.File
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking

fun generateReleaseNotes(latestReleaseTag: String, githubToken: String) = getCommitsSha(latestReleaseTag).getReleaseNotes(githubToken)

internal fun getCommitsSha(fromTag: String): List<String> {
val outputFile = File.createTempFile("sha", ".log")

"git log --pretty=%H $fromTag..HEAD".runCommand(fileForOutput = outputFile)

return outputFile.readLines()
}

private fun List<String>.getReleaseNotes(githubToken: String) = runBlocking {
map { sha -> async { getPrDetailsByCommit(sha, githubToken) } }
.awaitAll()
.filterIsInstance<Result.Success<List<GithubPullRequest>>>()
.map { it.value }
.filter { it.isNotEmpty() }
.mapNotNull { it.first().toReleaseNoteMessage() }
}

private fun GithubPullRequest.toReleaseNoteMessage() =
title.mapPrTitle()?.let { title ->
"- ${markdownLink(number.toString(), htmlUrl)} $title ${assignees.format()}"
}

private fun List<GithubUser>.format() = "(${joinToString { (login, url) -> markdownLink(login, url) }})"
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package flank.scripts.ci.releasenotes

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.kittinunf.result.success
import flank.scripts.ci.nexttag.generateNextReleaseTag
import flank.scripts.github.getLatestReleaseTag
import java.io.File
import kotlinx.coroutines.runBlocking

class GenerateReleaseNotesCommand :
CliktCommand("Command to append item to release notes", name = "generateReleaseNotes") {

private val token by option(help = "Git Token").required()
internal val releaseNotesFile by option(help = "Path to release_notes.md").default("release_notes.md")

override fun run() {
runBlocking {
getLatestReleaseTag(token).success {
File(releaseNotesFile).appendReleaseNotes(
messages = generateReleaseNotes(it.tag, token),
releaseTag = generateNextReleaseTag(it.tag)
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package flank.scripts.ci.releasenotes

import com.github.kittinunf.fuel.core.ResponseDeserializable
import flank.scripts.utils.toObject
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class GitHubRelease(
@SerialName("tag_name") val tag: String
)

object GithubReleaseDeserializable : ResponseDeserializable<GitHubRelease> {
override fun deserialize(content: String) = content.toObject(GitHubRelease.serializer())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package flank.scripts.ci.releasenotes

import com.github.kittinunf.fuel.core.ResponseDeserializable
import flank.scripts.utils.toObject
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.list

@Serializable
data class GithubPullRequest(
@SerialName("html_url") val htmlUrl: String,
val title: String,
val number: Int,
val assignees: List<GithubUser>
)

@Serializable
data class GithubUser(
val login: String,
@SerialName("html_url") val htmlUrl: String
)

object GithubPullRequestDeserializer : ResponseDeserializable<List<GithubPullRequest>> {
override fun deserialize(content: String): List<GithubPullRequest> {
return content.toObject(GithubPullRequest.serializer().list)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package flank.scripts.exceptions
import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.fuel.core.isClientError
import com.github.kittinunf.result.Result
import flank.scripts.release.hub.GitHubErrorResponse
import flank.scripts.github.GitHubErrorResponse
import flank.scripts.release.updatebugsnag.BugSnagResponse
import flank.scripts.utils.toObject

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package flank.scripts.exceptions

import flank.scripts.release.hub.GitHubErrorResponse
import flank.scripts.github.GitHubErrorResponse
import flank.scripts.release.updatebugsnag.BugSnagResponse

sealed class FlankScriptsExceptions : Exception()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package flank.scripts.release.hub
package flank.scripts.github

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand Down
33 changes: 33 additions & 0 deletions flank-scripts/src/main/kotlin/flank/scripts/github/GithubApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package flank.scripts.github

import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.extensions.authentication
import com.github.kittinunf.fuel.coroutines.awaitResult
import flank.scripts.ci.releasenotes.GithubPullRequestDeserializer
import flank.scripts.ci.releasenotes.GithubReleaseDeserializable
import flank.scripts.exceptions.mapClientError
import flank.scripts.exceptions.toGithubException

suspend fun getPrDetailsByCommit(commitSha: String, githubToken: String) =
Fuel.get("https://api.github.com/repos/flank/flank/commits/$commitSha/pulls")
.appendHeader("Accept", "application/vnd.github.groot-preview+json")
.appendHeader("Authorization", "token $githubToken")
.awaitResult(GithubPullRequestDeserializer)
.mapClientError { it.toGithubException() }

suspend fun getLatestReleaseTag(githubToken: String) =
Fuel.get("https://api.github.com/repos/flank/flank/releases/latest")
.appendHeader("Accept", "application/vnd.github.v3+json")
.appendHeader("Authorization", "token $githubToken")
.awaitResult(GithubReleaseDeserializable)
.mapClientError { it.toGithubException() }

fun deleteOldTag(tag: String, username: String, password: String) =
Fuel.delete(DELETE_ENDPOINT + tag)
.authentication()
.basic(username, password)
.response()
.third
.mapClientError { it.toGithubException() }

private const val DELETE_ENDPOINT = "https://api.github.com/repos/Flank/flank/git/refs/tags/"

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.kittinunf.result.Result
import flank.scripts.github.deleteOldTag
import kotlinx.coroutines.runBlocking

class DeleteOldTagCommand : CliktCommand(name = "deleteOldTag", help = "Delete old tag on GitHub") {
Expand Down
Loading