From 12420187a324998de83f0af2511bbc3115d696ca Mon Sep 17 00:00:00 2001 From: ryanbr Date: Tue, 7 Jul 2026 16:19:37 +1200 Subject: [PATCH 1/2] Release workflow: append GitHub's auto "What's Changed" PR breakdown to the notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each release's GitHub body is now: the curated docs/releases/.md notes + GitHub's auto-generated "What's Changed" breakdown (every merged PR since the previous RELEASE, with authors/links + New Contributors + full-changelog compare link) + the downloads footer. Uses the generate-notes REST API (deterministic; not dependent on gh flag-combining) diffed from the most recent NON-prerelease release (so testing-latest snapshots are ignored). Skipped on the very first release (no prior tag would diff to repo start). Best-effort — a failure is ignored. Verified: the API returns the PR list (#1..#9); first-release PREV detection returns empty as expected. --- .github/workflows/fork-release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index 91233d9185..ee6a8500ac 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -99,6 +99,21 @@ jobs: echo "::warning::No ${NOTES_MD} — write the release notes there first. Using a placeholder." printf 'NOOP **%s** — release build.\n' "$NEW" > "$BODY" fi + # Auto "What's Changed" breakdown: GitHub's generate-notes API lists every merged PR since the + # previous RELEASE (non-prerelease, so testing-latest snapshots are ignored), + contributors + + # a full-changelog compare link. Appended below the curated notes. Skipped on the very first + # release (no prior tag → the diff would run to repo start). Best-effort: a failure is ignored. + PREV=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 30 \ + --json tagName,isPrerelease,isDraft \ + --jq 'map(select(.isPrerelease==false and .isDraft==false)) | .[0].tagName // empty' 2>/dev/null || true) + if [ -n "$PREV" ]; then + AUTO=$(gh api --method POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -f tag_name="$TAG" -f previous_tag_name="$PREV" -f target_commitish="$(git rev-parse HEAD)" \ + --jq '.body' 2>/dev/null || true) + [ -n "$AUTO" ] && printf '\n---\n\n%s\n' "$AUTO" >> "$BODY" + else + echo "First release (no prior release tag) — skipping the auto PR breakdown." + fi { printf '\n---\n\n**Downloads** — NOOP Staging identity, installs **beside** the official app (separate data):\n' printf -- '- **Android** — `app-full-release-v%s.apk` (`com.noop.whoop.staging`)\n' "$NEW" From 9e372720b453de90c1aa255523fa2d493e330fc0 Mon Sep 17 00:00:00 2001 From: ryanbr Date: Tue, 7 Jul 2026 16:31:56 +1200 Subject: [PATCH 2/2] Automate the in-app "What's New": generate AppChangelog from docs/releases front-matter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-app What's New (AppChangelog) was two hand-edits kept byte-identical across Kotlin + Swift, plus a manual CURRENT_VERSION bump. Now a single source drives it: - docs/releases/v.md can carry a `whatsnew:` YAML front-matter block (title, date, items) — the curated in-app copy — above the full GitHub release notes. - Tools/appchangelog-gen.py reads it and prepends the generated Release entry to BOTH AppChangelog.kt and AppChangelog.swift (per-language escaping, so parity is guaranteed since both come from one source) and bumps CURRENT_VERSION/currentVersion. Idempotent (re-run only refreshes the constant). - fork-release.yml strips the front-matter when using docs/releases/.md as the GitHub release body, so the yaml never leaks into the release notes (files without front-matter pass through unchanged). Also brings the in-app changelog current: added the v8.2.2 entry (it was stuck at 7.9.0) by running the generator. Verified: Android compiles, entries are byte-parallel on both platforms, idempotent re-run keeps one entry, and the GitHub-body strip is confirmed both ways. --- .github/workflows/fork-release.yml | 4 +- Strand/System/AppChangelog.swift | 14 ++- Tools/appchangelog-gen.py | 114 ++++++++++++++++++ .../src/main/java/com/noop/ui/AppChangelog.kt | 14 ++- docs/releases/v8.2.2.md | 11 ++ 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 Tools/appchangelog-gen.py diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index ee6a8500ac..0e6f8216b1 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -94,7 +94,9 @@ jobs: BODY=$(mktemp) NOTES_MD="docs/releases/${TAG}.md" if [ -f "$NOTES_MD" ]; then - cat "$NOTES_MD" > "$BODY" + # Strip a leading YAML front-matter block (the `whatsnew:` used by appchangelog-gen) so it + # never leaks into the GitHub release body; files without front-matter pass through unchanged. + awk 'NR==1 && $0=="---"{fm=1; next} fm && $0=="---"{fm=0; next} !fm{print}' "$NOTES_MD" > "$BODY" else echo "::warning::No ${NOTES_MD} — write the release notes there first. Using a placeholder." printf 'NOOP **%s** — release build.\n' "$NEW" > "$BODY" diff --git a/Strand/System/AppChangelog.swift b/Strand/System/AppChangelog.swift index 3cf8423cc3..89a5e12d58 100644 --- a/Strand/System/AppChangelog.swift +++ b/Strand/System/AppChangelog.swift @@ -7,7 +7,7 @@ enum AppChangelog { /// Bump this when you add a release below. The "What's New" sheet shows automatically when the /// stored last-seen version is behind this. (Decoupled from the bundle version on purpose.) - static let currentVersion = "7.9.0" + static let currentVersion = "8.2.2" struct Release: Identifiable { let version: String @@ -19,6 +19,18 @@ enum AppChangelog { /// Newest first. static let releases: [Release] = [ + Release( + version: "8.2.2", + title: "Steadier connections, fixes, and a nicer sleep view", + date: "July 2026", + items: [ + "**Steadier Bluetooth.** A dropped strap reconnects without tearing down a live one, and the HR re-broadcast survives a Bluetooth toggle.", + "**Sharper HRV.** R-R intervals are rounded to match the other paths, and HRV windows need a few clean beats before they count.", + "**No more crash loops.** A corrupt local database sets the bad file aside and rebuilds a clean one instead of crashing on every launch.", + "**Fresh-install fix.** Your WHOOP shows up in the Devices list on a brand-new install.", + "**Readable in light mode.** Charge / Effort / Rest labels stay legible in both themes, and more of the app is translated.", + ] + ), Release( version: "7.9.0", title: "Coupled view, workouts rebuilt, journal numbers", diff --git a/Tools/appchangelog-gen.py b/Tools/appchangelog-gen.py new file mode 100644 index 0000000000..bfd46dc3b2 --- /dev/null +++ b/Tools/appchangelog-gen.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Generate the in-app "What's New" entry (AppChangelog) for BOTH platforms from a release file's +front-matter, so the Kotlin and Swift entries stay byte-identical and the version bump is automatic. + +A per-version notes file docs/releases/v.md may carry a YAML front-matter block: + + --- + whatsnew: + title: "Short headline for the in-app card" + date: "July 2026" + items: + - "**Bold lead.** One-line description." + - "**Another.** ..." + --- + # NOOP v + + +Running `Tools/appchangelog-gen.py docs/releases/v8.2.2.md` prepends the generated Release entry to +`releases` in AppChangelog.kt AND AppChangelog.swift and bumps CURRENT_VERSION/currentVersion to that +version. Idempotent: if the version is already the newest entry it only re-checks the constant. The +version comes from the filename (v8.2.2.md -> 8.2.2). +""" +import re +import sys +import pathlib + +try: + import yaml +except ImportError: + sys.exit("appchangelog-gen: needs PyYAML (pip install pyyaml)") + +ROOT = pathlib.Path(__file__).resolve().parent.parent +KT = ROOT / "android/app/src/main/java/com/noop/ui/AppChangelog.kt" +SW = ROOT / "Strand/System/AppChangelog.swift" + + +def frontmatter(md: pathlib.Path) -> dict: + m = re.match(r"^---\n(.*?)\n---\n", md.read_text(), re.S) + if not m: + sys.exit(f"appchangelog-gen: no YAML front-matter in {md}") + wn = (yaml.safe_load(m.group(1)) or {}).get("whatsnew") + if not (wn and wn.get("title") and wn.get("date") and wn.get("items")): + sys.exit(f"appchangelog-gen: front-matter needs whatsnew.{{title,date,items}} in {md}") + return wn + + +def esc_kt(s: str) -> str: + return s.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$") + + +def esc_sw(s: str) -> str: + return s.replace("\\", "\\\\").replace('"', '\\"') + + +def kt_block(ver, wn): + items = "\n".join(f' "{esc_kt(i)}",' for i in wn["items"]) + return ( + " Release(\n" + f' version = "{ver}",\n' + f' title = "{esc_kt(wn["title"])}",\n' + f' date = "{esc_kt(wn["date"])}",\n' + " items = listOf(\n" + f"{items}\n" + " ),\n" + " ),\n" + ) + + +def sw_block(ver, wn): + items = "\n".join(f' "{esc_sw(i)}",' for i in wn["items"]) + return ( + " Release(\n" + f' version: "{ver}",\n' + f' title: "{esc_sw(wn["title"])}",\n' + f' date: "{esc_sw(wn["date"])}",\n' + " items: [\n" + f"{items}\n" + " ]\n" + " ),\n" + ) + + +def apply(path, anchor, block, ver, const_re, const_new): + text = path.read_text() + idx = text.index(anchor) + len(anchor) + already = f'version = "{ver}"' in text[idx:idx + 400] or f'version: "{ver}"' in text[idx:idx + 400] + if already: + print(f" {path.name}: v{ver} already the newest entry — leaving entries, refreshing constant") + else: + text = text[:idx] + block + text[idx:] + text, n = re.subn(const_re, const_new, text, count=1) + if n != 1: + sys.exit(f"appchangelog-gen: could not bump the version constant in {path.name}") + path.write_text(text) + if not already: + print(f" {path.name}: inserted v{ver} entry + set constant") + + +def main(): + if len(sys.argv) != 2: + sys.exit("usage: appchangelog-gen.py docs/releases/v.md") + md = pathlib.Path(sys.argv[1]) + ver = md.stem.lstrip("vV") + wn = frontmatter(md) + print(f"appchangelog-gen: v{ver} — {wn['title']}") + apply(KT, "val releases: List = listOf(\n", kt_block(ver, wn), ver, + r'(const val CURRENT_VERSION = ")[^"]*(")', rf'\g<1>{ver}\g<2>') + apply(SW, "static let releases: [Release] = [\n", sw_block(ver, wn), ver, + r'(static let currentVersion = ")[^"]*(")', rf'\g<1>{ver}\g<2>') + print("appchangelog-gen: done. Review the diff, then compile.") + + +if __name__ == "__main__": + main() diff --git a/android/app/src/main/java/com/noop/ui/AppChangelog.kt b/android/app/src/main/java/com/noop/ui/AppChangelog.kt index 76cc09bc64..784999f39f 100644 --- a/android/app/src/main/java/com/noop/ui/AppChangelog.kt +++ b/android/app/src/main/java/com/noop/ui/AppChangelog.kt @@ -25,7 +25,7 @@ object AppChangelog { * Bump this when you add a release below. The "What's New" sheet shows automatically when the * stored last-seen version is behind this. (Decoupled from the bundle version on purpose.) */ - const val CURRENT_VERSION = "7.9.0" + const val CURRENT_VERSION = "8.2.2" data class Release( val version: String, @@ -36,6 +36,18 @@ object AppChangelog { /** Newest first. */ val releases: List = listOf( + Release( + version = "8.2.2", + title = "Steadier connections, fixes, and a nicer sleep view", + date = "July 2026", + items = listOf( + "**Steadier Bluetooth.** A dropped strap reconnects without tearing down a live one, and the HR re-broadcast survives a Bluetooth toggle.", + "**Sharper HRV.** R-R intervals are rounded to match the other paths, and HRV windows need a few clean beats before they count.", + "**No more crash loops.** A corrupt local database sets the bad file aside and rebuilds a clean one instead of crashing on every launch.", + "**Fresh-install fix.** Your WHOOP shows up in the Devices list on a brand-new install.", + "**Readable in light mode.** Charge / Effort / Rest labels stay legible in both themes, and more of the app is translated.", + ), + ), Release( version = "7.9.0", title = "Coupled view, workouts rebuilt, journal numbers", diff --git a/docs/releases/v8.2.2.md b/docs/releases/v8.2.2.md index db8c7d6b27..e76491af5c 100644 --- a/docs/releases/v8.2.2.md +++ b/docs/releases/v8.2.2.md @@ -1,3 +1,14 @@ +--- +whatsnew: + title: "Steadier connections, fixes, and a nicer sleep view" + date: "July 2026" + items: + - "**Steadier Bluetooth.** A dropped strap reconnects without tearing down a live one, and the HR re-broadcast survives a Bluetooth toggle." + - "**Sharper HRV.** R-R intervals are rounded to match the other paths, and HRV windows need a few clean beats before they count." + - "**No more crash loops.** A corrupt local database sets the bad file aside and rebuilds a clean one instead of crashing on every launch." + - "**Fresh-install fix.** Your WHOOP shows up in the Devices list on a brand-new install." + - "**Readable in light mode.** Charge / Effort / Rest labels stay legible in both themes, and more of the app is translated." +--- # NOOP v8.2.2 A reliability and polish release. Steadier Bluetooth, a stack of fixes from your reports, more of the app translated, and a nicer sleep view. Update from the Releases page, AltStore, or `brew upgrade`.