From 18980fd16caaf4a84bb6cdf112652f3271bf302a Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Fri, 27 Mar 2026 00:06:46 +0100 Subject: [PATCH 1/3] chore: replace changesets with lightweight .changes system Remove @changesets/cli dependency and .changeset directory. Add a simple .changes/ directory with markdown files using YAML frontmatter for semver bump type, technical changelog entry (title + body), optional user-facing summary, and optional structured notices (info/warning/error) for the website UI. Add scripts/release.py to automate: - status: show pending changes and computed next version - rc: create or bump a release candidate tag - stable: consume changes into CHANGELOG.md, export release.json, tag Clean up CHANGELOG.md to remove all RC entries, keeping only stable release notes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .changes/README.md | 58 +++ .changes/bug-fixes.md | 12 + .changes/captive-portal-wizard.md | 13 + .changes/frontend-svelte5.md | 15 + .changes/visual-ledc-drivers.md | 14 + .changes/wellturn-t330.md | 8 + .changeset/mighty-pets-report.md | 5 - .changeset/precious-eagle-cactus-fruit.md | 5 - .changeset/smooth-leds-dance.md | 5 - CHANGELOG.md | 447 +--------------------- scripts/release.py | 385 +++++++++++++++++++ 11 files changed, 507 insertions(+), 460 deletions(-) create mode 100644 .changes/README.md create mode 100644 .changes/bug-fixes.md create mode 100644 .changes/captive-portal-wizard.md create mode 100644 .changes/frontend-svelte5.md create mode 100644 .changes/visual-ledc-drivers.md create mode 100644 .changes/wellturn-t330.md delete mode 100644 .changeset/mighty-pets-report.md delete mode 100644 .changeset/precious-eagle-cactus-fruit.md delete mode 100644 .changeset/smooth-leds-dance.md create mode 100644 scripts/release.py diff --git a/.changes/README.md b/.changes/README.md new file mode 100644 index 00000000..69232b7f --- /dev/null +++ b/.changes/README.md @@ -0,0 +1,58 @@ +# Unreleased Changes + +Drop one markdown file per change in this directory. At release time, `scripts/release.py` folds these into `CHANGELOG.md` and exports a `release.json` for the website. + +## File format + +```yaml +--- +type: minor +--- + +Title line for the changelog entry + +Optional body with more detail, bullet points, etc. +All of this goes into CHANGELOG.md. + +- Detail one +- Detail two + +## Summary + +Short user-facing text for the website UI. Less technical. + +## Notices + +- warning: Users must re-pair their shockers after updating +- info: The captive portal now uses REST instead of WebSocket +- error: Third-party WS tools will break +``` + +### Fields + +**type** (required): `major`, `minor`, or `patch` + +**Changelog entry** (required): Everything between the frontmatter and the first `##` section. First line is the title, rest is the body. Both go into CHANGELOG.md. + +**Summary** (optional): Short user-friendly text for the website/app UI. + +**Notices** (optional): Structured list of `level: message` pairs. Valid levels: `info`, `warning`, `error`. These render as alert boxes in the website UI. + +### Minimal example + +```yaml +--- +type: patch +--- + +Fix crash on knockoff boards after network connects +``` + +## Release workflow + +```bash +python scripts/release.py status # See pending changes and next version +python scripts/release.py rc # Create or bump a release candidate tag +python scripts/release.py stable # Promote to stable, consume changes +python scripts/release.py --dry-run rc # Preview without making changes +``` diff --git a/.changes/bug-fixes.md b/.changes/bug-fixes.md new file mode 100644 index 00000000..938103b1 --- /dev/null +++ b/.changes/bug-fixes.md @@ -0,0 +1,12 @@ +--- +type: patch +--- + +Various bug fixes and stability improvements + +- Cooperative task shutdown with bounded timeout and force-kill fallback +- User-friendly WiFi disconnect error messages +- Remove erroneous "unexpected protocol: https, expected http" warning + +## Summary +Various bug fixes and stability improvements \ No newline at end of file diff --git a/.changes/captive-portal-wizard.md b/.changes/captive-portal-wizard.md new file mode 100644 index 00000000..7babe1e9 --- /dev/null +++ b/.changes/captive-portal-wizard.md @@ -0,0 +1,13 @@ +--- +type: minor +--- + +Full setup wizard, REST API migration, and WiFi CRUD overhaul + +- Migrate all LocalToHub WebSocket commands to HTTP REST endpoints +- Add guided setup wizard with multi-step stepper and advanced settings mode with vertical section navigation +- Portal close is now a soft signal that stays open until the device is fully online +- Add 5-minute auto-close timer when no WebSocket clients are connected + +## Summary +New guided setup wizard walks you through device configuration step by step. An advanced settings mode is also available for experienced users. The captive portal now stays open until the device is fully connected, preventing premature disconnects during setup. diff --git a/.changes/frontend-svelte5.md b/.changes/frontend-svelte5.md new file mode 100644 index 00000000..999f3ca9 --- /dev/null +++ b/.changes/frontend-svelte5.md @@ -0,0 +1,15 @@ +--- +type: minor +--- + +Rewrite frontend to Svelte 5 + Vite + +- Replace SvelteKit with plain Svelte 5 + Vite for the captive portal frontend +- Migrate to Svelte 5 runes ($state, $derived, $effect) +- Single-file HTML output via vite-plugin-singlefile for LittleFS + +## Summary +The captive portal frontend has been completely rewritten using Svelte 5 with its new runes-based reactivity system, replacing the previous SvelteKit setup. The UI is now built as a single HTML file for efficient storage on the device. + +## Notices +- info: The frontend was fully rewritten, but since it is served directly from the device's filesystem, no user action is required. The update is applied automatically with the firmware flash. diff --git a/.changes/visual-ledc-drivers.md b/.changes/visual-ledc-drivers.md new file mode 100644 index 00000000..d90795f5 --- /dev/null +++ b/.changes/visual-ledc-drivers.md @@ -0,0 +1,14 @@ +--- +type: minor +--- + +Replace LED managers with LEDC-driven drivers + +- Rename PinPatternManager/RGBPatternManager to MonoLedDriver/RgbLedDriver +- Replace raw GPIO toggling with ESP32 LEDC hardware PWM for smooth 8-bit brightness control +- Use chip-agnostic LEDC speed mode (SOC_LEDC_SUPPORT_HS_MODE) for ESP32/S2/S3/C3 compatibility +- Add cooperative task shutdown with chunked 50ms delays +- Add ledtest serial command for visual verification + +## Summary +LED indicators now use hardware PWM for smooth brightness transitions instead of simple on/off toggling. This applies to both single-color and RGB LEDs across all supported ESP32 chip variants. diff --git a/.changes/wellturn-t330.md b/.changes/wellturn-t330.md new file mode 100644 index 00000000..8b0d1098 --- /dev/null +++ b/.changes/wellturn-t330.md @@ -0,0 +1,8 @@ +--- +type: minor +--- + +Integrate remote WellturnT330 shocker support + +## Summary +Added support for the Wellturn T330 shocker model. diff --git a/.changeset/mighty-pets-report.md b/.changeset/mighty-pets-report.md deleted file mode 100644 index 4ba9b9d1..00000000 --- a/.changeset/mighty-pets-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'frontend': patch ---- - -Improve authtoken handling in 401 responses diff --git a/.changeset/precious-eagle-cactus-fruit.md b/.changeset/precious-eagle-cactus-fruit.md deleted file mode 100644 index 7f6af08a..00000000 --- a/.changeset/precious-eagle-cactus-fruit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'firmware': patch ---- - -fix: Serial now uses CRLF rather than just LF for better compatibility diff --git a/.changeset/smooth-leds-dance.md b/.changeset/smooth-leds-dance.md deleted file mode 100644 index eaa56c38..00000000 --- a/.changeset/smooth-leds-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'firmware': minor ---- - -feat(visual): Replace LED managers with LEDC-driven drivers, enabling hardware PWM brightness control on mono LEDs diff --git a/CHANGELOG.md b/CHANGELOG.md index a00972ea..439366ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,144 +60,6 @@ This release is a major firmware update bringing a fully reworked RF transmitter -# Version 1.5.0-rc.7 Release Notes - -Fix stable Semver parsing error, make beta builds more verbose, increase OTA task stack size, implement more guards in OTA code. - -**Full Changelog: [1.5.0-rc.6 -> 1.5.0-rc.7](https://github.com/OpenShock/Firmware/compare/1.5.0-rc.6...1.5.0-rc.7)** - - - - -# Version 1.5.0-rc.6 Release Notes - -Improved Captive-Portal responsiveness, removed mDNS server from Captive-Portal, some code cleanup. - -**Full Changelog: [1.5.0-rc.5 -> 1.5.0-rc.6](https://github.com/OpenShock/Firmware/compare/1.5.0-rc.5...1.5.0-rc.6)** - - - - -# Version 1.5.0-rc.5 Release Notes - -Fixed a bug where password was not being supplied to websocket serialization function, causing WiFi network to try to authenticate with no password. - -**Full Changelog: [1.5.0-rc.4 -> 1.5.0-rc.5](https://github.com/OpenShock/Firmware/compare/1.5.0-rc.4...1.5.0-rc.5)** - - - - -# Version 1.5.0-rc.4 Release Notes - -Fixed a bug where using Captive Portal to connect to a WiFi network would pair the password entered with another the network than selected, removed obnoxious "AbsolutelySureButton" - -**Full Changelog: [1.5.0-rc.3 -> 1.5.0-rc.4](https://github.com/OpenShock/Firmware/compare/1.5.0-rc.3...1.5.0-rc.4)** - - - - -# Version 1.5.0-rc.3 Release Notes - -Fixed a critical bug where the firmware could never connect to LCG, and addressed code correctness issues (variable initialization and type casting) along with compiler warning cleanups. - -**Full Changelog: [1.5.0-rc.2 -> 1.5.0-rc.3](https://github.com/OpenShock/Firmware/compare/1.5.0-rc.2...1.5.0-rc.3)** - - - - -# Version 1.5.0-rc.2 Release Notes - -This release candidate focuses on **E-Stop reliability**, **rate limiting behavior**, and **general internal cleanup and correctness improvements**. - -### Highlights - -- **Improved E-Stop handling** - - - More reliable state transitions. - - Removed event spamming by introducing change detection. - - Added a short **re-arm grace period** after clearing to prevent immediate re-triggering due to switch bounce or noise. - - Cleaner handling of external E-Stop triggers. - -- **Rate limiter improvements** - - - More efficient internal tracking of recent requests. - - Corrected cleanup and timing behavior under sustained load. - - More predictable blocking behavior when limits are exceeded. - -### Stability & Internal Cleanup - -- Safer handling of integer formatting and digit counting (avoids edge-case overflows). -- Reduced unnecessary string copying by tightening ownership where appropriate. -- Command handling and serial logic cleaned up for clearer control flow. -- General warning cleanups, minor API refinements, and consistency improvements across the codebase. - -### Build & Tooling - -- Minor CI and dependency updates. -- Expanded compiler warnings where possible to catch issues earlier during development. - -**Full Changelog: [1.5.0-rc.1 -> 1.5.0-rc.2](https://github.com/OpenShock/Firmware/compare/1.5.0-rc.1...1.5.0-rc.2)** - - - - -# Version 1.5.0-rc.1 Release Notes - -This release candidate focuses on **radio reliability**, **firmware behavior improvements**, and a refreshed **frontend**. - -### Highlights - -- Major **RF/RMT transmitter rework** for improved timing accuracy and more reliable shocker communication. -- Updated **AssignLCG** integration using the new backend endpoint (removes the old LCG override setting). -- Hub now reports its Wi-Fi signal strength (RSSI), this will be used to show connection health in UI's. -- **Serial output now uses CRLF** line endings for improved compatibility with Windows terminals. -- Added **T330 shocker protocol** support. - -### Radio & Timing - -- Reworked RF pipeline with more consistent timing and fewer internal allocations. -- Fixed timing issues for **CaiXianlin** and improved **PET998DR** handling. -- Additional guardrails added to the RF subsystem to prevent stalls/crash loops. - -### Firmware Behavior / System - -- Introduced internal **execution time limits** to prevent firmware from getting stuck in long-running operations. -- Replaced all `ESP.restart()` usage with ESP-IDF native `esp_restart()` calls -- Improved OTA, Wi-Fi initialization, and crash-loop resilience. - -### HTTP & Gateway - -- Clearer/more consistent HTTP error and status behavior. -- Improved 401 handling and token recovery. -- AssignLCG now reports firmware version and uses the updated endpoint. - -### Frontend & UX - -- Migrated to **Svelte 5 + shadcn**. -- Updated to **Tailwind CSS v4** and reduced frontend bundle size. -- UI palette synced with website. - -### Misc / Internal Changes - -- PlatformIO, espressif32, FlatBuffers, Node, pnpm, and other dependency updates. -- Build reproducibility improvements and CI cleanup. -- Various memory fixes, warning cleanups, and improved parsing logic. - -### Breaking Changes - -- **LCG override removed** (new AssignLCG endpoint required). -- **Serial output now uses CRLF** (update scripts if needed). - -### Notes - -- RF timing changes are substantial; please report shocker-specific regressions. -- Scripts relying on legacy AssignLCG behavior may need updates. - -**Full Changelog: [1.4.0 -> 1.5.0-rc.1](https://github.com/OpenShock/Firmware/compare/1.4.0...1.5.0-rc.1)** - - - - # Version 1.4.0 Release Notes This release is packed with bugfixes, optimizations, code cleanup, prepwork for ESP-IDF, and some features! @@ -225,42 +87,6 @@ This release is packed with bugfixes, optimizations, code cleanup, prepwork for -# Version 1.4.0-rc.2 Release Notes - -Fixed EStop debouncing logic. - -**Full Changelog: [1.4.0-rc.1 -> 1.4.0-rc.2](https://github.com/OpenShock/Firmware/compare/1.4.0-rc.1...1.4.0-rc.2)** - - - - -# Version 1.4.0-rc.1 Release Notes - -This release is packed with bugfixes, optimizations, code cleanup, prepwork for ESP-IDF, and some features! - -### Highlights - -- Add support for configuring hostname of ESP via Serial. -- Add support for configuring Emergency Stop via Captive Portal and Serial. -- Report available GPIO pins to Captive Portal Frontend. -- Massively refactor serial command handler. - -### Optimizations - -- Bump platform-espressif32 to version 6.9. -- Start using C++17 features including std::string_view. -- Clean up platformio.ini file. -- Lots of miscellanious code cleanup. -- Implement custom zero-copy type conversion methods with better error checking. -- Reduce log spam by the arduino library. -- Improve error handling of gpio pin selection. -- Attempt to make more sense out of the 998DR protocol serializer. - -**Full Changelog: [1.3.0 -> 1.4.0-rc.1](https://github.com/OpenShock/Firmware/compare/1.3.0...1.4.0-rc.1)** - - - - # Version 1.3.0 Release Notes This release adds support for more boards, has more bugfixes, better error handling, and optimization/cleanup. @@ -283,28 +109,6 @@ This release adds support for more boards, has more bugfixes, better error handl -# Version 1.3.0-rc.1 Release Notes - -This is the first release candidate for version 1.3.0. - -### Highlight - -- Added support for **DFRobot Firebeetle**, **Wemos S3 Mini** and **WaveShare S3 Zero** boards. - -### Minor Updates - -- Re-Add **PET998DR** Quiet Postamble. -- Fix CaiXianlin protocol sending non-zero when doing a beep command. -- Moved schema files to seperate repository. -- Improve error handling and logging. -- Dependency updates. -- Code cleanup, optimization and refactoring. - -**Full Changelog: [1.2.0 -> 1.3.0-rc.1](https://github.com/OpenShock/Firmware/compare/1.2.0...1.3.0-rc.1)** - - - - # Version 1.2.0 Release Notes This release adds a new shocker protocol, more bugfixes, configurability, and performance improvements. @@ -332,33 +136,6 @@ This release adds a new shocker protocol, more bugfixes, configurability, and pe -# Version 1.2.0-rc.1 Release Notes - -This is the first release candidate for version 1.2.0. - -### Highlight - -- Added support for **998DR** Petrainer RF protocol. - -### Major Updates - -- Add command to get/set api domain. -- Add command to get/set/clear override for Live Control Gateway (LCG) domain. - -### Minor Updates - -- Change transmission end command to last for 300 ms. -- Increase WDT timeout during OTA updates to prevent watchdog resets. -- Remove non thread-safe RF sequence caching. -- Update flatbuffers to 23.5.26. -- Start utilizing StringView more to reduce memory and CPU usage. -- Small code cleanup and refactoring. - -**Full Changelog: [1.1.2 -> 1.2.0-rc.1](https://github.com/OpenShock/Firmware/compare/1.1.2...1.2.0-rc.1)** - - - - # Version 1.1.2 Release Notes - Add support for OpenShock Core V2 Hardware @@ -392,62 +169,6 @@ This release is increases the stability and performance of the firmware, as well -# Version 1.1.1-rc.6 Release Notes - -Inlined the wait time check in RFTransmitter to re-check if we added any commands on receiving a event. - -**Full Changelog: [1.1.1-rc.5 -> 1.1.1-rc.6](https://github.com/OpenShock/Firmware/compare/1.1.1-rc.5...1.1.1-rc.6)** - - - - -# Version 1.1.1-rc.5 Release Notes - -Fixed a bug where the RFTransmitter loop would never delay, causing other tasks running on the same core to completely halt. - -**Full Changelog: [1.1.1-rc.4 -> 1.1.1-rc.5](https://github.com/OpenShock/Firmware/compare/1.1.1-rc.4...1.1.1-rc.5)** - - - - -# Version 1.1.1-rc.4 Release Notes - -Fix tag check again, this time for real. - -**Full Changelog: [1.1.1-rc.3 -> 1.1.1-rc.4](https://github.com/OpenShock/Firmware/compare/1.1.1-rc.3...1.1.1-rc.4)** - - - - -# Version 1.1.1-rc.3 Release Notes - -Increased performance margins for RFTransmitter to prevent commands from stacking up and getting delayed. - -Fixed python build script git-tag check to check `GIT_REF_NAME` instead of incorrect `GIT_BASE_REF` which caused it to build in debug mode. - -**Full Changelog: [1.1.1-rc.2 -> 1.1.1-rc.3](https://github.com/OpenShock/Firmware/compare/1.1.1-rc.2...1.1.1-rc.3)** - - - - -# Version 1.1.1-rc.2 Release Notes - -Removed null check on credentials password received in frontend, as null is expected due to sensitive data removal. - -**Full Changelog: [1.1.1-rc.1 -> 1.1.1-rc.2](https://github.com/OpenShock/Firmware/compare/1.1.1-rc.1...1.1.1-rc.2)** - - - - -# Version 1.1.1-rc.1 Release Notes - -In this release we enabled release builds, resulting in smaller, faster, and more stable firmware. - -**Full Changelog: [1.1.0 -> 1.1.1-rc.1](https://github.com/OpenShock/Firmware/compare/1.1.0...1.1.1-rc.1)** - - - - # Version 1.1.0 Release Notes It's finally here! The 1.1.0 release of OpenShock is now available for download. @@ -458,7 +179,7 @@ From introducing seamless Over-The-Air updates, adding support for new hardware, We've also squashed some pesky bugs and made various minor updates to streamline and optimize your experience. -Here’s what’s new: +Here's what's new: ### Major Updates @@ -526,142 +247,6 @@ Here’s what’s new: -# Version 1.1.0-rc.6 Release Notes - -Bugfixes: - -- Reduced latency, allocations, and network traffic for reporting wifi network scan results, making the networks instantly available in the frontend and improving the reliability of the captive portal. - -**Full Changelog: [1.1.0-rc.5 -> 1.1.0-rc.6](https://github.com/OpenShock/Firmware/compare/1.1.0-rc.5...1.1.0-rc.6)** - - - - -# Version 1.1.0-rc.5 Release Notes - -Bugfixes: - -- Fix what firmware boot type firmware reports to server. - -**Full Changelog: [1.1.0-rc.4 -> 1.1.0-rc.5](https://github.com/OpenShock/Firmware/compare/1.1.0-rc.4...1.1.0-rc.5)** - - - - -# Version 1.1.0-rc.4 Release Notes - -Bugfixes: - -- Make OTA update status reporting smoother. - -**Full Changelog: [1.1.0-rc.3 -> 1.1.0-rc.4](https://github.com/OpenShock/Firmware/compare/1.1.0-rc.3...1.1.0-rc.4)** - - - - -# Version 1.1.0-rc.3 Release Notes - -Bugfixes: - -- Fixed updateID not being sent with BootStatus message. - -**Full Changelog: [1.1.0-rc.2 -> 1.1.0-rc.3](https://github.com/OpenShock/Firmware/compare/1.1.0-rc.2...1.1.0-rc.3)** - - - - -# Version 1.1.0-rc.2 Release Notes - -This is the RC (Release Candidate) 2 for version 1.1.0 - -We did a couple of bugfixes: - -- Fixed User-Agent header not being set on websocket connections. -- Stopped frontend from requesting to connect to a secured network without a password. -- Do sanity checking on pairing code length in firmware to return a proper error message early. -- Fixed some SemVer parsing logic. - -**Full Changelog: [1.1.0-rc.1 -> 1.1.0-rc.2](https://github.com/OpenShock/Firmware/compare/1.1.0-rc.1...1.1.0-rc.2)** - - - - -# Version 1.1.0-rc.1 Release Notes - -It's been a while, and we think it's time for another beta release :smile: - -This is the RC (Release Candidate) 1 for version 1.1.0, hence the naming: 1.1.0-rc.1 - -This update is packed with some major enhancements and numerous improvements that we believe will improve your experience using your device. - -From introducing seamless Over-The-Air updates, adding support for new hardware, to enhancing the overall functionality and stability of the system, we've worked hard to improve upon our last release. - -We've also squashed some pesky bugs and made various minor updates to streamline and optimize your experience. - -Here’s what’s new: - -### Major Updates - -- **OTA (Over-The-Air) Updates**: - - Introduced a seamless OTA update capability, device can now be updated with the click of a button. - - Features: - - Updates can be triggered remotely via the OpenShock website. - - Device can automatically check for updates at a configured interval. - - Update state will be streamed back to frontend so you can see the status of your device in real time. - - Provided an option to deactivate each of these features individually through the Captive Portal for users preferring manual control. -- **Support for OpenShock Core V1**: Added support for @nullstalgia custom PCB, which is [fully open-source](https://github.com/nullstalgia/OpenShock-Hardware/tree/main/Core). -- **Captive Behavior Enhancement**: Phones and PC's now prompt the user with the Captive Portal upon WiFi connection. -- **More serial commands**: - - Read/Write configuration in JSON or raw binary format. - - Shocker command execution via serial. - - GPIO pin listing, excluding reserved pins. - - Serial command echo support for terminals lacking this feature. -- **Reworked partitions:** Moved configuration into its own partition, ensuring it persists across updates -- **Reserved pins**: - - Added support for reserved pins so users can no longer use reserved pins that might lead to ESP instability. - - User will now receive an error upon trying to set anything to use these pins. - - The available pins can be listed via the `AvailGPIO` serial command. -- **Shocker keepalive**: - - Improved shocker responsiveness by sending keepalive messages to them at a interval. - - This will prevent the shockers from entering sleep mode. - - Has option to be disabled via the `keepalive` serial command. -- **Config Handler Overhaul**: - - Rewrote config handler to be more modular and make it easier to expand upon the code base. - - Each config section is now seperated into its own file and class. - -### Minor Updates - -- Firmware upload now includes an MD5 sum. -- Enhanced reliability of WiFi scanning. -- Status LEDs: - - Reworked some code for LED pattern and state management. - - Added support for WS2812B RGB (Gamer :sunglasses:) LEDs. -- Improved WiFi connectivity speed post-setup. -- Updated Captive Portal color palette. -- Dependency cleanup: - - Removed ArduinoJSON - - Removed nonstd/span -- Removed Arduino-style loop behaviors, replaced with freeRTOS tasks. -- Improved FreeRTOS task management. -- Implemented self-ratelimiting on httpclient. -- Enhanced error checking in captive portal and firmware. -- CodeQL code quality checks integrated into CI/CD pipeline. -- Utilized filesystem partition hash as ETag for content caching. -- Improved logs to be more consise and verbose. -- Miscellaneous code cleanup, refactoring, and optimizations. - -### Bug Fixes - -- Resolved issue with WiFi scans getting stuck. -- Fixed connection problems with unsecured networks. -- Altered CommandHandler to use a queue kill message, preventing panic when deleting a mid-listening queue. -- Fixed ESP becoming unresponsive when the looptask would get deleted by captive portal deconstructor due to a missing null check. - -**Full Changelog: [1.0.0 -> 1.1.0-rc.1](https://github.com/OpenShock/Firmware/compare/1.0.0...1.1.0-rc.1)** - - - - # Version 1.0.0 Release Notes - We now support **six different boards**: @@ -675,7 +260,7 @@ Here’s what’s new: - The **Captive Portal** got a MASSIVE overhaul; - Serial commands have gotten alot better. - Improved board stability and configurability. -- Added support for having a E-Stop (emergency stop) connected to ESP as a panic button. Thanks to @nullstalgia ❤️ +- Added support for having a E-Stop (emergency stop) connected to ESP as a panic button. Thanks to @nullstalgia - And _much, much_ more behind the scenes, including bugfixes and code cleanup. **Full Changelog: [v0.8.1 -> 1.0.0](https://github.com/OpenShock/Firmware/compare/v0.8.1...1.0.0)** @@ -683,32 +268,4 @@ Here’s what’s new: -# Version 1.0.0-rc.4 - -**Full Changelog: [1.0.0-rc.3 -> 1.0.0-rc.4](https://github.com/OpenShock/Firmware/compare/1.0.0-rc.3...1.0.0-rc.4)** - - - - -# Version 1.0.0-rc.3 - -**Full Changelog: [1.0.0-rc.2 -> 1.0.0-rc.3](https://github.com/OpenShock/Firmware/compare/1.0.0-rc.2...1.0.0-rc.3)** - - - - -# Version 1.0.0-rc.2 - -**Full Changelog: [1.0.0-rc.1 -> 1.0.0-rc.2](https://github.com/OpenShock/Firmware/compare/1.0.0-rc.1...1.0.0-rc.2)** - - - - -# Version 1.0.0-rc.1 - -**Full Changelog: [v0.8.1 -> 1.0.0-rc.1](https://github.com/OpenShock/Firmware/compare/v0.8.1...1.0.0-rc.1)** - - - - # Version v0.8.1 diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 00000000..117f691b --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +""" +Release helper for OpenShock firmware. + +Reads .changes/*.md files, determines the semver bump, and manages +version tagging, CHANGELOG.md generation, and release.json export. + +Usage: + python scripts/release.py status Show pending changes and next version + python scripts/release.py rc Create or bump an RC tag + python scripts/release.py stable Promote to stable, consume changes into CHANGELOG.md +""" + +import argparse +import glob +import json +import os +import re +import subprocess +import sys +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + +CHANGES_DIR = '.changes' +CHANGELOG_FILE = 'CHANGELOG.md' +RELEASE_JSON_FILE = 'release.json' +BUMP_ORDER = {'patch': 0, 'minor': 1, 'major': 2} +NOTICE_LEVELS = {'info', 'warning', 'error'} + + +@dataclass +class Notice: + level: str + message: str + + +@dataclass +class Change: + bump: str + title: str + body: str + summary: str + notices: list + filename: str + + +def get_project_root(): + d = Path(__file__).resolve().parent.parent + if (d / 'platformio.ini').exists(): + return d + return Path.cwd() + + +def run_git(*args): + result = subprocess.run(['git'] + list(args), capture_output=True, text=True, cwd=get_project_root()) + if result.returncode != 0: + return None + return result.stdout.strip() + + +def get_latest_stable_tag(): + tags = run_git('tag', '--sort=-v:refname') + if not tags: + return None + for tag in tags.splitlines(): + tag = tag.strip() + if re.match(r'^\d+\.\d+\.\d+$', tag): + return tag + return None + + +def get_latest_rc_tag(base_version): + tags = run_git('tag', '--sort=-v:refname') + if not tags: + return None + pattern = re.compile(rf'^{re.escape(base_version)}-rc\.(\d+)$') + best = 0 + for tag in tags.splitlines(): + m = pattern.match(tag.strip()) + if m: + best = max(best, int(m.group(1))) + return best if best > 0 else None + + +def parse_version(version_str): + m = re.match(r'^(\d+)\.(\d+)\.(\d+)', version_str) + if not m: + raise ValueError(f'Invalid version: {version_str}') + return int(m.group(1)), int(m.group(2)), int(m.group(3)) + + +def bump_version(major, minor, patch, bump): + if bump == 'major': + return major + 1, 0, 0 + elif bump == 'minor': + return major, minor + 1, 0 + else: + return major, minor, patch + 1 + + +def parse_notices(text): + """Parse a notices section into a list of Notice objects.""" + notices = [] + for line in text.strip().split('\n'): + line = line.strip() + if not line or not line.startswith('- '): + continue + line = line[2:] # strip "- " + m = re.match(r'^(info|warning|error):\s*(.+)$', line) + if m: + notices.append(Notice(level=m.group(1), message=m.group(2).strip())) + return notices + + +def parse_change_file(path): + """Parse a change file with YAML frontmatter and optional ## sections.""" + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + + filename = os.path.basename(path) + + # Parse YAML frontmatter + m = re.match(r'^---\s*\n(.*?)\n---\s*\n(.*)$', content, re.DOTALL) + if not m: + print(f'Warning: skipping {filename} (no YAML frontmatter)', file=sys.stderr) + return None + + frontmatter = m.group(1) + body_raw = m.group(2).strip() + + # Extract type + type_match = re.search(r'^type:\s*(major|minor|patch)\s*$', frontmatter, re.MULTILINE) + if not type_match: + print(f'Warning: skipping {filename} (missing or invalid "type")', file=sys.stderr) + return None + + bump = type_match.group(1) + + # Split into sections by ## headers + sections = re.split(r'^(## \w+)\s*$', body_raw, flags=re.MULTILINE) + + # First chunk is the changelog (title + body) + changelog_raw = sections[0].strip() + + # Parse remaining sections + summary = '' + notices = [] + i = 1 + while i < len(sections) - 1: + header = sections[i].strip() + content_block = sections[i + 1].strip() + if header == '## Summary': + summary = content_block + elif header == '## Notices': + notices = parse_notices(content_block) + i += 2 + + # Split changelog into title (first line) and body (rest) + changelog_lines = changelog_raw.split('\n') + title = changelog_lines[0].strip() + body = '\n'.join(changelog_lines[1:]).strip() + + return Change( + bump=bump, + title=title, + body=body, + summary=summary, + notices=notices, + filename=filename, + ) + + +def read_changes(): + root = get_project_root() + changes = [] + for path in sorted(glob.glob(str(root / CHANGES_DIR / '*.md'))): + if os.path.basename(path).lower() == 'readme.md': + continue + change = parse_change_file(path) + if change: + changes.append(change) + return changes + + +def get_highest_bump(changes): + if not changes: + return None + return max((c.bump for c in changes), key=lambda b: BUMP_ORDER.get(b, 0)) + + +def cmd_status(args): + changes = read_changes() + if not changes: + print('No pending changes.') + return + + latest = get_latest_stable_tag() + print(f'Latest stable tag: {latest or "(none)"}') + + highest = get_highest_bump(changes) + if latest: + major, minor, patch = parse_version(latest) + new_major, new_minor, new_patch = bump_version(major, minor, patch, highest) + next_ver = f'{new_major}.{new_minor}.{new_patch}' + print(f'Bump level: {highest}') + print(f'Next version: {next_ver}') + print() + + for c in changes: + flags = [] + if c.summary: + flags.append('summary') + if c.notices: + flags.append(f'{len(c.notices)} notices') + extra = f' ({", ".join(flags)})' if flags else '' + print(f' [{c.bump}] {c.title}{extra} <- {c.filename}') + + +def cmd_rc(args): + changes = read_changes() + if not changes: + print('No pending changes, nothing to release.') + return 1 + + latest = get_latest_stable_tag() + if not latest: + print('Error: no stable tag found to base RC on.', file=sys.stderr) + return 1 + + highest = get_highest_bump(changes) + major, minor, patch = parse_version(latest) + new_major, new_minor, new_patch = bump_version(major, minor, patch, highest) + base = f'{new_major}.{new_minor}.{new_patch}' + + existing_rc = get_latest_rc_tag(base) + rc_num = (existing_rc or 0) + 1 + tag = f'{base}-rc.{rc_num}' + + if args.dry_run: + print(f'Would create tag: {tag}') + return + + run_git('tag', tag) + print(f'Created tag: {tag}') + print(f'Push with: git push origin {tag}') + + +def build_changelog_entry(tag, latest, changes): + """Build a CHANGELOG.md entry from changes.""" + lines = [f'# Version {tag} Release Notes\n'] + + for level in ['major', 'minor', 'patch']: + level_changes = [c for c in changes if c.bump == level] + if not level_changes: + continue + for c in level_changes: + lines.append(f'- {c.title}') + if c.body: + for bline in c.body.split('\n'): + lines.append(f' {bline}' if bline.strip() else '') + lines.append('') + + # Collect all notices + all_notices = [] + for c in changes: + all_notices.extend(c.notices) + + if all_notices: + lines.append('### Notices\n') + for n in all_notices: + lines.append(f'- **{n.level.upper()}**: {n.message}') + lines.append('') + + lines.append(f'**Full Changelog: [{latest} -> {tag}](https://github.com/OpenShock/Firmware/compare/{latest}...{tag})**\n') + return '\n'.join(lines) + + +def build_release_json(tag, latest, changes): + """Build a release.json for the website.""" + data = { + 'version': tag, + 'previous_version': latest, + 'date': datetime.now().strftime('%Y-%m-%d'), + 'changes': [], + 'notices': [], + } + + for c in changes: + entry = { + 'type': c.bump, + 'title': c.title, + } + if c.body: + entry['body'] = c.body + if c.summary: + entry['summary'] = c.summary + data['changes'].append(entry) + + for c in changes: + for n in c.notices: + data['notices'].append({ + 'level': n.level, + 'message': n.message, + }) + + return data + + +def cmd_stable(args): + changes = read_changes() + if not changes: + print('No pending changes, nothing to release.') + return 1 + + latest = get_latest_stable_tag() + if not latest: + print('Error: no stable tag found.', file=sys.stderr) + return 1 + + highest = get_highest_bump(changes) + major, minor, patch = parse_version(latest) + new_major, new_minor, new_patch = bump_version(major, minor, patch, highest) + tag = f'{new_major}.{new_minor}.{new_patch}' + + entry = build_changelog_entry(tag, latest, changes) + release_data = build_release_json(tag, latest, changes) + + if args.dry_run: + print(f'Would create tag: {tag}') + print(f'\nChangelog entry:\n{entry}') + print(f'\nrelease.json:\n{json.dumps(release_data, indent=2)}') + return + + root = get_project_root() + + # Write release.json + release_json_path = root / RELEASE_JSON_FILE + with open(release_json_path, 'w', encoding='utf-8') as f: + json.dump(release_data, f, indent=2) + f.write('\n') + print(f'Wrote {RELEASE_JSON_FILE}') + + # Prepend to CHANGELOG.md + changelog_path = root / CHANGELOG_FILE + existing = '' + if changelog_path.exists(): + existing = changelog_path.read_text(encoding='utf-8') + changelog_path.write_text(entry + '\n' + existing, encoding='utf-8') + print(f'Updated {CHANGELOG_FILE}') + + # Delete change files + for c in changes: + os.remove(root / CHANGES_DIR / c.filename) + print(f'Removed {len(changes)} change files') + + # Commit and tag + run_git('add', CHANGELOG_FILE, RELEASE_JSON_FILE, CHANGES_DIR) + run_git('commit', '-m', f'chore: release {tag}') + run_git('tag', tag) + print(f'Created tag: {tag}') + print(f'Push with: git push origin {tag} && git push') + + +def main(): + parser = argparse.ArgumentParser(description='OpenShock firmware release helper') + parser.add_argument('--dry-run', action='store_true', help='Show what would happen without making changes') + sub = parser.add_subparsers(dest='command') + + sub.add_parser('status', help='Show pending changes and next version') + sub.add_parser('rc', help='Create or bump an RC tag') + sub.add_parser('stable', help='Promote to stable release') + + args = parser.parse_args() + + if args.command == 'status' or args.command is None: + return cmd_status(args) + elif args.command == 'rc': + return cmd_rc(args) + elif args.command == 'stable': + return cmd_stable(args) + + +if __name__ == '__main__': + sys.exit(main() or 0) From 1b014438e947c5df9cee4bd57ec0dbb37d823008 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Fri, 27 Mar 2026 00:17:30 +0100 Subject: [PATCH 2/3] ci: automate release tagging and changelog generation Add release.yml workflow: - Beta push: auto-creates RC tag (1.6.0-rc.N) - Master push: consumes .changes/, updates CHANGELOG.md, creates stable tag - Requires RELEASE_TOKEN PAT secret for cross-workflow triggering Update ci-build.yml: - Remove master/beta from push triggers (handled by tags from release.yml) - Keep develop push deploys and tag-triggered builds Update get-vars.js: - Only validate stable tags against CHANGELOG.md (skip RC/beta/dev) - RC tags no longer require changelog entries Update release.py: - Output release JSON to stdout for CI to send to repository API - No longer writes release.json to disk Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/scripts/get-vars.js | 33 +++++++++++++--------- .github/workflows/ci-build.yml | 2 -- .github/workflows/release.yml | 51 ++++++++++++++++++++++++++++++++++ scripts/release.py | 21 ++++++-------- 4 files changed, 79 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/scripts/get-vars.js b/.github/scripts/get-vars.js index 33aa0ae1..bff72c1a 100644 --- a/.github/scripts/get-vars.js +++ b/.github/scripts/get-vars.js @@ -111,11 +111,12 @@ if (gitHeadRefName === 'master' || (isGitTag && isStableRelease(latestRelease))) } function getVersionChangeLog(lines) { + const isStableTag = isGitTag && isStableRelease(latestRelease); const emptyChangelog = lines.length === 0; - // Enforce that the changelog is not empty if we are on the master branch - if (isGitTag && emptyChangelog) { - setFailed('File "CHANGELOG.md" is empty, this must be populated in the master branch'); + // Only stable tags require changelog entries + if (isStableTag && emptyChangelog) { + setFailed('File "CHANGELOG.md" is empty, this must be populated for stable releases'); process.exit(); } @@ -133,17 +134,22 @@ function getVersionChangeLog(lines) { // Get the start of the entry const changeLogBegin = lines.findIndex((line) => line.startsWith(`# Version ${currentVersion}`)); - if (isGitTag && changeLogBegin === -1) { + if (isStableTag && changeLogBegin === -1) { setFailed( - `File "CHANGELOG.md" does not contain a changelog entry for version "${currentVersion}", this must be added in the master branch` + `File "CHANGELOG.md" does not contain a changelog entry for version "${currentVersion}"` ); process.exit(); } - // Enforce that the changelog entry is at the top of the file if we are on the master branch - if (isGitTag && changeLogBegin !== 0) { + // RC/beta/dev tags may not have a changelog entry - that's fine + if (changeLogBegin === -1) { + return ''; + } + + // Enforce that the changelog entry is at the top of the file for stable releases + if (isStableTag && changeLogBegin !== 0) { setFailed( - `Changelog entry for version "${currentVersion}" is not at the top of the file, you tag is either out of date or you have not updated the changelog` + `Changelog entry for version "${currentVersion}" is not at the top of the file` ); process.exit(); } @@ -161,10 +167,9 @@ function getVersionChangeLog(lines) { const emptyChangelogEntry = lines.slice(changeLogBegin + 1, changeLogEnd).filter((line) => line.trim() !== '').length === 0; - // Enforce that the changelog entry is not empty if we are on the master branch - if (isGitTag && emptyChangelogEntry) { + if (isStableTag && emptyChangelogEntry) { setFailed( - `Changelog entry for version "${currentVersion}" is empty, this must be populated in the master branch` + `Changelog entry for version "${currentVersion}" is empty` ); process.exit(); } @@ -195,15 +200,17 @@ const changelogVersions = fullChangelogLines // Get the changelog for the current version const versionChangeLog = getVersionChangeLog(fullChangelogLines); -// Enforce that all tags exist in the changelog +// Enforce that all stable tags exist in the changelog (RC/beta/dev tags are excluded) let missingTags = []; for (const tag of gitTagsArray) { + const parsed = semver.parse(tag); + if (parsed && parsed.prerelease.length > 0) continue; // Skip pre-release tags if (!changelogVersions.includes(tag)) { missingTags.push(tag); } } if (missingTags.length > 0) { - setFailed(`Changelog is missing the following tags: ${missingTags.join(', ')}`); + setFailed(`Changelog is missing the following stable tags: ${missingTags.join(', ')}`); process.exit(); } diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 3471682a..c889642c 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -1,8 +1,6 @@ on: push: branches: - - master - - beta - develop tags: - '[0-9]+.[0-9]+.[0-9]+' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..dce341e9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,51 @@ +on: + push: + branches: + - master + - beta + +name: release + +env: + PYTHON_VERSION: '3.13' + +permissions: + contents: write + +jobs: + auto-release: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + # A PAT is required so the tag push triggers ci-build. + # The default GITHUB_TOKEN does not trigger downstream workflows. + token: ${{ secrets.RELEASE_TOKEN }} + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Create RC tag (beta) + if: github.ref == 'refs/heads/beta' + run: | + python scripts/release.py rc + TAG=$(git describe --tags --abbrev=0) + git push origin "$TAG" + echo "::notice::Created and pushed tag: $TAG" + + - name: Create stable release (master) + if: github.ref == 'refs/heads/master' + run: | + python scripts/release.py stable + TAG=$(git describe --tags --abbrev=0) + git push origin HEAD "$TAG" + echo "::notice::Created and pushed tag: $TAG" diff --git a/scripts/release.py b/scripts/release.py index 117f691b..c2c74773 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -24,7 +24,6 @@ CHANGES_DIR = '.changes' CHANGELOG_FILE = 'CHANGELOG.md' -RELEASE_JSON_FILE = 'release.json' BUMP_ORDER = {'patch': 0, 'minor': 1, 'major': 2} NOTICE_LEVELS = {'info', 'warning', 'error'} @@ -329,17 +328,13 @@ def cmd_stable(args): if args.dry_run: print(f'Would create tag: {tag}') print(f'\nChangelog entry:\n{entry}') - print(f'\nrelease.json:\n{json.dumps(release_data, indent=2)}') + print(f'\nrelease.json (for API):\n{json.dumps(release_data, indent=2)}') return root = get_project_root() - # Write release.json - release_json_path = root / RELEASE_JSON_FILE - with open(release_json_path, 'w', encoding='utf-8') as f: - json.dump(release_data, f, indent=2) - f.write('\n') - print(f'Wrote {RELEASE_JSON_FILE}') + # Print release JSON to stdout for CI to capture and send to the API + print(json.dumps(release_data, indent=2)) # Prepend to CHANGELOG.md changelog_path = root / CHANGELOG_FILE @@ -347,19 +342,19 @@ def cmd_stable(args): if changelog_path.exists(): existing = changelog_path.read_text(encoding='utf-8') changelog_path.write_text(entry + '\n' + existing, encoding='utf-8') - print(f'Updated {CHANGELOG_FILE}') + print(f'Updated {CHANGELOG_FILE}', file=sys.stderr) # Delete change files for c in changes: os.remove(root / CHANGES_DIR / c.filename) - print(f'Removed {len(changes)} change files') + print(f'Removed {len(changes)} change files', file=sys.stderr) # Commit and tag - run_git('add', CHANGELOG_FILE, RELEASE_JSON_FILE, CHANGES_DIR) + run_git('add', CHANGELOG_FILE, CHANGES_DIR) run_git('commit', '-m', f'chore: release {tag}') run_git('tag', tag) - print(f'Created tag: {tag}') - print(f'Push with: git push origin {tag} && git push') + print(f'Created tag: {tag}', file=sys.stderr) + print(f'Push with: git push origin {tag} && git push', file=sys.stderr) def main(): From e1a56fac2f876737d3c1a198330a4af635941bd6 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Fri, 27 Mar 2026 00:20:31 +0100 Subject: [PATCH 3/3] ci: add PR check for change files Require PRs into develop to include a .changes/*.md file. PRs labeled "no-changelog" are exempt (for dependency bumps, CI-only changes, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/check-changes.yml | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/check-changes.yml diff --git a/.github/workflows/check-changes.yml b/.github/workflows/check-changes.yml new file mode 100644 index 00000000..339f766b --- /dev/null +++ b/.github/workflows/check-changes.yml @@ -0,0 +1,36 @@ +on: + pull_request: + branches: + - develop + types: [opened, reopened, synchronize, labeled, unlabeled] + +name: check-changes + +jobs: + require-changefile: + runs-on: ubuntu-latest + timeout-minutes: 1 + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check for change file + run: | + # PRs labeled "no-changelog" are exempt (dependency bumps, CI-only, etc.) + if echo '${{ toJSON(github.event.pull_request.labels.*.name) }}' | grep -q '"no-changelog"'; then + echo "::notice::Skipping changelog check (no-changelog label)" + exit 0 + fi + + # Check if any .changes/*.md files were added (excluding README.md) + CHANGES=$(git diff --name-only --diff-filter=A origin/${{ github.base_ref }}...HEAD -- '.changes/*.md' | grep -v README.md || true) + + if [ -z "$CHANGES" ]; then + echo "::error::No change file found. Add a .changes/.md file describing your change, or add the 'no-changelog' label to skip this check. See .changes/README.md for the format." + exit 1 + fi + + echo "Found change files:" + echo "$CHANGES"