-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #292 from saalfeldlab/auto-generate-release-note
[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
Showing
1 changed file
with
219 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |