Skip to content
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
96 changes: 96 additions & 0 deletions .github/workflows/create-release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Create Release Notes

on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 5.0.2)'
required: true
type: string
default: '5.0.2'
fromDate:
description: 'From date for changelog (ISO format, e.g., 2025-08-19T16:30:00)'
required: true
type: string
default: '2025-08-19T16:30:00'
toDate:
description: 'To date for changelog (ISO format, leave empty for current date)'
required: false
type: string
milestone:
description: 'Milestone version (e.g., 5.1)'
required: true
type: string
default: '5.1'

jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v3

- name: Generate changelog
run: |
GRADLE_ARGS="-Pversion=${{ inputs.version }} -Pmilestone=${{ inputs.milestone }} -PfromDate=${{ inputs.fromDate }}"
if [ -n "${{ inputs.toDate }}" ]; then
GRADLE_ARGS="$GRADLE_ARGS -PtoDate=${{ inputs.toDate }}"
fi
cd launch
gradle buildChangelog $GRADLE_ARGS
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


- name: Download openHAB distribution (ZIP)
run: |
curl -fLsS --retry 3 --retry-delay 5 \
-o openhab-${{ inputs.version }}.zip \
"https://openhab.jfrog.io/artifactory/libs-release-local/org/openhab/distro/openhab/${{ inputs.version }}/openhab-${{ inputs.version }}.zip"

- name: Download openHAB distribution (TAR.GZ)
run: |
curl -fLsS --retry 3 --retry-delay 5 \
-o openhab-${{ inputs.version }}.tar.gz \
"https://openhab.jfrog.io/artifactory/libs-release-local/org/openhab/distro/openhab/${{ inputs.version }}/openhab-${{ inputs.version }}.tar.gz"

- name: Download openHAB add-ons (KAR)
run: |
curl -fLsS --retry 3 --retry-delay 5 \
-o openhab-addons-${{ inputs.version }}.kar \
"https://openhab.jfrog.io/artifactory/libs-release-local/org/openhab/distro/openhab-addons/${{ inputs.version }}/openhab-addons-${{ inputs.version }}.kar"

- name: Create GitHub Release and upload assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Create or update release with notes from changelog and attach assets
if gh release view "${{ inputs.version }}" >/dev/null 2>&1; then
# Release exists: update notes and upload assets
gh release edit "${{ inputs.version }}" \
--title "openHAB ${{ inputs.version }}" \
--notes-file "launch/build/changelog.md"
else
gh release create "${{ inputs.version }}" \
--title "openHAB ${{ inputs.version }}" \
--notes-file "launch/build/changelog.md"
fi
gh release upload "${{ inputs.version }}" \
"openhab-${{ inputs.version }}.zip" \
"openhab-${{ inputs.version }}.tar.gz" \
"openhab-addons-${{ inputs.version }}.kar" \
--clobber
295 changes: 295 additions & 0 deletions launch/build-patch.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import groovy.json.*
import java.util.regex.Pattern
import java.util.regex.Matcher

task prepareDirs {
doLast {
mkdir 'build'
}
}

task buildChangelog {

dependsOn prepareDirs

doLast {
def outputText = ""
def stringsFile = new File("./build/changelog.md")

// Get parameters from system properties - fail if not provided
def openHABMilestone = project.findProperty('milestone')
def openHABPatch = project.findProperty('version')
def fromDate = project.findProperty('fromDate')
def toDate = project.findProperty('toDate')

// Validate that all required parameters are provided
if (!openHABMilestone) {
throw new GradleException("Required property 'milestone' not provided. Use -Pmilestone=<value>")
}
if (!openHABPatch) {
throw new GradleException("Required property 'version' not provided. Use -Pversion=<value>")
}
if (!fromDate) {
throw new GradleException("Required property 'fromDate' not provided. Use -PfromDate=<value>")
}

// Use current date if toDate is not provided
if (!toDate) {
def now = new Date()
toDate = now.format("yyyy-MM-dd'T'HH:mm:ss")
}

def mergedString = "%20merged:${fromDate}..${toDate}"

def fullChangeList = []

def categories = [
[
"type" : "core",
"orgName" : "openhab",
"milestone" : "${openHABMilestone}",
"repos" : [
[
"name" : "openhab-core"
]
]
],[
"type" : "uis",
"orgName" : "openhab",
"milestone" : "${openHABMilestone}",
"repos" : [
[
"name" : "openhab-webui",
]
]
],[
"type" : "add-ons",
"orgName" : "openhab",
"milestone" : "${openHABMilestone}",
"repos" : [
[
"name" : "openhab-addons",
],[
"name" : "org.openhab.binding.zigbee",
"addonName" : "Zigbee"
],[
"name" : "org.openhab.binding.zwave",
"addonName" : "ZWave"
]
]
]
]


categories.each { category ->

def pullRequests
def milestone = category.milestone
def milestoneString = ""
if (milestone != "") {milestoneString = "%20milestone:%22${milestone}%22"}
def orgName = category.orgName

category.repos.each { repo ->

def curlURL = ""
// Choose correct backport label depending on version (4.x uses 'backported4')
def labelName = "backported"
if (openHABPatch?.trim()?.startsWith("4.")) {
labelName = "backported4"
}
def labelString = "%20label:%22${labelName}%22%20-label:%22regression%22"
def repoName = repo.name
def addonName = repo.addonName

def searchesLeft = 1
def pageNumber = 1
def pageString = ""

while (searchesLeft > 0) {
curlURL = "https://api.github.com/search/issues?q=repo:${orgName}/${repoName}%20is:pr${mergedString}${labelString}&per_page=100${pageString}"
print "Parsing: ${orgName}/${repoName} (Milestone ${milestone}, Page ${pageNumber}"

// Perform HTTP request using native Groovy/Java (no external exec)
String curlOutputText = ""
try {
def conn = new URL(curlURL).openConnection()
conn.setRequestProperty('User-Agent', 'openhab-release-bot')
conn.setRequestProperty('Accept', 'application/vnd.github+json')
def token = System.getenv('GITHUB_TOKEN')
if (token) {
conn.setRequestProperty('Authorization', "Bearer ${token}")
conn.setRequestProperty('X-GitHub-Api-Version', '2022-11-28')
}
conn.connectTimeout = 15000
conn.readTimeout = 60000
curlOutputText = conn.inputStream.getText('UTF-8')
} catch (Exception e) {
throw new GradleException("HTTP request to GitHub failed: ${e.message}")
}

// Be nice to the API
Thread.sleep(2000)

def jsonSlurper = new JsonSlurper()
def searchResults = jsonSlurper.parseText(curlOutputText)

searchResults.items.each{pullRequest ->
def pullTitle = pullRequest.title
def pullNumber = pullRequest.number
def pullURL = pullRequest.html_url
def pullDate = pullRequest.closed_at
def pullCategory = category.type
def pullAddon = ""
def docsURL = ""
def pullType = ""
def addonType = ""

// Assign the addon name
Pattern pattern = Pattern.compile("\\[(.*?)\\](.*)")
Matcher matcher = pattern.matcher(pullTitle)

if (matcher.find()) {
pullAddon = matcher.group(1)
pullTitle = matcher.group(2).trim()
} else if (repo.addonName) {
pullAddon = repo.addonName
} else {
pullAddon = "misc"
}

if (repoName == "openhab-core" || repoName == "openhab-distro") {
pullAddon = "Core"
}

// Normalize UI add-on names for Web UI repo
if (repoName == "openhab-webui") {
if (pullAddon?.equalsIgnoreCase("basicui")) {
pullAddon = "Basic UI"
} else if (pullAddon?.equalsIgnoreCase("habpanel")) {
pullAddon = "HABPanel"
} else if (pullAddon?.equalsIgnoreCase("habot")) {
pullAddon = "HABot"
} else if (pullAddon?.equalsIgnoreCase("cometvisu")) {
pullAddon = "CometVisu"
} else if (pullAddon?.equalsIgnoreCase("iconset")) {
pullAddon = "Classic Iconset"
} else {
pullAddon = "Main UI"
}
}

// Assign the pull request type (bug, enhancment etc)
pullRequest.labels.each { label ->
switch(label.name.toLowerCase())
{
case "bug":
pullType="bug"
break
case "enhancement":
pullType="enhancement"
break
}
}

if (repoName == "openhab-core" && (pullType == "bug" || pullType == "enhancement" || pullType == "feature")) {
if ((pullAddon?.equalsIgnoreCase("Misc")) || (pullAddon.toLowerCase() == "core")) {
pullCategory = "core"
pullAddon = "Core"
} else {
pullCategory = "Add-ons"
}
}

// Add PR to full changelog list
if (pullType != "") {
fullChangeList << [
"category" : "${pullCategory}",
"type" : "${pullType}",
"addon" : "${pullAddon}",
"description" : "${pullTitle}",
"number" : "${pullNumber}",
"url" : "${pullURL}",
"docs" : "${docsURL}",
"class" : "${addonType}"
]
}
}

// If there's more pages to go, search these too
if (pageString == "") {
def resultsFound = searchResults.total_count
println ", Pull Requests Found: ${resultsFound})"
if (resultsFound > 100) {
searchesLeft = Math.ceil(resultsFound/100)
}
} else {
println ")"
}
searchesLeft = searchesLeft - 1
pageNumber = pageNumber + 1
pageString = "&page=${pageNumber}"
}
}
}

outputText = outputText + "\nThis patch release contains the following bug fixes:\n"

["Core", "Add-ons", "UIs"].each {categoryName ->

if (categoryName == "Core") {
outputText = outputText + "\n### Runtime\n\n"
outputText = outputText + "| Type | Issue | Change |\n"
outputText = outputText + "|-|-|-|\n"
} else if (categoryName == "UIs") {
outputText = outputText + "\n### User Interfaces\n\n"
outputText = outputText + "| UI | Type | Issue | Change |\n"
outputText = outputText + "|-|-|-|-|\n"
} else {
outputText = outputText + "\n### ${categoryName}\n\n"
outputText = outputText + "| Add-on | Type | Issue | Change |\n"
outputText = outputText + "|-|-|-|-|\n"
}
def sortedList = fullChangeList.findAll{it.type != "new"}.findAll{it.category == "${categoryName.toLowerCase()}"}.sort{it.type}.reverse().sort{it.addon.toLowerCase()}

def previousType = ""
def previousAddon = ""
sortedList.each {
def addonText = ""
def typeText = ""

if (it.addon.toLowerCase() != previousAddon) {
addonText = "**" + "${it.addon}" + "**"
previousType = ""
if (categoryName == "Core") {
outputText = outputText + "| | | |\n"
} else {
outputText = outputText + "| | | | |\n"
}
}
if (it.type != previousType) {
switch (it.type){
case "bug":
typeText = "Bug Fixes"
break
case "enhancement":
typeText = "Enhancements"
break
case "feature":
typeText = "New Feature"
break
}
typeText = "*" + "${typeText}" + "*"
}

if (categoryName == "Core") {
outputText = outputText + "| ${typeText} | [${it.number}](${it.url}) | ${it.description} |\n"
} else {
outputText = outputText + "| ${addonText} | ${typeText} | [${it.number}](${it.url}) | ${it.description} |\n"
}
previousAddon = it.addon.toLowerCase()
previousType = it.type
}
}
stringsFile.text = outputText
}
}
1 change: 1 addition & 0 deletions launch/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.buildFileName = 'build-patch.gradle'