Skip to content

Commit

Permalink
Merge pull request #292 from saalfeldlab/auto-generate-release-note
Browse files Browse the repository at this point in the history
[UNVERSIONED] Auto-generate release note by @hanslovsky 

Add a [kscript](https://github.com/holgerbrandl/kscript) that auto-generates release notes from pull request messages:
 1. Scan commits from current `HEAD` to tag with latest version smaller than current (SNAPSHOT) version.
 2. Filter all commit messages
 3. In each commit message, look for any of these tags, encapuslated in brackets `[$TAG]`:
    - `BREAKING`
    - `FEATURE`
    - `BUGFIX`
    - `UNVERSIONED`

The script will suggest a new SemVer version based on the presence of these tags (`UNVERSIONED` will not increase the version). For example, if `HEAD` was on 8018669, the output would be:
```
$ ./create-changelog.kts
# Paintera 0.18.0
Previous release: 0.18.0


## Changelog

### Other
 - Merge pull request #290 from uschmidt83/patch-1: Typo in Readme


## Pull Requests

### #290
Merge pull request #290 from uschmidt83/patch-1

Typo in Readme
```
It is the maintainers' (@saalfeldlab/lab @saalfeldlab/paintera @saalfeldlab/paintera-admin ) responsibility to add appropriate tags to the commit message when clicking the merge button.

A good workflow to trigger a new Paintera release would then be:
```
git checkout master && git pull
./create-changelog.kts > changelog.md
# modify changelog.md if necessary
scijava-scripts/release-version.sh # use version suggested by the changelog script if appropriate
# add changelog to release at https://github.com/saalfeldlab/paintera/releases
# modify generate-bash-completion.kts to use latest Paintera release
./generate-bash-completion.kts
# upload paintera_completion to release at https://github.com/saalfeldlab/paintera/releases
# modify generate-bash-completion.kts to use current SNAPSHOT version (can push directly to master for that one)
```
  • Loading branch information
hanslovsky authored Aug 8, 2019
2 parents 140e602 + 8018669 commit 570877a
Showing 1 changed file with 219 additions and 0 deletions.
219 changes: 219 additions & 0 deletions create-changelog.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/usr/bin/env kscript

@file:DependsOn("org.json:json:20180813")
@file:DependsOn("org.apache.maven:maven-model:3.6.1")
@file:DependsOn("org.eclipse.jgit:org.eclipse.jgit:5.4.0.201906121030-r")
@file:DependsOn("org.slf4j:slf4j-simple:1.7.25")

import org.apache.maven.model.io.xpp3.MavenXpp3Reader
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.net.HttpURLConnection
import java.net.URL

data class Change(
val sha: String,
val author: String,
val message: String,
val date: String) {
constructor(commit: JSONObject) : this(
sha = commit.getString("sha"),
author = (commit["author"] as JSONObject).getString("login"),
message = (commit["commit"] as JSONObject).getString("message"),
date = (((commit["commit"] as JSONObject)["author"] as JSONObject)).getString("date"))
}

fun versionFromString(str: String): Version {
return if (str.contains("-")) {
val split = str.split("-")
val Mmp = split[0].split(".").map { it.toInt() }.toIntArray()
Version(Mmp[0], Mmp[1], Mmp[2], split[1])
} else {
val Mmp = str.split(".").map { it.toInt() }.toIntArray()
Version(Mmp[0], Mmp[1], Mmp[2], null)
}
}

data class Version(
val major: Int,
val minor: Int,
val patch: Int,
val preRelease: String? = null) : Comparable<Version> {

val versionString = preRelease?.let { "$major.$minor.$patch-$preRelease" } ?: "$major.$minor.$patch"

override fun toString() = versionString

override fun compareTo(other: Version): Int {
val majorComp = major.compareTo(other.major)
if (majorComp != 0)
return majorComp
val minorComp = minor.compareTo(other.minor)
if (minorComp != 0)
return minorComp
val patchComp = patch.compareTo(other.patch)
if (patchComp != 0)
return patchComp
if (preRelease === null && other.preRelease != null)
return 1
if (other.preRelease === null && preRelease != null)
return -1
else if (other.preRelease === null && preRelease === null)
return 0
return preRelease!!.compareTo(other.preRelease!!)
}

fun bump(major: Boolean = false, minor: Boolean = false, patch: Boolean = false): Version {
if (major) return Version(this.major + 1, 0, 0, null)
if (minor) return Version(this.major, this.minor + 1, 0, null)
if (patch) return Version(this.major, this.minor, this.patch + 1, null)
return Version(this.major, this.minor, this.patch, this.preRelease)
}

}

val GITHUB_API_URL = "https://api.github.com";

fun getTags(repo: String) = JSONArray(fromURL(URL("$GITHUB_API_URL/repos/$repo/tags"))).filterIsInstance<JSONObject>()

fun getCommitTags(repo: String) = getTags(repo).filter { it.has("commit") }

fun getParentsFromTag(repo: String, tag: JSONObject) = JSONObject(fromURL(URL("$GITHUB_API_URL/repos/$repo/commits/${(tag["commit"] as JSONObject)["sha"]}")))["parents"] as JSONArray

fun getParentFromTag(repo: String, tag: JSONObject) = getParentsFromTag(repo, tag)
.also { require(it.length() == 1) { "Release tags must have exactly one parent but got $it" } }[0] as JSONObject

fun compare(repo: String, shaFrom: String, shaTo: String): List<Change> {
val compareUrl = URL("$GITHUB_API_URL/repos/$repo/compare/$shaFrom...$shaTo")
val compareObj = JSONObject(fromURL(compareUrl))
val commits = compareObj["commits"] as JSONArray
return commits.map { Change(it as JSONObject) }
}


fun fromURL(url: URL): String {
val conn = url.openConnection() as HttpURLConnection;
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");

if (conn.getResponseCode() != 200)
throw RuntimeException("Failed : HTTP error code : ${conn.responseCode}")

val br = conn.inputStream.bufferedReader();
val text = br.use(BufferedReader::readText)
conn.disconnect()
return text
}

fun generateChangelog() {

val reader = MavenXpp3Reader()
val model = FileReader("pom.xml").use { reader.read(it) }
val isSnapshot = model.version.contains("-SNAPSHOT")
val version = versionFromString(model.version)

val tags = getCommitTags("saalfeldlab/paintera").filter {
try {
versionFromString(it.getString("name").replace("paintera-", ""))
true
} catch (e: Exception) {
false
}
}

val repo = FileRepositoryBuilder().setGitDir(File(".git")).build()
val head = repo.resolve("HEAD")
val commitTo = if (isSnapshot) {
head.name
} else {
"paintera-${model.version}"
.takeUnless { t -> tags.count { it.getString("name") == t } == 0 }
?: head.name
}

val tagFrom = tags.first { versionFromString(it.getString("name").replace("paintera-", "")) < version }
val commitFrom = getParentFromTag("saalfeldlab/paintera", tagFrom)
val relevantCommits = compare(
repo = "saalfeldlab/paintera",
shaFrom = commitFrom.getString("sha"),
shaTo = commitTo)
val mergeCommits = relevantCommits.filter { it.message.startsWith("Merge pull request #") }

val breaking = mutableListOf<String>()
val features = mutableListOf<String>()
val fixes = mutableListOf<String>()
val unversioned = mutableListOf<String>()
val visitedCommits = mutableSetOf<String>()
val pullRequests = mutableListOf<Pair<Int, String>>()

val regex = "Merge pull request #([0-9]+)".toRegex()

for (commit in mergeCommits) {
if (visitedCommits.contains(commit.sha)) continue
visitedCommits.add(commit.sha)
val lines = commit.message.lines()
val matchResult = regex.find(lines[0])
val pullRequestNumber = if (matchResult === null) {
-1
} else {
matchResult.groupValues[1].toInt()
}
pullRequests += Pair(pullRequestNumber, commit.message)
var isAnything = false
for (line in lines) {
var l = line
val isBreaking = if (l.contains("[BREAKING]")) {
l = l.replace("[BREAKING]", "").trim()
true
} else false
val isFeature = if (l.contains("[FEATURE]")) {
l = l.replace("[FEATURE]", "").trim()
true
} else false
val isBugfix = if (l.contains("[BUGFIX]")) {
l = l.replace("[BUGFIX]", "").trim()
true
} else false
val isUnversioned = if (l.contains("[UNVERSIONED]")) {
l = l.replace("[UNVERSIONED]", "").trim()
true
} else false
isAnything = isBreaking || isFeature || isBugfix || isUnversioned
if (pullRequestNumber > 0) l = "$l (#$pullRequestNumber)"
if (isBreaking) breaking.add(l)
if (isFeature) features.add(l)
if (isBugfix) fixes.add(l)
if (isUnversioned) unversioned.add(l)
}
if (!isAnything) {
unversioned.add(commit.message.replace("\n+".toRegex(), ": "))
}
}

val versionFrom = versionFromString(tagFrom.getString("name").replace("paintera-", ""))
val suggestedVersion = versionFrom.bump(major = !breaking.isEmpty(), minor = !features.isEmpty(), patch = !fixes.isEmpty())

var text = "# Paintera $suggestedVersion\nPrevious release: $versionFrom\n\n\n## Changelog"

if (!breaking.isEmpty())
text = "$text\n\n### Breaking Changes${breaking.map { "\n - $it" }.joinToString("")}"

if (!features.isEmpty())
text = "$text\n\n### New Features${features.map { "\n - $it" }.joinToString("")}"

if (!fixes.isEmpty())
text = "$text\n\n### Bug Fixes${fixes.map { "\n - $it" }.joinToString("")}"

if (!unversioned.isEmpty())
text = "$text\n\n### Other${unversioned.map { "\n - $it" }.joinToString("")}"
val pullRequestStrings = pullRequests.map { "### #${it.first}\n${it.second}" }
text = "$text\n\n\n## Pull Requests\n\n${pullRequestStrings.joinToString("\n\n")}"

println(text)
}

generateChangelog()

0 comments on commit 570877a

Please sign in to comment.