App updates
Once someone installs your desktop app, you will want to ship them fixes without asking them to download it again by hand. RubyEverywhere apps update themselves from an S3-compatible bucket you own: AWS S3, Cloudflare R2, Backblaze B2, MinIO, or anything else that speaks the S3 API.
This page covers doing it yourself with the every CLI. If you build on the
hosted Platform, it can
publish releases for you into the same kind of
bucket, in the same format.
How it works
An update is two things in your bucket:
- The signed app artifact.
- An update manifest, a small JSON file naming the latest version, its URL, and its signature.
On launch, and on a timer after that, the installed app fetches
{updates.url}/{channel}/{os}/{arch}/latest.json, compares versions, and if
there is a newer one it downloads the artifact, checks the signature, and
installs it. Because updates are signed with a key that never touches the
bucket, a bucket compromise cannot push a bad build to your users.
Updates need code signing too. The updater installs a real macOS app bundle, so set signing up first.
1. Mint a signing key
The update signature is separate from your Apple certificate. Mint the keypair once per app:
every updates keygen
The secret key lands in ~/.rubyeverywhere/keys/<bundle_id>.key, never in your
repo, and the command prints the block to paste into everywhere.yml. It offers
to encrypt the key with a passphrase; say yes unless a CI job needs to sign
unattended.
Back the secret key up. Losing it strands every shipped app on its current version, because an app trusts exactly one public key and refuses an update signed with any other.
2. Configure the feed
Add an updates: block with the public base URL the app should check (your
bucket's public endpoint, an r2.dev domain, a CloudFront distribution, or a
custom domain in front of one) and the public key from step 1:
app:
name: Notes
bundle_id: com.example.notes
mode: local
updates:
url: https://updates.example.com/notes
public_key: "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3"
channel: stable # the channel this build polls
auto: check # off | check (notify) | download | install (silent)
interval: 21600 # seconds between background checks, minimum 300
s3: # publish-side only; never ships in the app
bucket: my-app-releases
endpoint: https://<account>.r2.cloudflarestorage.com
region: auto
# prefix: notes # optional key prefix inside the bucket
# force_path_style: true # for MinIO and similar
Both url and public_key must be set or the updater is left out of the build
entirely. url must be https, apart from loopback for local testing: the
shell refuses to fetch an http feed and stops checking, so the app would ship
looking fine and never update.
All of this is baked into the build, so set it before the version you want people to update from.
Omit endpoint for AWS S3 and set a real region instead.
3. Give the CLI write access
Publishing needs write credentials for the bucket. every publish reads the
standard AWS variables, so keep them out of your repo:
export AWS_ACCESS_KEY_ID="โฆ"
export AWS_SECRET_ACCESS_KEY="โฆ"
# export AWS_SESSION_TOKEN="โฆ" # only for temporary credentials
If your signing key has a passphrase, add
EVERYWHERE_SIGNING_KEY_PASSPHRASE. In CI, where there is no
~/.rubyeverywhere, point at the key with --key, EVERYWHERE_SIGNING_KEY_FILE
(a path), or EVERYWHERE_SIGNING_KEY (the key itself).
4. Publish a release
Cut a signed, notarized release, then publish it. every release writes
dist/release.json, and that receipt is exactly what every publish reads:
every release
every publish
โ Signing Notes-1.4.0.zip
โ signed and self-verified against updates.public_key
โ Publishing 1.4.0 to stable/macos/arm64
โ uploading Notes-1.4.0.zip (24.7MB)
Notes-latest.zip alias refreshed
โ latest.json โ 1.4.0
โ Published ๐
โ Notes 1.4.0 is live on the stable channel
feed https://updates.example.com/notes/stable/macos/arm64/latest.json
4 versions in the index ยท shipped apps pick this up within 360 minutes
Bump app.version for each release. Publishing the same version twice with
different bytes is refused unless you pass --force.
Add release notes with --notes "โฆ" or --notes-file CHANGELOG.md. They reach
users as plain text in the shell's update dialog and as HTML for an in-app
changelog.
Bucket layout
You never manage these by hand, but for reference every publish writes:
my-app-releases/
stable/macos/arm64/
latest.json # the manifest the app polls
index.json # every version published to this channel and target
Notes-1.4.0.zip # the artifact
Notes-1.4.0.zip.minisig
Notes-1.3.0.zip # older releases, kept for rollback
Notes-latest.zip # a stable download link, refreshed each release
Each channel and target gets its own directory, so channels are parallel
manifests in the same bucket. Add updates.s3.prefix to nest all of it under
one key prefix.
Artifacts are immutable and cached forever; latest.json, index.json and the
alias are marked no-cache. Keep the bucket, or its public URL, readable by
anyone: the artifacts are signed, so serving them publicly is safe.
Rolling back
If a release turns out to be bad, point the manifest back at a version already in the bucket:
every publish --rollback-to 1.3.0
Nothing is deleted, and the artifact is already there, so this is fast. Apps that already took 1.4.0 will not downgrade, so ship a higher version to move everyone off the bad build.
Update channels
Feeds are split by channel: stable by default, plus any others you publish to,
like beta or nightly. Set the build's default with updates.channel, and
publish elsewhere with every publish --channel beta.
Users can switch channels at runtime through the bridge, so you need no separate beta build:
Everywhere.updates.channel // what the shell checks now
await Everywhere.updates.setChannel("beta") // persisted across relaunches
await Everywhere.updates.check() // scan the new channel right away
The shell saves the choice in the app's data directory, where it overrides
everywhere.yml from then on. Because your settings page is server-rendered,
you decide who even sees a channel picker: render the beta option only for
users in your beta group and the shell honours whatever they pick. Switching
back to stable never downgrades; the app stays where it is until stable
moves past it.