Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/fork-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,28 @@ 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"
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"
Expand Down
14 changes: 13 additions & 1 deletion Strand/System/AppChangelog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down
114 changes: 114 additions & 0 deletions Tools/appchangelog-gen.py
Original file line number Diff line number Diff line change
@@ -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<VER>.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<VER>
<the full release notes — the GitHub release body; the front-matter is stripped there>

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<VER>.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<Release> = 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()
14 changes: 13 additions & 1 deletion android/app/src/main/java/com/noop/ui/AppChangelog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,6 +36,18 @@ object AppChangelog {

/** Newest first. */
val releases: List<Release> = 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",
Expand Down
11 changes: 11 additions & 0 deletions docs/releases/v8.2.2.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
Loading