eleven / messenger Public

mac: embed the changelog in Sparkle's in-app "What's new" (#209)

release-mac.sh shipped a generic one-line note ("Eleven X for macOS.")
and nothing in the in-app updater. Add optional LCHAT_RELEASE_NOTES_HTML
/ _MD: the HTML is embedded inline as the appcast item's <description>
(embed-release-notes.py) so Sparkle renders it in the updater, and the
markdown becomes the GitHub release page body.

Inline beats a <sparkle:releaseNotesLink> here: no dependence on GitHub's
application/octet-stream content-type for release assets, and no 404 once
a newer release takes over the /latest/download/ path. The signed
<enclosure> is left untouched, so re-running is safe.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paul Campbell 3ad21005dfd261862b7af43b740bdd1a2033c648 parent a3e2c08
2 files changed, +58 −1
  • ios/embed-release-notes.py +38 −0
  • ios/release-mac.sh +20 −1
diff --git a/ios/embed-release-notes.py b/ios/embed-release-notes.py
new file mode 100644
index 0000000..0883ef7
--- /dev/null
+++ b/ios/embed-release-notes.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+# Embed release-notes HTML as an inline <description> on the appcast item whose
+# sparkle:shortVersionString matches, and drop any sparkle:releaseNotesLink.
+#
+# Why inline rather than a <sparkle:releaseNotesLink>: Sparkle renders an inline
+# <description> straight in the updater with no extra network fetch, so it
+# doesn't depend on the notes host's content-type (GitHub serves release assets
+# as application/octet-stream) and never 404s the way GitHub's /latest/download/
+# link does once a newer release becomes "latest". The HTML is XML-escaped here,
+# which Sparkle's parser decodes back to markup before rendering — functionally
+# identical to a CDATA block. The signed <enclosure> is left untouched.
+#
+# Usage: embed-release-notes.py <appcast.xml> <short-version> <notes.html>
+import sys, xml.etree.ElementTree as ET
+
+appcast, version, notes_path = sys.argv[1], sys.argv[2], sys.argv[3]
+SPARKLE = "http://www.andymatuschak.org/xml-namespaces/sparkle"
+ET.register_namespace("sparkle", SPARKLE)
+
+with open(notes_path, encoding="utf-8") as f:
+ notes = f.read()
+
+tree = ET.parse(appcast)
+matched = False
+for item in tree.getroot().iter("item"):
+ sv = item.find(f"{{{SPARKLE}}}shortVersionString")
+ if sv is None or sv.text != version:
+ continue
+ for tag in (f"{{{SPARKLE}}}releaseNotesLink", "description"):
+ for el in item.findall(tag):
+ item.remove(el)
+ ET.SubElement(item, "description").text = notes
+ matched = True
+
+if not matched:
+ sys.exit(f"embed-release-notes: no appcast item with shortVersionString {version}")
+
+tree.write(appcast, encoding="UTF-8", xml_declaration=True)
diff --git a/ios/release-mac.sh b/ios/release-mac.sh
index 11235d2..8d14f5b 100755
--- a/ios/release-mac.sh
+++ b/ios/release-mac.sh
@@ -43,6 +43,14 @@ APP_NAME="Eleven"
# Keep in sync with the `packages.Sparkle.exactVersion` pin in ios/project.yml
# — generate_appcast's flags are matched against this exact release's --help.
SPARKLE_VERSION="2.9.4"
+
+# Optional release notes ("what's new"), surfaced two ways. The HTML is embedded
+# inline as the appcast item's <description> (see embed-release-notes.py) so it
+# renders in Sparkle's in-app updater; the markdown is used as the GitHub release
+# page body. Either var may be unset — that channel falls back to a generic
+# one-liner.
+NOTES_HTML="${LCHAT_RELEASE_NOTES_HTML:-}"
+NOTES_MD="${LCHAT_RELEASE_NOTES_MD:-}"
command -v dmgbuild >/dev/null || {
echo "error: dmgbuild not found — install it with: pip install dmgbuild" >&2
exit 1
@@ -134,8 +142,19 @@ chmod +x "$TMP/sparkle-tools/bin/generate_appcast"
--maximum-deltas 0 \
"$APPCAST_DIR"
+# Embed the "what's new" HTML inline as this item's <description> so it renders
+# in the in-app updater (see embed-release-notes.py for why inline beats a link).
+if [ -n "$NOTES_HTML" ]; then
+ python3 "$IOS/embed-release-notes.py" "$APPCAST_DIR/appcast.xml" "$VERSION" "$NOTES_HTML"
+fi
+
+# Publish the DMG + appcast; use the markdown notes (when provided) as the
+# GitHub release page body, else a generic one-liner.
+NOTES_ARG=(--notes "$APP_NAME $VERSION for macOS.")
+[ -n "$NOTES_MD" ] && NOTES_ARG=(--notes-file "$NOTES_MD")
+
gh release create "v$VERSION" -R "$RELEASES_REPO" \
- --title "$APP_NAME $VERSION" --notes "$APP_NAME $VERSION for macOS." \
+ --title "$APP_NAME $VERSION" "${NOTES_ARG[@]}" \
"$APPCAST_DIR/$APP_NAME-$VERSION.dmg" "$APPCAST_DIR/appcast.xml"
echo