From 4186cbf0b258db126c616e180cf34239b106df14 Mon Sep 17 00:00:00 2001 From: Karsten Hassel Date: Wed, 10 Dec 2025 18:56:31 +0100 Subject: [PATCH] [core] auto create release notes with every push on develop (#3985) and remove CHANGELOG.md logic. This is my attempt to create a draft release instead of editing a changelog, see discussion on discord. Logic: - new github workflow `.github/workflows/release-notes.yaml` - runs with every push on `develop` (so after PR's are merged) - collects the commits on `develop` which are newer than the latest tag - searches the commit messages for keywords defined in an array and group the messages into categories (this is a first shot, we will update this ...) - creates markdown content - looks for an untagged and unpublished draft release with name `unreleased`, if it exists, it will be deleted - creates an untagged and unpublished draft release with name `unreleased` with markdown content created above Example created on my fork (this caused having `MagicMirrorOrg` in the PR-Links): grafik Please review this PR, it is a draft release at the moment because I got problems in my fork where I tested this: The created draft release is not visible at the moment (they are visible via api). AFAIS this is a queue problem on GitHub, maybe I flooded their queue while testing ... So I will test this tomorrow again before removing `draft` here. --- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/dependabot.yaml | 2 - .../workflows/enforce-pullrequest-rules.yaml | 10 +- .github/workflows/release-notes.yaml | 33 +++ CHANGELOG.md | 54 +---- Collaboration.md | 21 +- js/app.js | 4 +- js/releasenotes.js | 198 ++++++++++++++++++ 8 files changed, 248 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/release-notes.yaml create mode 100644 js/releasenotes.js diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f1d23499..9eb30081 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ Hello and thank you for wanting to contribute to the MagicMirror² project! -**Please make sure that you have followed these 4 rules before submitting your Pull Request:** +**Please make sure that you have followed these 3 rules before submitting your Pull Request:** > 1. Base your pull requests against the `develop` branch. > 2. Include these infos in the description: @@ -12,8 +12,6 @@ Hello and thank you for wanting to contribute to the MagicMirror² project! > > 3. Please run `node --run lint:prettier` before submitting so that > style issues are fixed. -> 4. Don't forget to add an entry about your changes to -> the CHANGELOG.md file. **Note**: Sometimes the development moves very fast. It is highly recommended that you update your branch of `develop` before creating a diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 5e3e8630..c6a9585d 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -6,7 +6,6 @@ updates: interval: "weekly" target-branch: "develop" labels: - - "Skip Changelog" - "dependencies" - package-ecosystem: "npm" @@ -15,6 +14,5 @@ updates: interval: "monthly" target-branch: "develop" labels: - - "Skip Changelog" - "dependencies" - "javascript" diff --git a/.github/workflows/enforce-pullrequest-rules.yaml b/.github/workflows/enforce-pullrequest-rules.yaml index 77d3befb..eda5b224 100644 --- a/.github/workflows/enforce-pullrequest-rules.yaml +++ b/.github/workflows/enforce-pullrequest-rules.yaml @@ -1,6 +1,5 @@ -# This workflow enforces on every pull request: -# - the update of our CHANGELOG.md file, see: https://github.com/dangoslen/changelog-enforcer -# - that the PR is not based against master, taken from https://github.com/oppia/oppia-android/pull/2832/files +# This workflow enforces on every pull request that the PR is not based against master, +# taken from https://github.com/oppia/oppia-android/pull/2832/files name: "Enforce Pull-Request Rules" @@ -13,11 +12,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: "Enforce changelog" - uses: dangoslen/changelog-enforcer@v3 - with: - changeLogPath: "CHANGELOG.md" - skipLabels: "Skip Changelog" - name: "Enforce develop branch" if: ${{ github.event.pull_request.base.ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }} run: | diff --git a/.github/workflows/release-notes.yaml b/.github/workflows/release-notes.yaml new file mode 100644 index 00000000..d992fdfe --- /dev/null +++ b/.github/workflows/release-notes.yaml @@ -0,0 +1,33 @@ +# This workflow writes a draft release on GitHub named `unreleased` after every push on develop + +name: "Create Release Notes" + +on: + push: + branches: [develop] + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + release-notes: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + with: + fetch-depth: "0" + - name: "Use Node.js" + uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: "npm" + - name: "Create Markdown content" + run: | + export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + node js/releasenotes.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c3ec38..90e770a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,58 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror². -## [2.34.0] - unreleased +## Obsolete -planned for 2026-01-01 - -### Added - -- [weather] feat: add configurable forecast date format option (#3918) -- [core] Add new `server:watch` script to run MagicMirror² server-only with automatic restarts when files (defined in `config.watchTargets`) change (#3920) -- [weather] add error handling to fetch functions including cors (#3791) -- [l10n] Add Portuguese (Portugal & Brazil) translations for alert module, Refine Portuguese (Portugal) translations - -### Removed - -- [weather] Removed deprecated `ukmetoffice` datapoint provider (#3842, #3952) - -### Changed - -- [core] refactor: replace `module-alias` dependency with internal alias resolver (#3893) -- [check_config] refactor: improve error handling (#3927) -- [calendar] test: remove "Recurring event per timezone" test (#3929) -- [calendar] chore: remove `requiresVersion: "2.1.0"` (#3932) -- [tests] migrate from `jest` to `vitest` (#3940, #3941) -- [ci] Add concurrency to automated tests workflow to cancel outdated runs (#3943) -- [tests] replace `node-libgpiod` with `serialport` in electron-rebuild workflow (#3945) -- [calendar] hide repeatingCountTitle if the event count is zero (#3949) -- [weatherprovider] update override warning wording (#3914) -- [core] configure cspell to check default modules only and fix typos (#3955) -- [core] refactor: replace `XMLHttpRequest` with `fetch` in `translator.js` (#3950) -- [tests] migrate e2e tests to Playwright (#3950) -- [calendar] refactor: migrate CalendarFetcher to ES6 class and improve error handling (#3958) -- [gitignore] cleanup/simplify .gitignore (#3952, #3954, #3968, #3969) -- [compliments] refactor: optimize `loadComplimentFile` method and add unit tests(#3969) -- [core] chore: simplify Wayland start script (#3974) -- [calendar] refactor: simplify recurring event handling and event exclusion logic (#3976) - -### Fixed - -- feat: add ESlint rule `no-sparse-arrays` for config check to fix #3910 (#3911) -- fixed eslint warnings shown in #3911 and updated npm publish docs (#3913) -- [core] refactor: replace `express-ipfilter` with lightweight custom middleware (#3917) - This fixes security issue [CVE-2023-42282](https://github.com/advisories/GHSA-78xj-cgh5-2h22), which is not very likely to be exploitable in MagicMirror² setups, but still should be fixed. -- fixed the Environment Canada weather URL (#3912) and now converts a windspeed of 'calm' to 0 -- fixed problems with daylight-saving-time in weather provider `openmeto` (#3930, #3931) -- [newsfeed] fixed header layout issue introduced with prettier njk linting (#3946) -- [weather] fixed windy icon not showing up in pirateweather (#3957) -- [compliments] fixed duplicate query param "?" when constructing refresh url (#3967) -- [compliments] fixed compliments remote file minimum delay to be 15 minutes (#3970) -- [calendar] prevent excessive fetching with smart refresh strategy (#3976) - -### Updated - -- [core] Update dependencies incl. electron to v39 (#3909, #3916, #3921, #3925, #3934, #3982) -- [logger] Add prefixes to most Log messages (#3923, #3926) +This file is no longer being updated. Release notes are now automatically generated via a GitHub action. ## [2.33.0] - 2025-10-01 @@ -1877,7 +1828,6 @@ It includes (but is not limited to) the following features: This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the) -[2.34.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.33.0...develop [2.33.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.32.0...v2.33.0 [2.32.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.31.0...v2.32.0 [2.31.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.30.0...v2.31.0 diff --git a/Collaboration.md b/Collaboration.md index ac3afaab..e06904c1 100644 --- a/Collaboration.md +++ b/Collaboration.md @@ -34,29 +34,28 @@ Are done by - [ ] create `prep-release` branch from `develop` - [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0` - [ ] test `prep-release` branch - - [ ] update `CHANGELOG.md` - - [ ] add all contributor names: `...` - - [ ] add min. node version: > ⚠️ This release needs nodejs version `v22.20.0` or higher - - [ ] check release link at the bottom of the file - [ ] commit and push all changes - [ ] create pull request from `prep-release` to `develop` branch with title `Prepare Release 2.xx.0` - [ ] after successful test run via github actions: merge pull request to `develop` +- [ ] review the content of the automatically generated draft release named `unreleased` + - [ ] check contributor names + - [ ] check auto generated min. node version and adjust it for better readability if necessary + - [ ] check if all elements are assigned to the correct category + - [ ] change release name to `v2.xx.0` - [ ] after successful test run via github actions: create pull request from `develop` to `master` branch - [ ] add label `mastermerge` - [ ] title of the PR is `Release 2.xx.0` - - [ ] description of the PR is the section of the `CHANGELOG.md` + - [ ] description of the PR is the body of the draft release with name `v2.xx.0` - [ ] after PR tests run without issues, merge PR -- [ ] create new release with - - [ ] corresponding version tag `v2.xx.0` - - [ ] a release name: `...` - - [ ] description of the release is the section of the `CHANGELOG.md` +- [ ] edit draft release with name `v2.xx.0` + - [ ] set corresponding version tag `v2.xx.0` (with `Select tag` and then `Create new tag`) + - [ ] update release link in `Compare to previous Release` by replacing `develop` with new tag `v2.xx.0` + - [ ] publish the release (button at the bottom) ### Draft new development release - [ ] checkout `develop` branch - [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0-develop` -- [ ] draft new section in `CHANGELOG.md` - - [ ] create new release link at the bottom of the file - [ ] commit and push `develop` branch - [ ] if new release will be in January, update the year in LICENSE.md diff --git a/js/app.js b/js/app.js index 50dccfed..43e1119b 100644 --- a/js/app.js +++ b/js/app.js @@ -158,7 +158,7 @@ function App () { const deprecatedOptions = deprecated.configs; const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option)); if (usedDeprecated.length > 0) { - Log.warn(`WARNING! Your config is using deprecated option(s): ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`); + Log.warn(`WARNING! Your config is using deprecated option(s): ${usedDeprecated.join(", ")}. Check README and Documentation for more up-to-date ways of getting the same functionality.`); } // check for deprecated module options @@ -167,7 +167,7 @@ function App () { const deprecatedModuleOptions = deprecated[element.module]; const usedDeprecatedModuleOptions = deprecatedModuleOptions.filter((option) => element.config.hasOwnProperty(option)); if (usedDeprecatedModuleOptions.length > 0) { - Log.warn(`WARNING! Your config for module ${element.module} is using deprecated option(s): ${usedDeprecatedModuleOptions.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`); + Log.warn(`WARNING! Your config for module ${element.module} is using deprecated option(s): ${usedDeprecatedModuleOptions.join(", ")}. Check README and Documentation for more up-to-date ways of getting the same functionality.`); } } } diff --git a/js/releasenotes.js b/js/releasenotes.js new file mode 100644 index 00000000..13c66ab4 --- /dev/null +++ b/js/releasenotes.js @@ -0,0 +1,198 @@ +/* eslint no-console: "off" */ +const util = require("node:util"); +const exec = util.promisify(require("node:child_process").exec); +const fs = require("node:fs"); + +const createReleaseNotes = async () => { + let repoName = "MagicMirrorOrg/MagicMirror"; + if (process.env.GITHUB_REPOSITORY) { + repoName = process.env.GITHUB_REPOSITORY; + } + const baseUrl = `https://api.github.com/repos/${repoName}`; + + const getOptions = (type) => { + if (process.env.GITHUB_TOKEN) { + return { method: `${type}`, headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } }; + } else { + return { method: `${type}` }; + } + }; + + const execShell = async (command) => { + const { stdout = "", stderr = "" } = await exec(command); + if (stderr) console.error(`Error in execShell executing command ${command}: ${stderr}`); + return stdout; + }; + + // Check Draft Release + const draftReleases = []; + const jsonReleases = await fetch(`${baseUrl}/releases`, getOptions("GET")).then((res) => res.json()); + for (const rel of jsonReleases) { + if (rel.draft && rel.tag_name === "" && rel.published_at === null && rel.name === "unreleased") draftReleases.push(rel); + } + + let draftReleaseId = 0; + if (draftReleases.length > 1) { + throw new Error("More than one draft release found, exiting."); + } else { + if (draftReleases[0]) draftReleaseId = draftReleases[0].id; + } + + // Get last Git Tag + const gitTag = await execShell("git describe --tags `git rev-list --tags --max-count=1`"); + const lastTag = gitTag.toString().replaceAll("\n", ""); + console.info(`latest tag is ${lastTag}`); + + // Get Git Commits + const gitOut = await execShell(`git log develop --pretty=format:"%H --- %s" --after="$(git log -1 --format=%aI ${lastTag})"`); + console.info(gitOut); + const commits = gitOut.toString().split("\n"); + + // Get Node engine version from package.json + const nodeVersion = JSON.parse(fs.readFileSync("package.json")).engines.node; + + // Search strings + const labelArr = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather", "envcanada", "openmeteo", "openweathermap", "smhi", "ukmetoffice", "yr", "eslint", "bump", "dependencies", "deps", "logg", "translation", "test", "ci"]; + + // Map search strings to categories + const getFirstLabel = (text) => { + let res; + labelArr.every((item) => { + const labelIncl = text.includes(item); + if (labelIncl) { + switch (item) { + case "ci": + case "test": + res = "testing"; + break; + case "logg": + res = "logging"; + break; + case "eslint": + case "bump": + case "deps": + res = "dependencies"; + break; + case "envcanada": + case "openmeteo": + case "openweathermap": + case "smhi": + case "ukmetoffice": + case "yr": + case "weather": + res = "modules/weather"; + break; + case "alert": + res = "modules/alert"; + break; + case "calendar": + res = "modules/calendar"; + break; + case "clock": + res = "modules/clock"; + break; + case "compliments": + res = "modules/compliments"; + break; + case "helloworld": + res = "modules/helloworld"; + break; + case "newsfeed": + res = "modules/newsfeed"; + break; + case "updatenotification": + res = "modules/updatenotification"; + break; + default: + res = item; + break; + } + return false; + } else { + return true; + } + }); + if (!res) res = "core"; + return res; + }; + + const grouped = {}; + const contrib = []; + const sha = []; + + // Loop through each Commit + for (const item of commits) { + + const cm = item.trim(); + // ignore `prepare release` line + if (cm.length > 0 && !cm.match(/^.* --- prepare .*-develop$/gi)) { + + const [ref, title] = cm.split(" --- "); + + const groupTitle = getFirstLabel(title.toLowerCase()); + + if (!grouped[groupTitle]) { + grouped[groupTitle] = []; + } + + grouped[groupTitle].push(`- ${title}`); + + sha.push(ref); + } + } + + // function to remove duplicates + const sortedArr = (arr) => { + return arr.filter((item, + index) => (arr.indexOf(item) === index && item !== "@dependabot[bot]")).sort(function (a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }); + }; + + // Get Contributors logins + for (const ref of sha) { + const jsonRes = await fetch(`${baseUrl}/commits/${ref}`, getOptions("GET")).then((res) => res.json()); + + if (jsonRes && jsonRes.author && jsonRes.author.login) contrib.push(`@${jsonRes.author.login}`); + } + + // Build Markdown content + let markdown = "## Release Notes\n"; + + markdown += `Thanks to: ${sortedArr(contrib).join(", ")}\n`; + markdown += `> ⚠️ This release needs nodejs version ${nodeVersion}\n`; + markdown += "\n"; + markdown += `[Compare to previous Release ${lastTag}](https://github.com/${repoName}/compare/${lastTag}...develop)\n\n`; + + const sorted = Object.keys(grouped) + .sort() // Sort the keys alphabetically + .reduce((obj, key) => { + obj[key] = grouped[key]; // Rebuild the object with sorted keys + return obj; + }, {}); + + for (const group in sorted) { + markdown += `\n### [${group}]\n`; + markdown += `${sorted[group].join("\n")}\n`; + } + + console.info(markdown); + + // Create Github Release + if (process.env.GITHUB_TOKEN) { + if (draftReleaseId > 0) { + // delete release + await fetch(`${baseUrl}/releases/${draftReleaseId}`, getOptions("DELETE")); + console.info(`Old Release with id ${draftReleaseId} deleted.`); + } + + const relContent = getOptions("POST"); + relContent.body = JSON.stringify( + { tag_name: "", name: "unreleased", body: `${markdown}`, draft: true } + ); + const createRelease = await fetch(`${baseUrl}/releases`, relContent).then((res) => res.json()); + console.info(`New release created with id ${createRelease.id}, GitHub-Url: ${createRelease.html_url}`); + } +}; + +createReleaseNotes();