diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index e8ea11789..7429740df 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -54,10 +54,11 @@ jobs: MASTER_ZHC_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman-converters"') MASTER_ZH_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman"') MASTER_FRONTEND_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-frontend"') + MASTER_WINDFRONT_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-windfront"') wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--zigbee2mqtt/CHANGELOG.md > z2m/CHANGELOG.md cd z2m pnpm i --frozen-lockfile - node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION >> ../changelog.md + node scripts/generateChangelog.mjs $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION $MASTER_WINDFRONT_VERSION >> ../changelog.md env: GH_TOKEN: ${{secrets.GH_TOKEN}} - name: Update changelog gist diff --git a/package.json b/package.json index 9d4b556ad..4992b6f34 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "zigbee-herdsman": "6.0.1", "zigbee-herdsman-converters": "25.8.0", "zigbee2mqtt-frontend": "0.9.20", - "zigbee2mqtt-windfront": "^1.8.1" + "zigbee2mqtt-windfront": "1.8.1" }, "devDependencies": { "@biomejs/biome": "^2.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 505b0ea04..a2ffca230 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,7 +84,7 @@ importers: specifier: 0.9.20 version: 0.9.20 zigbee2mqtt-windfront: - specifier: ^1.8.1 + specifier: 1.8.1 version: 1.8.1 devDependencies: '@biomejs/biome': diff --git a/scripts/generateChangelog.js b/scripts/generateChangelog.js deleted file mode 100644 index 45eacffca..000000000 --- a/scripts/generateChangelog.js +++ /dev/null @@ -1,157 +0,0 @@ -const path = require("node:path"); -const fs = require("node:fs"); -const process = require("node:process"); -const {execSync} = require("node:child_process"); -const zhc = require("zigbee-herdsman-converters"); - -const z2mTillVersion = process.argv[2]; -const zhcTillVersion = process.argv[3]; -const zhTillVersion = process.argv[4]; -const frontendTillVersion = process.argv[5]; - -const changelogs = [ - { - tillVersion: z2mTillVersion, - project: "koenkk/zigbee2mqtt", - contents: fs.readFileSync(path.join(__dirname, "..", "CHANGELOG.md"), "utf-8").split("\n"), - }, - { - tillVersion: zhcTillVersion, - project: "koenkk/zigbee-herdsman-converters", - contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee-herdsman-converters", "CHANGELOG.md"), "utf-8").split("\n"), - }, - { - tillVersion: zhTillVersion, - project: "koenkk/zigbee-herdsman", - contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee-herdsman", "CHANGELOG.md"), "utf-8").split("\n"), - }, - { - tillVersion: frontendTillVersion, - project: "nurikk/zigbee2mqtt-frontend", - isFrontend: true, - contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee2mqtt-frontend", "CHANGELOG.md"), "utf-8").split("\n"), - }, -]; - -const releaseRe = /## \[(.+)\]/; -const changes = {features: [], fixes: [], detect: [], add: [], error: [], frontend: []}; -let context = null; -const changeRe = [ - /^\* (\*\*(.+):\*\*)?(.+)\((\[#\d+\]\(.+\))\) \(\[.+\]\(https:.+\/(.+)\)\)$/, - /^\* (\*\*(.+):\*\*)?(.+)(https:\/\/github\.com.+) \(\[.+\]\(https:.+\/(.+)\)\)$/, - /^\* (\*\*(.+):\*\*)?(.+)() \(\[.+\]\(https:.+\/(.+)\)\)$/, -]; -const frontendChangeRe = /^\* (\*\*.+:\*\* )()?(.+) \(\[.+\]\(https:.+\/()(.+)\)\)(.+)?$/; - -let commitUserLookup = {}; -const commitUserFile = path.join(__dirname, "commit-user-lookup.json"); -if (fs.existsSync(commitUserFile)) { - commitUserLookup = JSON.parse(fs.readFileSync(commitUserFile, "utf8")); -} - -const definitions = require("zigbee-herdsman-converters/devices/index").default.map((d) => zhc.prepareDefinition(d)); -const whiteLabels = definitions.filter((d) => d.whiteLabel).flatMap((d) => d.whiteLabel); -const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1); - -for (const changelog of changelogs) { - for (const line of changelog.contents) { - const releaseMatch = line.match(releaseRe); - const changeMatch = changelog.isFrontend ? line.match(frontendChangeRe) : changeRe.map((re) => line.match(re)).find((e) => e); - if (releaseMatch) { - if (releaseMatch[1] === changelog.tillVersion) { - break; - } - } else if (line === "### Features") { - context = "features"; - } else if (line === "### Bug Fixes") { - context = "fixes"; - } else if (line.startsWith("* **ignore:**")) { - // continue; - } else if (changeMatch) { - let localContext = changelog.isFrontend ? "frontend" : changeMatch[2] ? changeMatch[2] : context; - if (!changes[localContext]) localContext = "error"; - - const commit = changeMatch[5]; - const commitUserKey = `${changelog.project}-${commit} `; - let user = - commitUserKey in commitUserLookup - ? commitUserLookup[commitUserKey] - : execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`) - .toString() - .trim(); - if (user !== "null") commitUserLookup[commitUserKey] = user; - const messages = []; - let message = changeMatch[3].trim(); - if (message.endsWith(".")) message = message.substring(0, message.length - 1); - - if (changelog.isFrontend) { - changes[localContext].push( - `- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`, - ); - messages.push(capitalizeFirstChar(message)); - } else { - const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/); - if (otherUser) { - user = otherUser[1]; - message = message.replace(otherUser[0], ""); - } - - if (localContext === "add") { - for (const model of message.split(",")) { - const definition = definitions.find((d) => d.model === model.trim()); - const whiteLabel = whiteLabels.find((d) => d.model === model.trim()); - const match = definition || whiteLabel; - if (match) { - messages.push(`\`${match.model}\` ${match.vendor} ${match.description}`); - } else { - changes.error.push(`${line} (model '${model}' does not exist)`); - } - } - } else { - messages.push(capitalizeFirstChar(message)); - } - - let issue = changeMatch[4].trim(); - if (issue && !issue.startsWith("[#")) issue = `[#${issue.split("/").pop()}](${issue})`; - if (!issue) { - issue = "_NO_ISSUE_"; - localContext = "error"; - } - - for (const message of messages) { - changes[localContext].push(`- ${issue} ${message} (@${user})`); - } - } - } else if (line === "# Changelog" || line === "### ⚠ BREAKING CHANGES" || !line) { - // continue; - } else { - changes.error.push(`- Unmatched line: ${line}`); - } - } -} - -let result = ""; -const names = [ - ["features", "Improvements"], - ["fixes", "Fixes"], - ["frontend", "Frontend"], - ["add", "New supported devices"], - ["detect", "Fixed device detections"], - ["error", "Changelog generator error"], -]; -for (const name of names) { - result += `# ${name[1]}\n`; - if (name[0] === "add") { - result += `This release adds support for ${changes.add.length} devices: \n`; - } - - for (const change of changes[name[0]]) { - result += `${change}\n`; - } - - result += "\n"; -} - -fs.writeFileSync(commitUserFile, JSON.stringify(commitUserLookup), "utf-8"); - -console.log(result.trim()); diff --git a/scripts/generateChangelog.mjs b/scripts/generateChangelog.mjs new file mode 100644 index 000000000..cb4818ad9 --- /dev/null +++ b/scripts/generateChangelog.mjs @@ -0,0 +1,185 @@ +import {execSync} from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import zhc from "zigbee-herdsman-converters"; +import definitionIndex from "zigbee-herdsman-converters/devices/index"; + +const z2mTillVersion = process.argv[2]; +const zhcTillVersion = process.argv[3]; +const zhTillVersion = process.argv[4]; +const frontendTillVersion = process.argv[5]; +const windfrontTillVersion = process.argv[6]; + +const changelogs = [ + { + tillVersion: z2mTillVersion, + project: "koenkk/zigbee2mqtt", + contents: fs.readFileSync(path.join(import.meta.dirname, "..", "CHANGELOG.md"), "utf-8").split("\n"), + }, + { + tillVersion: zhcTillVersion, + project: "koenkk/zigbee-herdsman-converters", + contents: fs + .readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee-herdsman-converters", "CHANGELOG.md"), "utf-8") + .split("\n"), + }, + { + tillVersion: zhTillVersion, + project: "koenkk/zigbee-herdsman", + contents: fs.readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee-herdsman", "CHANGELOG.md"), "utf-8").split("\n"), + }, + { + tillVersion: frontendTillVersion, + project: "nurikk/zigbee2mqtt-frontend", + isFrontend: true, + contents: fs.readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee2mqtt-frontend", "CHANGELOG.md"), "utf-8").split("\n"), + }, + { + tillVersion: windfrontTillVersion, + project: "Nerivec/zigbee2mqtt-windfront", + }, +]; + +const releaseRe = /## \[(.+)\]/; +const windfrontChangeRe = /^\* (feat|fix): (.+?)(?: by @([^\s]+) in (https:\/\/github\.com\/Nerivec\/zigbee2mqtt-windfront\/pull\/(\d+)))?$/gm; +const changes = {features: [], fixes: [], detect: [], add: [], error: [], frontend: [], windfront: []}; +let context = null; +const changeRe = [ + /^\* (\*\*(.+):\*\*)?(.+)\((\[#\d+\]\(.+\))\) \(\[.+\]\(https:.+\/(.+)\)\)$/, + /^\* (\*\*(.+):\*\*)?(.+)(https:\/\/github\.com.+) \(\[.+\]\(https:.+\/(.+)\)\)$/, + /^\* (\*\*(.+):\*\*)?(.+)() \(\[.+\]\(https:.+\/(.+)\)\)$/, +]; +const frontendChangeRe = /^\* (\*\*.+:\*\* )()?(.+) \(\[.+\]\(https:.+\/()(.+)\)\)(.+)?$/; + +let commitUserLookup = {}; +const commitUserFile = path.join(import.meta.dirname, "commit-user-lookup.json"); +if (fs.existsSync(commitUserFile)) { + commitUserLookup = JSON.parse(fs.readFileSync(commitUserFile, "utf8")); +} + +const definitions = definitionIndex.default.map((d) => zhc.prepareDefinition(d)); +const whiteLabels = definitions.filter((d) => d.whiteLabel).flatMap((d) => d.whiteLabel); +const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1); + +for (const changelog of changelogs) { + if (changelog.project === "Nerivec/zigbee2mqtt-windfront") { + const releaseRsp = await fetch("https://api.github.com/repos/Nerivec/zigbee2mqtt-windfront/releases"); + const releases = await releaseRsp.json(); + for (const release of releases) { + if (release.name === `v${windfrontTillVersion}`) { + break; + } + + let match = windfrontChangeRe.exec(release.body); + while (match !== null) { + const [, type, message, user, prLink, prId] = match; + const entry = `- ${prId && prLink ? `[#${prId}](${prLink}) ` : ""}${type}: ${message} ${user ? `(@${user.split("[")[0]})` : ""}`; + changes.windfront.push(entry); + match = windfrontChangeRe.exec(release.body); + } + } + } else { + for (const line of changelog.contents) { + const releaseMatch = line.match(releaseRe); + const changeMatch = changelog.isFrontend ? line.match(frontendChangeRe) : changeRe.map((re) => line.match(re)).find((e) => e); + if (releaseMatch) { + if (releaseMatch[1] === changelog.tillVersion) { + break; + } + } else if (line === "### Features") { + context = "features"; + } else if (line === "### Bug Fixes") { + context = "fixes"; + } else if (line.startsWith("* **ignore:**")) { + // continue; + } else if (changeMatch) { + let localContext = changelog.isFrontend ? "frontend" : changeMatch[2] ? changeMatch[2] : context; + if (!changes[localContext]) localContext = "error"; + + const commit = changeMatch[5]; + const commitUserKey = `${changelog.project}-${commit} `; + let user = + commitUserKey in commitUserLookup + ? commitUserLookup[commitUserKey] + : execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`) + .toString() + .trim(); + if (user !== "null") commitUserLookup[commitUserKey] = user; + const messages = []; + let message = changeMatch[3].trim(); + if (message.endsWith(".")) message = message.substring(0, message.length - 1); + + if (changelog.isFrontend) { + changes[localContext].push( + `- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`, + ); + messages.push(capitalizeFirstChar(message)); + } else { + const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/); + if (otherUser) { + user = otherUser[1]; + message = message.replace(otherUser[0], ""); + } + + if (localContext === "add") { + for (const model of message.split(",")) { + const definition = definitions.find((d) => d.model === model.trim()); + const whiteLabel = whiteLabels.find((d) => d.model === model.trim()); + const match = definition || whiteLabel; + if (match) { + messages.push(`\`${match.model}\` ${match.vendor} ${match.description}`); + } else { + changes.error.push(`${line} (model '${model}' does not exist)`); + } + } + } else { + messages.push(capitalizeFirstChar(message)); + } + + let issue = changeMatch[4].trim(); + if (issue && !issue.startsWith("[#")) issue = `[#${issue.split("/").pop()}](${issue})`; + if (!issue) { + issue = "_NO_ISSUE_"; + localContext = "error"; + } + + for (const message of messages) { + changes[localContext].push(`- ${issue} ${message} (@${user})`); + } + } + } else if (line === "# Changelog" || line === "### ⚠ BREAKING CHANGES" || !line) { + // continue; + } else { + changes.error.push(`- Unmatched line: ${line}`); + } + } + } +} + +let result = ""; +const names = [ + ["features", "Improvements"], + ["fixes", "Fixes"], + ["windfront", "Windfront"], + ["frontend", "Frontend"], + ["add", "New supported devices"], + ["detect", "Fixed device detections"], + ["error", "Changelog generator error"], +]; +for (const name of names) { + result += `# ${name[1]}\n`; + if (name[0] === "add") { + result += `This release adds support for ${changes.add.length} devices: \n`; + } + + for (const change of changes[name[0]]) { + result += `${change}\n`; + } + + result += "\n"; +} + +fs.writeFileSync(commitUserFile, JSON.stringify(commitUserLookup), "utf-8"); + +console.log(result.trim());