releaseflow
PricingDocsLog in

Move from S3

Docs

  • Overview
  • Authentication
  • Quickstart
  • Sparkle (macOS)
  • Electron
  • License keys
  • Channels
  • From S3 feed
  • From Hazel / Nuts
  • API reference

On this page

  • What changes
  • 1. Create the app
  • 2. Publish to GitHub
  • 3. Match versions
  • 4. Verify signatures
  • 5. Keep the old URL
  • 6. Roll out
  • Limits

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 SUPublicEDKey in Info.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.xml and its <enclosure> URLs point at https://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, with sparkle: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

  1. Open /apps/new, choose Sparkle, pick a slug and select the GitHub repository that will hold the releases.
  2. Paste the same base64 public key you have in SUPublicEDKey into EdDSA Public Key, and set Minimum System Version if your appcast items carried one. It applies to every item in the feed.
  3. 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.
  4. Keep Public (enable downloads/updates) on. The feed returns 404 for 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.sig as the sparkle:edSignature for MyApp.zip. Use sign_update -p so the file holds only the signature, not the sparkle:edSignature="…" length="…" fragment.
  • A .zip is preferred over a .dmg for the enclosure. A .zip counts as macOS when its name contains mac or darwin, or when it has a .sig next 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, so 1.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 -4

4. 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 .sig was 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 --verify exits with an error when the archive does not match the signature for your key. Run it on the machine that holds the signing key.
  • length equals 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.xml

Option 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

  1. Publish the next version to GitHub (step 2) while the bucket feed is still untouched, and verify it (step 4).
  2. 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, then defaults delete com.example.MyApp SUFeedURL. Sparkle logs a warning about a feed URL in user defaults; that is expected for this test.
  3. Switch the old URL over (step 5, A or B).
  4. Ship a build whose Info.plist points at ReleaseFlow directly, with SUPublicEDKey unchanged:
    <key>SUFeedURL</key>
    <string>https://releaseflow.net/{username}/{slug}/sparkle/appcast.xml</string>
    <!-- Keep the existing SUPublicEDKey unchanged -->
  5. 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 SURequireSignedFeed enabled will reject it, mirrored or not; keep those on a feed you sign yourself.
  • No sparkle:phasedRolloutInterval, sparkle:criticalUpdate, sparkle:channel or 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.

← ChannelsFrom Hazel / Nuts →
releaseflow
PrivacyTermsDocsChangelogSupport