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>
2 files changed,
+58
−1
ios/embed-release-notes.py+38 −0ios/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 |