rastrillo / native Public

Clone
git clone https://amadan.net/rastrillo/native

Plain git — no account needed to clone.

Download

Download this file

1# Oficina Mac updates
2
3Meet, Calendar, Memoria, Docs and Sheets share `RastrilloMacUpdates`, a macOS-only
4Sparkle 2.9.6 integration. Each app owns one updater and puts Check for Updates
5and the automatic-check preference in its application menu. By default Sparkle asks
6before enabling automatic checks, and installation uses the standard
7confirmation/relaunch flow and the app's normal quit handling. iOS uses Apple's
8distribution mechanisms.
9
10## Quiet updates (opt-in)
11
12An app that must never be interrupted (Calls, during a call) opts into quiet
13updates in two places, and needs both:
14
15- `NativeUpdater(policy: .quiet)` in code. A scheduled check never opens a window.
16 A staged update waits for quit, and `pendingUpdate` becomes `.ready`. The app
17 menu gains "Update Ready — Restart <App>", and `NativeUpdateNotice` is a one-line
18 notice for the app's home screen. If the person has turned automatic downloads
19 off, a found update becomes `.available` and the notice offers it. Set
20 `isBusy` while the app must not be disturbed. Scheduled checks are then declined
21 and rerun once it clears, and the restart action is disabled. A declined check
22 also means no download during a call. Check for Updates always shows Sparkle's
23 standard UI, because the person asked for it. It is disabled while a staged
24 update waits: the Restart item replaces it.
25- `"install": "automatic"` in `mac-updates.json`. Packaging then writes
26 `SUAllowsAutomaticUpdates`, `SUEnableAutomaticChecks`, `SUAutomaticallyUpdate`
27 and a daily `SUScheduledCheckInterval` into Info.plist. There is no first-launch
28 permission question. These are defaults, so a person's own choice (the menu
29 toggle, or `automaticallyInstallsUpdates` in the app's settings) is stored in
30 their preferences and wins. The default, `"prompt"`, is the behaviour above,
31 and release and publication reject an archive whose Info.plist disagrees with
32 its configuration's mode.
33
34The installed build decides how an update arrives. A person running a build
35from before quiet updates sees Sparkle's usual prompts once more, for the update
36that brings quiet updates.
37
38The delegate methods are optional Objective-C selectors. A drifted Swift
39signature still compiles but is never called, and the app is back to Sparkle's
40alerts. `QuietUpdaterTests.testDelegateAnswersSparklesSelectors` pins them.
41
42Unconfigured development builds disable update controls. A release must pass its
43app-specific `native/mac-updates.json` to packaging. These files contain only the
44public Ed25519 key, Keychain account name, bundle ID and HTTPS appcast URL. The
45private Sparkle key stays in the login Keychain under the `oficina` account; it is
46separate from Apple signing and notarization credentials. Keep that key for future
47updates; rotating it requires a deliberate Sparkle key migration.
48
49## Build and notarize
50
51Use Team Tito Limited's Developer ID Application certificate, team `9U4A84C4WK`.
52The existing `slopbox-notary` Keychain profile authenticated successfully during
53setup; successful notarization of the actual release is the final validation.
54One team profile is reusable across the five apps. No credentials belong in Git.
55
56For the four standard apps, from their `native` directory:
57
58```sh
59scripts/build-mac.sh --configuration release --updates-config mac-updates.json \
60 --version 0.1.0 --build 2 \
61 --sign-identity 'Developer ID Application: Team Tito Limited (9U4A84C4WK)'
62```
63
64Meet uses `OFICINA_BUILD_CONFIGURATION=release` instead of `--configuration`.
65`OFICINA_NATIVE_PACKAGE_PATH` can select an unpublished local shared-library
66checkout for development; omit it to use the reviewed pinned dependency.
67
68`tools/release_mac.py` takes the built app, `--config`, `--sparkle-tools` (the
69Sparkle SwiftPM artifact's `bin` directory), `--identity`, `--team`,
70`--notary-profile` and a new `--output` directory. It copies the app, signs helpers
71and frameworks before the host, verifies the selected Developer ID team,
72notarizes and staples the app, checks Gatekeeper, then creates and EdDSA-signs the
73final ZIP. Meet receives camera and audio-input hardened-runtime entitlements.
74The result contains the ZIP and a `release.json` audit manifest. Nothing is
75installed or published by preparation.
76
77The initial binaries built here target Apple silicon. `generate_appcast` reads
78minimum macOS and architecture requirements from the archive, so incompatible
79machines are not offered these updates. Docs and Sheets require macOS 26;
80Calendar, Memoria and Meet require macOS 14.
81
82## Amadan hosting
83
84Use a dedicated **public, artifacts-only** repository `oficina/native-releases`.
85Do not change the visibility of any application source repository. Initialize its
86`main` branch and clone it before publishing. The proposed feeds are:
87
88```
89https://raw.amadan.net/oficina/native-releases/main/<app>/appcast.xml
90```
91
92Git holds the feeds and nothing else. Each archive is an Amadan release asset,
93stored in object storage, on a release tagged `<app>-<build>`:
94
95```
96https://amadan.net/oficina/native-releases/-/releases/<app>-<build>/<archive>.zip
97```
98
99That URL redirects to a short-lived signed download, which Sparkle follows.
100Never commit an archive: every zip in Git stays in history for good and is
101copied by every clone. On 2026-10-26 a repack of about 1.1 GB of them took
102amadan.net down for ten minutes (`oficina/native-releases#1`). Assets can be up
103to 500 MB each, and `release_mac.py` still caps archives at 100 MiB.
104
105Run `tools/publish_mac_update.py RELEASE --config CONFIG --repo ARTIFACT_CLONE
106--sparkle-tools TOOLS` to validate and print a plan. Add `--publish` only when the
107concrete release is ready to publish. It verifies the archive signature, actual
108archived app metadata, manifest, Keychain public key, monotonic build number,
109clean main checkout and correct origin. It pushes the release tag and uploads the
110archive with `amadan release create`. Then it downloads the archive anonymously
111and checks its length and SHA-256. Only after that does it generate and push the
112appcast, using Sparkle's official tool. The generated version must match the
113signed app. Existing feed items are preserved; deltas are disabled to keep
114upload sizes bounded.
115
116`amadan release create` needs writer access to the artifact repository. Set
117`AMADAN_HOME` to the publishing account's credentials (an agent publishes as its
118agent account), and use a Git credential for that same account to push the tag
119and feed.
120
121If publication stops before the feed commit, the old feed stays usable. Rerun the
122same command. The tag is reused, and an asset cannot be replaced, so the second
123upload is refused. The publisher accepts that refusal only when the public
124download already matches the signed archive, and then publishes the feed. A
125different archive for a build already uploaded needs a new build number. If it
126stopped while writing the feed, before its commit, discard the half-written feed
127and rerun. The publisher only starts on a clean checkout, so this removes nothing
128else: `git reset --hard HEAD`, then `git clean -f -- <app>/appcast.xml` for an
129app's first feed. If only the final feed push failed, the feed commit is already
130verified. Push it with `git push origin HEAD:refs/heads/main`, and do not rerun,
131because a rerun refuses a build the local feed already lists.
132
133The hub does not yet make an asset's bytes atomic against two simultaneous
134uploads of the same name. Only one publisher may run for a build at a time.
135Sparkle still checks each download's EdDSA signature, so replaced bytes fail
136to install rather than installing.
137
138Older feed items point at zips committed before this change, by full commit SHA
139on `raw.amadan.net`. They keep working. Removing those zips from history is a
140separate decision, for the repository's owner.
141
142Users of pre-Sparkle builds need one initial installation of a Sparkle-enabled
143release. Subsequent releases can update in place. The first publication should
144include a real update/relaunch check before broad distribution.
145
146## Validation
147
148`make ci` includes Swift tests, Mac/iOS companion builds, Go race tests, relocated
149resource packaging and release-boundary tests. Publication tests check that
150metadata tampering, archive tampering, failed public download, a refused upload
151without a matching asset and generated version mismatch cannot publish a feed.
152They also check that no archive is ever added to Git, and that the archive is
153uploaded and verified between the tag push and the feed push. Independent mutation probes confirmed these
154checks detect removal of the corresponding guards.
155