Move from S3
For macOS apps that use Sparkle with an appcast.xml you generate yourself (for example with generate_appcast) and upload to S3, R2 or another bucket. After the move, ReleaseFlow builds the appcast from your GitHub releases, and existing installs keep updating through the old URL until they run a build that points at ReleaseFlow.
What changes and what stays
- Stays: your EdDSA key pair and the
SUPublicEDKeyinInfo.plist. Sparkle checks the update archive against that key, not against the server it came from, so builds signed with the same private key keep verifying. - Changes: builds go to GitHub Releases instead of the bucket. The feed is
https://releaseflow.net/{username}/{slug}/sparkle/appcast.xmland its<enclosure>URLs point athttps://releaseflow.net/{username}/{slug}/updates/download/{version}/{fileName}, which streams the asset from GitHub (private repositories included). - Generated for you: one
<item>per enabled, non-draft release with a macOS asset, withsparkle:version,sparkle:shortVersionString,length,sparkle:edSignature, release notes from the GitHub release body and, if set on the app,sparkle:minimumSystemVersion.
1. Create the Sparkle app
- Open /apps/new, choose Sparkle, pick a slug and select the GitHub repository that will hold the releases.
- Paste the same base64 public key you have in
SUPublicEDKeyinto EdDSA Public Key, and set Minimum System Version if your appcast items carried one. It applies to every item in the feed. - If you ran a separate beta appcast, turn on the beta channel in the app settings. Beta builds then live at
https://releaseflow.net/{username}/{slug}/sparkle/appcast.xml?channel=beta. - Keep Public (enable downloads/updates) on. The feed returns
404for apps that are not public.
2. Publish builds to GitHub Releases
Tag the release with the version (v1.3.0 or 1.3.0; a leading v is dropped). Tags containing alpha go to the alpha channel, tags containing beta or rc to beta, everything else to stable. Upload the update archive and its signature side by side:
# Same key pair as before: the public key is already in SUPublicEDKey.
# -p prints only the base64 signature (88 characters), which ReleaseFlow reads.
./bin/sign_update -p MyApp.zip > MyApp.zip.sig
gh release create v1.3.0 MyApp.zip MyApp.zip.sig --notes-file NOTES.md- ReleaseFlow reads
MyApp.zip.sigas thesparkle:edSignatureforMyApp.zip. Usesign_update -pso the file holds only the signature, not thesparkle:edSignature="…" length="…"fragment. - A
.zipis preferred over a.dmgfor the enclosure. A.zipcounts as macOS when its name containsmacordarwin, or when it has a.signext to it. - Only the releases you want offered need to be on GitHub. Older versions can stay in the bucket; Sparkle only installs the newest item it accepts.
Creating the app installs a GitHub release webhook on the repository when ReleaseFlow can, and every feed request re-syncs releases older than an hour. Press Sync Releases on the app page to import right away.
3. Match sparkle:version to CFBundleVersion
Sparkle compares sparkle:version with the installed app’s CFBundleVersion. ReleaseFlow derives it from the tag with one of two strategies:
SEMVER(default):1.3.0→1.3.0;1.3.0-beta.2→1.3.0b2.NUMERIC_BUILD: major × 10000 + minor × 100 + patch, so1.3.0→10300;1.3.0-beta.2→10300b2.
If your builds used an incrementing build number such as 1248, a SEMVER feed would offer 1.3.0, which Sparkle treats as lower than 1248, and nobody would update. Pick the strategy whose numbers are higher than every CFBundleVersion already in the wild, and set CFBundleVersion of new builds to the same value. The strategy is set through the REST API with an API key from /settings/api-keys:
# Find the app id
curl -s https://releaseflow.net/api/rest/apps/{slug} \
-H "x-api-key: $RELEASEFLOW_API_KEY"
# Emit numeric build versions (1.3.0 -> 10300) instead of 1.3.0
curl -s -X PATCH https://releaseflow.net/api/rest/apps/{id} \
-H "x-api-key: $RELEASEFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sparkleVersionStrategy":"NUMERIC_BUILD"}'Then compare the old and new feeds:
curl -s https://YOUR-BUCKET.s3.amazonaws.com/appcast.xml \
| grep -oE 'sparkle:(shortVersionString|version)="[^"]*"' | head -4
# ?sync=1 is honoured only for the app owner, so send your API key with it
curl -s "https://releaseflow.net/{username}/{slug}/sparkle/appcast.xml?sync=1" -H "x-api-key: $RELEASEFLOW_API_KEY" \
| grep -oE 'sparkle:(shortVersionString|version)="[^"]*"' | head -44. Verify the feed and signatures
Do this before any user can reach the new feed. The appcast is cached for 5 minutes. ?sync=1 starts a sync in the background, but only for the app owner (signed in, or with the owner’s API key in x-api-key); for anyone else it is ignored. Re-request after a few seconds if a new release is missing.
curl -s https://releaseflow.net/{username}/{slug}/sparkle/appcast.xml -o appcast.xml
grep -oE '(url|length|sparkle:edSignature)="[^"]*"' appcast.xml | head -3
# Download the enclosure URL and check it against the signature
curl -sL -o MyApp.zip "https://releaseflow.net/{username}/{slug}/updates/download/1.3.0/MyApp.zip"
stat -f%z MyApp.zip # must equal length=
./bin/sign_update --verify MyApp.zip "SIGNATURE_FROM_APPCAST"- Every item you expect has a
sparkle:edSignature. If it is missing, the.sigwas not in the release when it synced, or it was not a 64-byte base64 signature: upload a correct one, press Sync Releases, check again. - To keep unsigned releases out of the feed entirely, turn on Require EdDSA signature (Sparkle) in the app settings. The newest signed release then stays the update.
sign_update --verifyexits with an error when the archive does not match the signature for your key. Run it on the machine that holds the signing key.lengthequals the downloaded file size.
5. Keep the old feed URL working
Shipped builds read SUFeedURL from their own Info.plist, so the old URL has to keep answering until those builds are gone. Back up the current file first:
aws s3 cp s3://YOUR-BUCKET/appcast.xml appcast.backup.xmlOption A: mirror the ReleaseFlow feed into the bucket
Works for every bucket URL, including plain https://BUCKET.s3.amazonaws.com/appcast.xml. S3 REST endpoints do not redirect, and S3 website endpoints, which do, have no HTTPS. Run this after each release or on a schedule:
#!/bin/sh
# Copy the ReleaseFlow feed over the old S3 object. Run after each release
# (or on a schedule). Refuses to publish a feed without an <enclosure>.
set -e
curl -fsS https://releaseflow.net/{username}/{slug}/sparkle/appcast.xml -o appcast.xml
grep -q '<enclosure' appcast.xml
aws s3 cp appcast.xml s3://YOUR-BUCKET/appcast.xml \
--content-type "application/xml; charset=utf-8" \
--cache-control "max-age=300"The copied file still points its enclosures at ReleaseFlow, so old installs download the new build from ReleaseFlow and verify it with the key they already have.
Option B: redirect at the CDN
If the old URL is served by CloudFront or another CDN on a domain you control, answer it with a 301 to the ReleaseFlow appcast:
// CloudFront Function (viewer request) on the distribution serving the old URL
function handler(event) {
if (event.request.uri === "/appcast.xml") {
return {
statusCode: 301,
statusDescription: "Moved Permanently",
headers: {
location: {
value: "https://releaseflow.net/{username}/{slug}/sparkle/appcast.xml",
},
},
};
}
return event.request;
}Check it with curl -sIL on the old URL, then on one Mac running a shipped build before relying on it. Keep the redirect HTTPS-to-HTTPS.
6. Roll out
- Publish the next version to GitHub (step 2) while the bucket feed is still untouched, and verify it (step 4).
- Test on one Mac with a shipped build: point it at ReleaseFlow with
defaults write com.example.MyApp SUFeedURL "https://releaseflow.net/{username}/{slug}/sparkle/appcast.xml", choose Check for Updates…, install, thendefaults delete com.example.MyApp SUFeedURL. Sparkle logs a warning about a feed URL in user defaults; that is expected for this test. - Switch the old URL over (step 5, A or B).
- Ship a build whose
Info.plistpoints at ReleaseFlow directly, withSUPublicEDKeyunchanged:<key>SUFeedURL</key> <string>https://releaseflow.net/{username}/{slug}/sparkle/appcast.xml</string> <!-- Keep the existing SUPublicEDKey unchanged --> - Keep the mirror or redirect in place while older versions are still in use.
To pull a bad release, turn its switch off on the app page: it drops out of the feed (allow up to 5 minutes for caches) and its update download returns 403. To undo the whole move, restore the backup:
aws s3 cp appcast.backup.xml s3://YOUR-BUCKET/appcast.xml \
--content-type "application/xml; charset=utf-8"Limits
- The appcast is not signed. Apps with
SURequireSignedFeedenabled will reject it, mirrored or not; keep those on a feed you sign yourself. - No
sparkle:phasedRolloutInterval,sparkle:criticalUpdate,sparkle:channelor delta updates are emitted. Use the beta channel and the release switch to stage releases; users download the full archive. - Only GitHub Releases are read. Builds have to be attached to a GitHub release to appear in the feed.
Sources
- Sparkle: publishing an update
- Sparkle: customization (SUFeedURL, SUPublicEDKey)
- Amazon S3: website endpoints vs REST endpoints
Next: move from Hazel or Nuts. Feed reference and troubleshooting: /help.