rastrillo / native Public
Oficina Mac updates
Meet, Calendar, Memoria, Docs and Sheets share RastrilloMacUpdates, a macOS-only Sparkle 2.9.6 integration. Each app owns one updater and puts Check for Updates and the automatic-check preference in its application menu. By default Sparkle asks before enabling automatic checks, and installation uses the standard confirmation/relaunch flow and the app's normal quit handling. iOS uses Apple's distribution mechanisms.
Quiet updates (opt-in)
An app that must never be interrupted (Calls, during a call) opts into quiet updates in two places, and needs both:
NativeUpdater(policy: .quiet)in code. A scheduled check never opens a window. A staged update waits for quit, andpendingUpdatebecomes.ready. The app menu gains "Update Ready — Restart <App>", andNativeUpdateNoticeis a one-line notice for the app's home screen. If the person has turned automatic downloads off, a found update becomes.availableand the notice offers it. SetisBusywhile the app must not be disturbed. Scheduled checks are then declined and rerun once it clears, and the restart action is disabled. A declined check also means no download during a call. Check for Updates always shows Sparkle's standard UI, because the person asked for it. It is disabled while a staged update waits: the Restart item replaces it."install": "automatic"inmac-updates.json. Packaging then writesSUAllowsAutomaticUpdates,SUEnableAutomaticChecks,SUAutomaticallyUpdateand a dailySUScheduledCheckIntervalinto Info.plist. There is no first-launch permission question. These are defaults, so a person's own choice (the menu toggle, orautomaticallyInstallsUpdatesin the app's settings) is stored in their preferences and wins. The default,"prompt", is the behaviour above, and release and publication reject an archive whose Info.plist disagrees with its configuration's mode.
The installed build decides how an update arrives. A person running a build from before quiet updates sees Sparkle's usual prompts once more, for the update that brings quiet updates.
The delegate methods are optional Objective-C selectors. A drifted Swift signature still compiles but is never called, and the app is back to Sparkle's alerts. QuietUpdaterTests.testDelegateAnswersSparklesSelectors pins them.
Unconfigured development builds disable update controls. A release must pass its app-specific native/mac-updates.json to packaging. These files contain only the public Ed25519 key, Keychain account name, bundle ID and HTTPS appcast URL. The private Sparkle key stays in the login Keychain under the oficina account; it is separate from Apple signing and notarization credentials. Keep that key for future updates; rotating it requires a deliberate Sparkle key migration.
Build and notarize
Use Team Tito Limited's Developer ID Application certificate, team 9U4A84C4WK. The existing slopbox-notary Keychain profile authenticated successfully during setup; successful notarization of the actual release is the final validation. One team profile is reusable across the five apps. No credentials belong in Git.
For the four standard apps, from their native directory:
scripts/build-mac.sh --configuration release --updates-config mac-updates.json \
--version 0.1.0 --build 2 \
--sign-identity 'Developer ID Application: Team Tito Limited (9U4A84C4WK)'Meet uses OFICINA_BUILD_CONFIGURATION=release instead of --configuration. OFICINA_NATIVE_PACKAGE_PATH can select an unpublished local shared-library checkout for development; omit it to use the reviewed pinned dependency.
tools/release_mac.py takes the built app, --config, --sparkle-tools (the Sparkle SwiftPM artifact's bin directory), --identity, --team, --notary-profile and a new --output directory. It copies the app, signs helpers and frameworks before the host, verifies the selected Developer ID team, notarizes and staples the app, checks Gatekeeper, then creates and EdDSA-signs the final ZIP. Meet receives camera and audio-input hardened-runtime entitlements. The result contains the ZIP and a release.json audit manifest. Nothing is installed or published by preparation.
The initial binaries built here target Apple silicon. generate_appcast reads minimum macOS and architecture requirements from the archive, so incompatible machines are not offered these updates. Docs and Sheets require macOS 26; Calendar, Memoria and Meet require macOS 14.
Amadan hosting
Use a dedicated public, artifacts-only repository oficina/native-releases. Do not change the visibility of any application source repository. Initialize its main branch and clone it before publishing. The proposed feeds are:
https://raw.amadan.net/oficina/native-releases/main/<app>/appcast.xmlGit holds the feeds and nothing else. Each archive is an Amadan release asset, stored in object storage, on a release tagged <app>-<build>:
https://amadan.net/oficina/native-releases/-/releases/<app>-<build>/<archive>.zipThat URL redirects to a short-lived signed download, which Sparkle follows. Never commit an archive: every zip in Git stays in history for good and is copied by every clone. On 2026-10-26 a repack of about 1.1 GB of them took amadan.net down for ten minutes (oficina/native-releases#1). Assets can be up to 500 MB each, and release_mac.py still caps archives at 100 MiB.
Run tools/publish_mac_update.py RELEASE --config CONFIG --repo ARTIFACT_CLONE --sparkle-tools TOOLS to validate and print a plan. Add --publish only when the concrete release is ready to publish. It verifies the archive signature, actual archived app metadata, manifest, Keychain public key, monotonic build number, clean main checkout and correct origin. It pushes the release tag and uploads the archive with amadan release create. Then it downloads the archive anonymously and checks its length and SHA-256. Only after that does it generate and push the appcast, using Sparkle's official tool. The generated version must match the signed app. Existing feed items are preserved; deltas are disabled to keep upload sizes bounded.
amadan release create needs writer access to the artifact repository. Set AMADAN_HOME to the publishing account's credentials (an agent publishes as its agent account), and use a Git credential for that same account to push the tag and feed.
If publication stops before the feed commit, the old feed stays usable. Rerun the same command. The tag is reused, and an asset cannot be replaced, so the second upload is refused. The publisher accepts that refusal only when the public download already matches the signed archive, and then publishes the feed. A different archive for a build already uploaded needs a new build number. If it stopped while writing the feed, before its commit, discard the half-written feed and rerun. The publisher only starts on a clean checkout, so this removes nothing else: git reset --hard HEAD, then git clean -f -- <app>/appcast.xml for an app's first feed. If only the final feed push failed, the feed commit is already verified. Push it with git push origin HEAD:refs/heads/main, and do not rerun, because a rerun refuses a build the local feed already lists.
The hub does not yet make an asset's bytes atomic against two simultaneous uploads of the same name. Only one publisher may run for a build at a time. Sparkle still checks each download's EdDSA signature, so replaced bytes fail to install rather than installing.
Older feed items point at zips committed before this change, by full commit SHA on raw.amadan.net. They keep working. Removing those zips from history is a separate decision, for the repository's owner.
Users of pre-Sparkle builds need one initial installation of a Sparkle-enabled release. Subsequent releases can update in place. The first publication should include a real update/relaunch check before broad distribution.
Validation
make ci includes Swift tests, Mac/iOS companion builds, Go race tests, relocated resource packaging and release-boundary tests. Publication tests check that metadata tampering, archive tampering, failed public download, a refused upload without a matching asset and generated version mismatch cannot publish a feed. They also check that no archive is ever added to Git, and that the archive is uploaded and verified between the tag push and the feed push. Independent mutation probes confirmed these checks detect removal of the corresponding guards.