← back to Exo
Add GitHub releases with Sparkle release notes integration (#1172)
07cf2c1aa10bd73593afaca269f09eb585c1c665 · 2026-01-16 16:47:33 +0000 · Alex Cheema
## Motivation
Closes #1140
Currently releases are uploaded to S3 for Sparkle updates but there's no
GitHub Release created, and Sparkle update dialogs don't show release
notes. Users have no visibility into what changed.
## Changes
- Added release workflow documentation comment at top of `build-app.yml`
- Added "Fetch release notes for Sparkle" step that converts markdown
from draft GitHub release to HTML
- Added "Inject release notes into appcast" step that embeds HTML in
appcast.xml with CDATA
- Added "Publish GitHub Release" step that attaches DMG and publishes
the draft
## Why It Works
- Sparkle's `<description>` tag supports HTML wrapped in CDATA for
rendering in update dialogs
- GitHub's markdown API (`/markdown`) converts the release notes to HTML
with proper formatting
- Draft releases allow writing polished notes before the build, then the
workflow publishes them automatically
- The workflow fails if no draft release exists, ensuring release notes
are always provided
## Test Plan
### Manual Testing
1. Create a draft GitHub release for a new tag with markdown release
notes
2. Push the tag to trigger the workflow
3. Verify the GitHub release is published with DMG attached
4. Download appcast.xml from S3 and verify
`<description><![CDATA[...]]></description>` contains HTML
5. Test Sparkle update dialog on macOS to confirm release notes appear
### Automated Testing
No automated tests added - this is CI workflow configuration.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Files touched
M .github/workflows/build-app.ymlM app/EXO/EXO.xcodeproj/project.pbxprojM app/EXO/EXO.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Diff
commit 07cf2c1aa10bd73593afaca269f09eb585c1c665
Author: Alex Cheema <41707476+AlexCheema@users.noreply.github.com>
Date: Fri Jan 16 16:47:33 2026 +0000
Add GitHub releases with Sparkle release notes integration (#1172)
## Motivation
Closes #1140
Currently releases are uploaded to S3 for Sparkle updates but there's no
GitHub Release created, and Sparkle update dialogs don't show release
notes. Users have no visibility into what changed.
## Changes
- Added release workflow documentation comment at top of `build-app.yml`
- Added "Fetch release notes for Sparkle" step that converts markdown
from draft GitHub release to HTML
- Added "Inject release notes into appcast" step that embeds HTML in
appcast.xml with CDATA
- Added "Publish GitHub Release" step that attaches DMG and publishes
the draft
## Why It Works
- Sparkle's `<description>` tag supports HTML wrapped in CDATA for
rendering in update dialogs
- GitHub's markdown API (`/markdown`) converts the release notes to HTML
with proper formatting
- Draft releases allow writing polished notes before the build, then the
workflow publishes them automatically
- The workflow fails if no draft release exists, ensuring release notes
are always provided
## Test Plan
### Manual Testing
1. Create a draft GitHub release for a new tag with markdown release
notes
2. Push the tag to trigger the workflow
3. Verify the GitHub release is published with DMG attached
4. Download appcast.xml from S3 and verify
`<description><![CDATA[...]]></description>` contains HTML
5. Test Sparkle update dialog on macOS to confirm release notes appear
### Automated Testing
No automated tests added - this is CI workflow configuration.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
---
.github/workflows/build-app.yml | 99 +++++++++++++++++++++-
app/EXO/EXO.xcodeproj/project.pbxproj | 2 +-
.../xcshareddata/swiftpm/Package.resolved | 4 +-
3 files changed, 101 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml
index 542dc800..9a0a6b9e 100644
--- a/.github/workflows/build-app.yml
+++ b/.github/workflows/build-app.yml
@@ -1,5 +1,16 @@
name: Build EXO macOS DMG
+# Release workflow:
+# 1. Create a draft GitHub Release with the tag name (e.g. v1.0.0) and write release notes in markdown
+# 2. Push the tag: git tag v1.0.0 && git push origin v1.0.0
+# 3. This workflow builds, signs, and notarizes the DMG
+# 4. Release notes are embedded in appcast.xml for Sparkle (rendered as markdown)
+# 5. DMG and appcast.xml are uploaded to S3
+# 6. The draft GitHub Release is published with the DMG attached
+#
+# For alpha releases (e.g. v1.0.0-alpha.1): draft release and notes are optional.
+# If no draft exists, a release is auto-created with generated notes.
+
on:
workflow_dispatch:
push:
@@ -12,7 +23,7 @@ jobs:
build-macos-app:
runs-on: "macos-26"
env:
- SPARKLE_VERSION: 2.8.1
+ SPARKLE_VERSION: 2.9.0-beta.1
SPARKLE_DOWNLOAD_PREFIX: ${{ secrets.SPARKLE_DOWNLOAD_PREFIX }}
SPARKLE_FEED_URL: ${{ secrets.SPARKLE_FEED_URL }}
SPARKLE_ED25519_PUBLIC: ${{ secrets.SPARKLE_ED25519_PUBLIC }}
@@ -87,6 +98,47 @@ jobs:
exit 1
fi
+ - name: Fetch and validate release notes
+ if: github.ref_type == 'tag'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ # Find draft release - either by matching tag name or by finding untagged draft with matching name
+ RELEASE_JSON=$(gh api repos/${{ github.repository }}/releases --jq ".[] | select(.draft == true) | select(.tag_name == \"$GITHUB_REF_NAME\" or .tag_name == \"\" or .name == \"$GITHUB_REF_NAME\")" 2>/dev/null | head -1 || echo "")
+
+ if [[ -z "$RELEASE_JSON" ]]; then
+ if [[ "$IS_ALPHA" == "true" ]]; then
+ echo "No draft release found for alpha tag $GITHUB_REF_NAME (optional for alphas)"
+ echo "HAS_RELEASE_NOTES=false" >> $GITHUB_ENV
+ exit 0
+ fi
+ echo "ERROR: No draft release found for tag $GITHUB_REF_NAME"
+ echo "Please create a draft release with release notes before pushing the tag."
+ exit 1
+ fi
+
+ # Extract release notes
+ NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // ""')
+ if [[ -z "$NOTES" || "$NOTES" == "null" ]]; then
+ if [[ "$IS_ALPHA" == "true" ]]; then
+ echo "Draft release has no notes (optional for alphas)"
+ echo "HAS_RELEASE_NOTES=false" >> $GITHUB_ENV
+ exit 0
+ fi
+ echo "ERROR: Draft release exists but has no release notes"
+ echo "Please add release notes to the draft release before pushing the tag."
+ exit 1
+ fi
+
+ # Save release ID for later publishing
+ RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id')
+ echo "DRAFT_RELEASE_ID=$RELEASE_ID" >> $GITHUB_ENV
+ echo "HAS_RELEASE_NOTES=true" >> $GITHUB_ENV
+
+ echo "Found draft release (ID: $RELEASE_ID), saving release notes..."
+ echo "$NOTES" > /tmp/release_notes.md
+ echo "RELEASE_NOTES_FILE=/tmp/release_notes.md" >> $GITHUB_ENV
+
# ============================================================
# Install dependencies
# ============================================================
@@ -304,6 +356,28 @@ jobs:
$CHANNEL_FLAG \
.
+ - name: Inject release notes into appcast
+ if: github.ref_type == 'tag' && env.HAS_RELEASE_NOTES == 'true'
+ env:
+ RELEASE_VERSION: ${{ env.RELEASE_VERSION }}
+ run: |
+ # Inject markdown release notes with sparkle:format="markdown" (Sparkle 2.9+)
+ export NOTES=$(cat "$RELEASE_NOTES_FILE")
+
+ # Insert description after the enclosure tag for this version
+ awk '
+ /<enclosure[^>]*>/ && index($0, ENVIRON["RELEASE_VERSION"]) {
+ print
+ print " <description sparkle:format=\"markdown\"><![CDATA["
+ print ENVIRON["NOTES"]
+ print " ]]></description>"
+ next
+ }
+ { print }
+ ' output/appcast.xml > output/appcast.xml.tmp && mv output/appcast.xml.tmp output/appcast.xml
+
+ echo "Injected markdown release notes for version $RELEASE_VERSION"
+
# ============================================================
# Upload artifacts
# ============================================================
@@ -336,3 +410,26 @@ jobs:
aws s3 cp "$DMG_NAME" "s3://${SPARKLE_S3_BUCKET}/${PREFIX}EXO-latest.dmg"
aws s3 cp appcast.xml "s3://${SPARKLE_S3_BUCKET}/${PREFIX}appcast.xml" --content-type application/xml --cache-control no-cache
fi
+
+ - name: Publish GitHub Release
+ if: github.ref_type == 'tag'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ DMG_PATH="output/EXO-${RELEASE_VERSION}.dmg"
+
+ if [[ "$HAS_RELEASE_NOTES" == "true" ]]; then
+ # Update the draft release with the tag and upload DMG
+ gh api --method PATCH "repos/${{ github.repository }}/releases/$DRAFT_RELEASE_ID" \
+ -f tag_name="$GITHUB_REF_NAME" \
+ -F draft=false
+ gh release upload "$GITHUB_REF_NAME" "$DMG_PATH" --clobber
+ echo "Published release $GITHUB_REF_NAME with DMG attached"
+ else
+ # Alpha without draft release - create one with auto-generated notes
+ gh release create "$GITHUB_REF_NAME" "$DMG_PATH" \
+ --title "$GITHUB_REF_NAME" \
+ --generate-notes \
+ --prerelease
+ echo "Created alpha release $GITHUB_REF_NAME with auto-generated notes"
+ fi
diff --git a/app/EXO/EXO.xcodeproj/project.pbxproj b/app/EXO/EXO.xcodeproj/project.pbxproj
index 47553174..bd593086 100644
--- a/app/EXO/EXO.xcodeproj/project.pbxproj
+++ b/app/EXO/EXO.xcodeproj/project.pbxproj
@@ -585,7 +585,7 @@
repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 2.8.1;
+ minimumVersion = 2.9.0-beta.1;
};
};
/* End XCRemoteSwiftPackageReference section */
diff --git a/app/EXO/EXO.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/app/EXO/EXO.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 351bd259..d30fbdf9 100644
--- a/app/EXO/EXO.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/app/EXO/EXO.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git",
"state" : {
- "revision" : "5581748cef2bae787496fe6d61139aebe0a451f6",
- "version" : "2.8.1"
+ "revision" : "e641adb41915a8409895e2e30666aa64e487b637",
+ "version" : "2.9.0-beta.1"
}
}
],
← 83c5285a reduce logs
·
back to Exo
·
Fix draft release detection query (#1175) ae0a804c →