Skip to content

Commit 9860d18

Browse files
authored
Add GHA for release note creation for patches (#1793)
* Add GHA for release note creation for patches Signed-off-by: Kai Kreuzer <kai@openhab.org>
1 parent 6dd1729 commit 9860d18

3 files changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: Create Release Notes
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'Release version (e.g., 5.0.2)'
8+
required: true
9+
type: string
10+
default: '5.0.2'
11+
fromDate:
12+
description: 'From date for changelog (ISO format, e.g., 2025-08-19T16:30:00)'
13+
required: true
14+
type: string
15+
default: '2025-08-19T16:30:00'
16+
toDate:
17+
description: 'To date for changelog (ISO format, leave empty for current date)'
18+
required: false
19+
type: string
20+
milestone:
21+
description: 'Milestone version (e.g., 5.1)'
22+
required: true
23+
type: string
24+
default: '5.1'
25+
26+
jobs:
27+
create-release:
28+
runs-on: ubuntu-latest
29+
permissions:
30+
contents: write
31+
32+
steps:
33+
- name: Checkout repository
34+
uses: actions/checkout@v4
35+
with:
36+
fetch-depth: 0
37+
38+
- name: Set up Java
39+
uses: actions/setup-java@v4
40+
with:
41+
distribution: 'temurin'
42+
java-version: '17'
43+
44+
- name: Set up Gradle
45+
uses: gradle/actions/setup-gradle@v3
46+
47+
- name: Generate changelog
48+
run: |
49+
GRADLE_ARGS="-Pversion=${{ inputs.version }} -Pmilestone=${{ inputs.milestone }} -PfromDate=${{ inputs.fromDate }}"
50+
if [ -n "${{ inputs.toDate }}" ]; then
51+
GRADLE_ARGS="$GRADLE_ARGS -PtoDate=${{ inputs.toDate }}"
52+
fi
53+
cd launch
54+
gradle buildChangelog $GRADLE_ARGS
55+
env:
56+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57+
58+
59+
- name: Download openHAB distribution (ZIP)
60+
run: |
61+
curl -fLsS --retry 3 --retry-delay 5 \
62+
-o openhab-${{ inputs.version }}.zip \
63+
"https://openhab.jfrog.io/artifactory/libs-release-local/org/openhab/distro/openhab/${{ inputs.version }}/openhab-${{ inputs.version }}.zip"
64+
65+
- name: Download openHAB distribution (TAR.GZ)
66+
run: |
67+
curl -fLsS --retry 3 --retry-delay 5 \
68+
-o openhab-${{ inputs.version }}.tar.gz \
69+
"https://openhab.jfrog.io/artifactory/libs-release-local/org/openhab/distro/openhab/${{ inputs.version }}/openhab-${{ inputs.version }}.tar.gz"
70+
71+
- name: Download openHAB add-ons (KAR)
72+
run: |
73+
curl -fLsS --retry 3 --retry-delay 5 \
74+
-o openhab-addons-${{ inputs.version }}.kar \
75+
"https://openhab.jfrog.io/artifactory/libs-release-local/org/openhab/distro/openhab-addons/${{ inputs.version }}/openhab-addons-${{ inputs.version }}.kar"
76+
77+
- name: Create GitHub Release and upload assets
78+
env:
79+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80+
run: |
81+
# Create or update release with notes from changelog and attach assets
82+
if gh release view "${{ inputs.version }}" >/dev/null 2>&1; then
83+
# Release exists: update notes and upload assets
84+
gh release edit "${{ inputs.version }}" \
85+
--title "openHAB ${{ inputs.version }}" \
86+
--notes-file "launch/build/changelog.md"
87+
else
88+
gh release create "${{ inputs.version }}" \
89+
--title "openHAB ${{ inputs.version }}" \
90+
--notes-file "launch/build/changelog.md"
91+
fi
92+
gh release upload "${{ inputs.version }}" \
93+
"openhab-${{ inputs.version }}.zip" \
94+
"openhab-${{ inputs.version }}.tar.gz" \
95+
"openhab-addons-${{ inputs.version }}.kar" \
96+
--clobber

launch/build-patch.gradle

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import groovy.json.*
2+
import java.util.regex.Pattern
3+
import java.util.regex.Matcher
4+
5+
task prepareDirs {
6+
doLast {
7+
mkdir 'build'
8+
}
9+
}
10+
11+
task buildChangelog {
12+
13+
dependsOn prepareDirs
14+
15+
doLast {
16+
def outputText = ""
17+
def stringsFile = new File("./build/changelog.md")
18+
19+
// Get parameters from system properties - fail if not provided
20+
def openHABMilestone = project.findProperty('milestone')
21+
def openHABPatch = project.findProperty('version')
22+
def fromDate = project.findProperty('fromDate')
23+
def toDate = project.findProperty('toDate')
24+
25+
// Validate that all required parameters are provided
26+
if (!openHABMilestone) {
27+
throw new GradleException("Required property 'milestone' not provided. Use -Pmilestone=<value>")
28+
}
29+
if (!openHABPatch) {
30+
throw new GradleException("Required property 'version' not provided. Use -Pversion=<value>")
31+
}
32+
if (!fromDate) {
33+
throw new GradleException("Required property 'fromDate' not provided. Use -PfromDate=<value>")
34+
}
35+
36+
// Use current date if toDate is not provided
37+
if (!toDate) {
38+
def now = new Date()
39+
toDate = now.format("yyyy-MM-dd'T'HH:mm:ss")
40+
}
41+
42+
def mergedString = "%20merged:${fromDate}..${toDate}"
43+
44+
def fullChangeList = []
45+
46+
def categories = [
47+
[
48+
"type" : "core",
49+
"orgName" : "openhab",
50+
"milestone" : "${openHABMilestone}",
51+
"repos" : [
52+
[
53+
"name" : "openhab-core"
54+
]
55+
]
56+
],[
57+
"type" : "uis",
58+
"orgName" : "openhab",
59+
"milestone" : "${openHABMilestone}",
60+
"repos" : [
61+
[
62+
"name" : "openhab-webui",
63+
]
64+
]
65+
],[
66+
"type" : "add-ons",
67+
"orgName" : "openhab",
68+
"milestone" : "${openHABMilestone}",
69+
"repos" : [
70+
[
71+
"name" : "openhab-addons",
72+
],[
73+
"name" : "org.openhab.binding.zigbee",
74+
"addonName" : "Zigbee"
75+
],[
76+
"name" : "org.openhab.binding.zwave",
77+
"addonName" : "ZWave"
78+
]
79+
]
80+
]
81+
]
82+
83+
84+
categories.each { category ->
85+
86+
def pullRequests
87+
def milestone = category.milestone
88+
def milestoneString = ""
89+
if (milestone != "") {milestoneString = "%20milestone:%22${milestone}%22"}
90+
def orgName = category.orgName
91+
92+
category.repos.each { repo ->
93+
94+
def curlURL = ""
95+
// Choose correct backport label depending on version (4.x uses 'backported4')
96+
def labelName = "backported"
97+
if (openHABPatch?.trim()?.startsWith("4.")) {
98+
labelName = "backported4"
99+
}
100+
def labelString = "%20label:%22${labelName}%22%20-label:%22regression%22"
101+
def repoName = repo.name
102+
def addonName = repo.addonName
103+
104+
def searchesLeft = 1
105+
def pageNumber = 1
106+
def pageString = ""
107+
108+
while (searchesLeft > 0) {
109+
curlURL = "https://api.github.com/search/issues?q=repo:${orgName}/${repoName}%20is:pr${mergedString}${labelString}&per_page=100${pageString}"
110+
print "Parsing: ${orgName}/${repoName} (Milestone ${milestone}, Page ${pageNumber}"
111+
112+
// Perform HTTP request using native Groovy/Java (no external exec)
113+
String curlOutputText = ""
114+
try {
115+
def conn = new URL(curlURL).openConnection()
116+
conn.setRequestProperty('User-Agent', 'openhab-release-bot')
117+
conn.setRequestProperty('Accept', 'application/vnd.github+json')
118+
def token = System.getenv('GITHUB_TOKEN')
119+
if (token) {
120+
conn.setRequestProperty('Authorization', "Bearer ${token}")
121+
conn.setRequestProperty('X-GitHub-Api-Version', '2022-11-28')
122+
}
123+
conn.connectTimeout = 15000
124+
conn.readTimeout = 60000
125+
curlOutputText = conn.inputStream.getText('UTF-8')
126+
} catch (Exception e) {
127+
throw new GradleException("HTTP request to GitHub failed: ${e.message}")
128+
}
129+
130+
// Be nice to the API
131+
Thread.sleep(2000)
132+
133+
def jsonSlurper = new JsonSlurper()
134+
def searchResults = jsonSlurper.parseText(curlOutputText)
135+
136+
searchResults.items.each{pullRequest ->
137+
def pullTitle = pullRequest.title
138+
def pullNumber = pullRequest.number
139+
def pullURL = pullRequest.html_url
140+
def pullDate = pullRequest.closed_at
141+
def pullCategory = category.type
142+
def pullAddon = ""
143+
def docsURL = ""
144+
def pullType = ""
145+
def addonType = ""
146+
147+
// Assign the addon name
148+
Pattern pattern = Pattern.compile("\\[(.*?)\\](.*)")
149+
Matcher matcher = pattern.matcher(pullTitle)
150+
151+
if (matcher.find()) {
152+
pullAddon = matcher.group(1)
153+
pullTitle = matcher.group(2).trim()
154+
} else if (repo.addonName) {
155+
pullAddon = repo.addonName
156+
} else {
157+
pullAddon = "misc"
158+
}
159+
160+
if (repoName == "openhab-core" || repoName == "openhab-distro") {
161+
pullAddon = "Core"
162+
}
163+
164+
// Normalize UI add-on names for Web UI repo
165+
if (repoName == "openhab-webui") {
166+
if (pullAddon?.equalsIgnoreCase("basicui")) {
167+
pullAddon = "Basic UI"
168+
} else if (pullAddon?.equalsIgnoreCase("habpanel")) {
169+
pullAddon = "HABPanel"
170+
} else if (pullAddon?.equalsIgnoreCase("habot")) {
171+
pullAddon = "HABot"
172+
} else if (pullAddon?.equalsIgnoreCase("cometvisu")) {
173+
pullAddon = "CometVisu"
174+
} else if (pullAddon?.equalsIgnoreCase("iconset")) {
175+
pullAddon = "Classic Iconset"
176+
} else {
177+
pullAddon = "Main UI"
178+
}
179+
}
180+
181+
// Assign the pull request type (bug, enhancment etc)
182+
pullRequest.labels.each { label ->
183+
switch(label.name.toLowerCase())
184+
{
185+
case "bug":
186+
pullType="bug"
187+
break
188+
case "enhancement":
189+
pullType="enhancement"
190+
break
191+
}
192+
}
193+
194+
if (repoName == "openhab-core" && (pullType == "bug" || pullType == "enhancement" || pullType == "feature")) {
195+
if ((pullAddon?.equalsIgnoreCase("Misc")) || (pullAddon.toLowerCase() == "core")) {
196+
pullCategory = "core"
197+
pullAddon = "Core"
198+
} else {
199+
pullCategory = "Add-ons"
200+
}
201+
}
202+
203+
// Add PR to full changelog list
204+
if (pullType != "") {
205+
fullChangeList << [
206+
"category" : "${pullCategory}",
207+
"type" : "${pullType}",
208+
"addon" : "${pullAddon}",
209+
"description" : "${pullTitle}",
210+
"number" : "${pullNumber}",
211+
"url" : "${pullURL}",
212+
"docs" : "${docsURL}",
213+
"class" : "${addonType}"
214+
]
215+
}
216+
}
217+
218+
// If there's more pages to go, search these too
219+
if (pageString == "") {
220+
def resultsFound = searchResults.total_count
221+
println ", Pull Requests Found: ${resultsFound})"
222+
if (resultsFound > 100) {
223+
searchesLeft = Math.ceil(resultsFound/100)
224+
}
225+
} else {
226+
println ")"
227+
}
228+
searchesLeft = searchesLeft - 1
229+
pageNumber = pageNumber + 1
230+
pageString = "&page=${pageNumber}"
231+
}
232+
}
233+
}
234+
235+
outputText = outputText + "\nThis patch release contains the following bug fixes:\n"
236+
237+
["Core", "Add-ons", "UIs"].each {categoryName ->
238+
239+
if (categoryName == "Core") {
240+
outputText = outputText + "\n### Runtime\n\n"
241+
outputText = outputText + "| Type | Issue | Change |\n"
242+
outputText = outputText + "|-|-|-|\n"
243+
} else if (categoryName == "UIs") {
244+
outputText = outputText + "\n### User Interfaces\n\n"
245+
outputText = outputText + "| UI | Type | Issue | Change |\n"
246+
outputText = outputText + "|-|-|-|-|\n"
247+
} else {
248+
outputText = outputText + "\n### ${categoryName}\n\n"
249+
outputText = outputText + "| Add-on | Type | Issue | Change |\n"
250+
outputText = outputText + "|-|-|-|-|\n"
251+
}
252+
def sortedList = fullChangeList.findAll{it.type != "new"}.findAll{it.category == "${categoryName.toLowerCase()}"}.sort{it.type}.reverse().sort{it.addon.toLowerCase()}
253+
254+
def previousType = ""
255+
def previousAddon = ""
256+
sortedList.each {
257+
def addonText = ""
258+
def typeText = ""
259+
260+
if (it.addon.toLowerCase() != previousAddon) {
261+
addonText = "**" + "${it.addon}" + "**"
262+
previousType = ""
263+
if (categoryName == "Core") {
264+
outputText = outputText + "| | | |\n"
265+
} else {
266+
outputText = outputText + "| | | | |\n"
267+
}
268+
}
269+
if (it.type != previousType) {
270+
switch (it.type){
271+
case "bug":
272+
typeText = "Bug Fixes"
273+
break
274+
case "enhancement":
275+
typeText = "Enhancements"
276+
break
277+
case "feature":
278+
typeText = "New Feature"
279+
break
280+
}
281+
typeText = "*" + "${typeText}" + "*"
282+
}
283+
284+
if (categoryName == "Core") {
285+
outputText = outputText + "| ${typeText} | [${it.number}](${it.url}) | ${it.description} |\n"
286+
} else {
287+
outputText = outputText + "| ${addonText} | ${typeText} | [${it.number}](${it.url}) | ${it.description} |\n"
288+
}
289+
previousAddon = it.addon.toLowerCase()
290+
previousType = it.type
291+
}
292+
}
293+
stringsFile.text = outputText
294+
}
295+
}

launch/settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.buildFileName = 'build-patch.gradle'

0 commit comments

Comments
 (0)