Skip to content

Commit 57073fe

Browse files
refs #268: add task for generating release notes from GitHub milestone issues (#269)
1 parent 99dadd5 commit 57073fe

File tree

3 files changed

+209
-0
lines changed

3 files changed

+209
-0
lines changed

build.gradle.kts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import com.jashmore.gradle.GithubReleaseNotesTask
12
import com.jashmore.gradle.JacocoCoverallsPlugin
23
import com.jashmore.gradle.ReleasePlugin
34
import com.jashmore.gradle.release
@@ -201,6 +202,65 @@ release {
201202
signingPassword = System.getenv("GPG_SIGNING_PASSWORD")
202203
}
203204

205+
val milestonePropertyName = "milestoneVersion"
206+
val generateReleaseNotesTaskName = "generateReleaseNotes"
207+
208+
tasks.register<GithubReleaseNotesTask>(generateReleaseNotesTaskName) {
209+
group = "release"
210+
description = "Task for generating the release notes from the issues in a GitHub milestone"
211+
212+
doLast {
213+
if (!project.hasProperty(milestonePropertyName)) {
214+
throw RuntimeException(
215+
"""
216+
Required property $milestonePropertyName has not been set.
217+
218+
Usage: ./gradlew $generateReleaseNotesTaskName -P$milestonePropertyName=4.0.0
219+
""".trimIndent()
220+
)
221+
}
222+
milestoneVersion = project.properties[milestonePropertyName] as String
223+
githubUser = "JaidenAshmore"
224+
repositoryName = "java-dynamic-sqs-listener"
225+
groupings = {
226+
group {
227+
title = "Enhancements"
228+
filter = {
229+
labels.any { it.name == "enhancement" }
230+
}
231+
renderer = {
232+
"""
233+
|### $title [GH-$number]
234+
|
235+
|${body.substringAfter("### Release Notes")}
236+
|
237+
""".trimMargin()
238+
}
239+
}
240+
241+
group {
242+
title = "Bug Fixes"
243+
filter = { labels.any { it.name == "bug" } }
244+
renderer = {
245+
"""
246+
| - [GH-$number]: $title
247+
""".trimMargin()
248+
}
249+
}
250+
251+
group {
252+
title = "Documentation"
253+
filter = { labels.any { it.name == "documentation" } }
254+
renderer = {
255+
"""
256+
| - [GH-$number]: $title
257+
""".trimMargin()
258+
}
259+
}
260+
}
261+
}
262+
}
263+
204264
/**
205265
* Used to print out the current version so it can be saved as an output variable in a GitHub workflow.
206266
*/

buildSrc/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ plugins {
1111
dependencies {
1212
implementation("gradle.plugin.org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.10.1")
1313
implementation("io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.21.2")
14+
implementation("org.eclipse.mylyn.github:org.eclipse.egit.github.core:2.1.5")
1415
implementation(gradleKotlinDsl())
1516
}
1617

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.jashmore.gradle
2+
3+
import org.eclipse.egit.github.core.Issue
4+
import org.eclipse.egit.github.core.client.GitHubClient
5+
import org.eclipse.egit.github.core.service.IssueService
6+
import org.eclipse.egit.github.core.service.MilestoneService
7+
import org.eclipse.egit.github.core.service.RepositoryService
8+
import org.gradle.api.DefaultTask
9+
import org.gradle.api.tasks.Input
10+
import org.gradle.api.tasks.TaskAction
11+
12+
typealias IssueRenderer = (issue: Issue) -> String
13+
14+
data class IssueGrouping(val title: String, val description: String?, val renderer: IssueRenderer, val filter: (issue: Issue) -> Boolean)
15+
16+
@DslMarker
17+
annotation class ReleaseNotesDsl
18+
19+
@ReleaseNotesDsl
20+
class GroupingDsl {
21+
/**
22+
* The required title of the grouping, e.g. Enhancements or bug fixes.
23+
*/
24+
var title: String? = null
25+
26+
/**
27+
* The optional description of the group that will be included underneath the title.
28+
*/
29+
var description: String? = null
30+
31+
/**
32+
* The renderer that will be able to render the release note informatiion for the issue in the group.
33+
*/
34+
var renderer: (Issue.() -> String)? = null
35+
36+
/**
37+
* The filter to determine if the issue should be present in this group.
38+
*
39+
* Note that each issue will only display in a single group and will prioritise the first group that it exists in.
40+
*/
41+
var filter: Issue.() -> Boolean = { false }
42+
}
43+
44+
@ReleaseNotesDsl
45+
class GroupingsDsl {
46+
var groups = mutableListOf<IssueGrouping>()
47+
48+
/**
49+
* Add a new group into the groupings.
50+
*/
51+
fun group(init: GroupingDsl.() -> Unit) {
52+
val grouping = GroupingDsl()
53+
grouping.init()
54+
groups.add(IssueGrouping(
55+
grouping.title ?: throw IllegalArgumentException("Expected field 'title' not set"),
56+
grouping.description,
57+
grouping.renderer ?: throw IllegalArgumentException("Expected field 'renderer' not set"),
58+
grouping.filter
59+
))
60+
}
61+
}
62+
63+
/**
64+
* Task for generating release notes from the issues in a GitHub milestone.
65+
*/
66+
open class GithubReleaseNotesTask : DefaultTask() {
67+
/**
68+
* The name of the milestone to obtain issues for, e.g. 4.0.0.
69+
*/
70+
@get:Input
71+
var milestoneVersion: String? = null
72+
73+
/**
74+
* The username of the GitHub user that owns the repository, e.g. JaidenAshmore.
75+
*/
76+
@get:Input
77+
var githubUser: String? = null
78+
79+
/**
80+
* The name of the repository, e.g. java-dynamic-sqs-listener.
81+
*/
82+
@get:Input
83+
var repositoryName: String? = null
84+
85+
/**
86+
* Defines the groups of issues and how to render them.
87+
*
88+
* For example, this can be used to group enhancements or bug fixes into separate groups and for each issue render the issue in some way.
89+
*/
90+
@get:Input
91+
var groupings: GroupingsDsl.() -> Unit = { }
92+
93+
/**
94+
* Optional auth token that can be used to use a personal or OAuth token for the client.
95+
*/
96+
@get:Input
97+
var authToken: String = ""
98+
99+
@TaskAction
100+
fun generate() {
101+
val milestoneVersion = this@GithubReleaseNotesTask.milestoneVersion ?: throw IllegalArgumentException("Required field milestoneVersion is not set")
102+
val githubUser = this@GithubReleaseNotesTask.githubUser ?: throw IllegalArgumentException("Required field githubUser is not set")
103+
val repositoryName = this@GithubReleaseNotesTask.repositoryName ?: throw IllegalArgumentException("Required field repositoryName is not set")
104+
105+
val client = GitHubClient()
106+
if (authToken.isNotEmpty()) {
107+
client.setOAuth2Token(authToken)
108+
}
109+
val repository = RepositoryService(client).getRepository(githubUser, repositoryName)
110+
val milestone = MilestoneService(client).getMilestones(repository, "all")
111+
.first { it.title == milestoneVersion }
112+
113+
val groupingsDsl = GroupingsDsl()
114+
groupingsDsl.groupings()
115+
116+
val issues = IssueService(client).getIssues(repository, mutableMapOf(
117+
"milestone" to milestone.number.toString(),
118+
"state" to "closed"
119+
))
120+
121+
val issuesRendered = mutableSetOf<Issue>()
122+
123+
val releaseMarkdown = groupingsDsl.groups.joinToString("\n") {
124+
val matchingIssues = issues
125+
.filter(it.filter)
126+
.filterNot(issuesRendered::contains)
127+
128+
if (matchingIssues.isEmpty()) {
129+
return@joinToString ""
130+
}
131+
132+
val renderedIssueInformation = matchingIssues
133+
.onEach(issuesRendered::add)
134+
.joinToString("\n", postfix = "\n") { issue -> it.renderer(issue) }
135+
"""
136+
|## ${it.title}
137+
|${it.description ?: ""}
138+
|
139+
|$renderedIssueInformation
140+
""".trimMargin()
141+
}
142+
143+
println("MD: \n$releaseMarkdown")
144+
}
145+
}
146+
147+
148+

0 commit comments

Comments
 (0)