|
| 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 | +} |
0 commit comments