docs/operations/releases.md
Releases
rac is released from GitHub Actions with the Build macOS binaries of
Helium workflow (.github/workflows/build.yml). It builds Apple Silicon
by default (Intel or both are options), signs and packages each disk image,
writes the update feeds, and publishes a GitHub release. Installed copies
of rac find the new version through that release. How updates work, and why,
is in internals/overview.md.
The build phases only compile. They run on the Runner input's machine
and get no signing or notarization secrets; the phase that finishes uploads
the unsigned app. A last sign job in building.yml, always on GitHub's
own macos-latest, then:
- checks the binaries for rac's developer switches (the G6 gate, below);
- imports the certificate into a temporary keychain, which is deleted when the job ends, even if it fails;
- signs, notarizes and staples the app, packages the disk image, then signs, notarizes and staples the disk image too;
- mounts the disk image and checks it (
codesign --verify --deep --strict,spctlsayingNotarized Developer IDfor the app and the disk image,stapler validate, the signing team, and no entitlement that needs a provisioning profile it doesn't have). The mount has no quarantine flag, so this doesn't replay a first launch from a browser; - makes the update deltas.
The release job also runs on GitHub's runner, because it holds the update
signing key. So moving the compile to another provider never moves a key.
Before it writes the update feeds, it runs the same disk image check on every
downloaded image (notarized builds only), so an artifact swapped after the
sign job isn't signed into a feed.
The one thing a build phase learns about the entitlements profile is whether
it exists: the settings job in building.yml turns that into a plain
true or false for RAC_BRANDED_ENTITLEMENTS. The profile itself only
reaches the sign job.
One-time setup
Make an update signing key:
sh1 python3 -m venv /tmp/sparkle-venv 2 /tmp/sparkle-venv/bin/pip install cryptography 3 /tmp/sparkle-venv/bin/python devutils/rac/sparkle.py keygen Keep a copy of both lines somewhere safe, such as a password manager. If the private key is lost, every installed copy of rac stops accepting updates, and people have to download rac again by hand.
Add these repository secrets in GitHub (Settings > Secrets and variables > Actions):
Secret Value PROD_MACOS_SPARKLE_ED_PUB_KEYThe key after public:fromkeygen, without the labelRAC_SPARKLE_ED_PRIVATE_KEYThe key after private:fromkeygen, without the labelFor signed and notarized builds, add the Apple Developer secrets too:
PROD_MACOS_CERTIFICATE(the Developer ID certificate as base64.p12),PROD_MACOS_CERTIFICATE_PWD,PROD_MACOS_CERTIFICATE_NAME,PROD_MACOS_CI_KEYCHAIN_PWD(any new password),PROD_MACOS_NOTARIZATION_APPLE_ID,PROD_MACOS_NOTARIZATION_TEAM_ID, andPROD_MACOS_NOTARIZATION_PWD(an app-specific password). Only thesignjob reads them. Without them, CI signs ad hoc, and macOS blocks the first launch of the downloaded app until it's allowed in System Settings > Privacy & Security. Updates still work, because Sparkle checks rac's own signature.PROD_MACOS_SPECIAL_ENTITLEMENTS_PROFILE_B64(a base64 provisioning profile) is optional; without it the build has no branded entitlements, so passkeys saved with Touch ID or in iCloud Keychain, and other features that need keychain access groups, are unavailable. Before adding it, setMAC_TEAM_IDinrac/branding/identity.patch(it's empty), or the app claims an application identifier with no team and won't launch. Thesignjob fails on that rather than ship it.
Never change the signing key once rac is released. Builds only accept updates signed with the key they were built with.
The website
Do this once, when the first release is out. Until then the site's download page is hidden and shows sample data.
- Set
RELEASE_READYtotrueinsite/src/config.ts. The download buttons and the nav then go to/download, search engines may index it, and it shows the latest release instead of sample data. - Add a
GITHUB_TOKENenvironment variable to the Vercel project: a fine-grained token with no permissions beyond public repositories. The site reads the latest release from GitHub's API at build time, and without a token, builds share a low rate limit and can fail. - Rebuild the site after each release, so the download page shows it. In
Vercel, create a deploy hook for
main(Settings > Git > Deploy Hooks), save its URL as the repository secretVERCEL_DEPLOY_HOOK, and add a last step to the release job in.github/workflows/build.yml:yaml1 - name: Rebuild the website 2 run: curl -fsS -X POST "${{ secrets.VERCEL_DEPLOY_HOOK }}"
Making a release
- Raise the version in
rac_version.txtand commit it. Use a patch bump (0.1.1) for fixes, a minor bump (0.2.0) for features or a Helium sync. The workflow stops if the version isn't higher than every earlier release. - In GitHub, open Actions > Build macOS binaries of Helium > Run
workflow, choose
main, and check Create a release after build is done. Architectures defaults toarm64; choosex86_64orbothto build Intel too. A full build takes several hours per architecture. One architecture failing doesn't cancel the other. If a build phase can't download the previous phase's checkpoint, it retries twice before failing. If only thesignjob fails (a notarization timeout, say), use Re-run failed jobs: the unsigned app is kept, so nothing is compiled again. - When it finishes, check the new release. It should have a disk image and
appcast-<arch>.xmlfor each architecture you built, and a.deltafile from each of the last few releases (none on the first). A release only has the feeds of the architectures it built, so installed copies of an architecture that was left out stop seeing updates until a release includes it again. It's tagged with the version, such as0.1.1, on the commit the workflow was started from. - Open https://racbrowser.com/download and check that it shows the new version.
Installed copies check the feed at launch and then hourly, download the update in
the background, and install it when rac quits. The feed's address
(notkainoa/rac) is built into rac, in rac/updates/update-feed.patch.
Don't mark a good release as a prerelease or a draft. GitHub's latest
link skips them, so nobody would get the update. (Marking a bad one is the
point of the next section.)
If a release is bad
Whether a release is bad is the maintainer's call. An agent flips a release only on their word.
- Un-publish it fast. On GitHub, edit the bad release and mark it a
pre-release.
releases/latestthen points back at the previous release, so no new install or update is offered the bad one. Copies that already updated keep it. - Never delete a published tag: installed copies may have updated to it,
and versions in
rac_version.txtonly go up. Fix forward instead — raise the version and put out a higher release, within 48 hours when the bad one breaks the browser. - When the bad build can't launch at all, its users have to download rac again by hand. Post a note on the release and on the site, both through the maintainer.
- Afterwards, read the live feed (and
appcast-x86_64.xmlif you built Intel) and confirm it names the release before the bad one:Copies already on the bad version ignore that feed, because Sparkle only offers a higher version. They wait for the fix. Then rebuild the website (see The website), so its download page, which readssh1 curl -fsSL https://github.com/notkainoa/rac/releases/latest/download/appcast-arm64.xml releases/latest, stops showing the bad release.
Before a release goes out, or when one looks wrong, run the Release smoke
tests workflow (.github/workflows/smoke.yml) from Actions > Release
smoke tests > Run workflow. Give it a build run's ID to test its
disk-image artifact, or a release tag to test a published one. Add an
earlier release tag to run the update check too, and tick Require
Gatekeeper approval for signed and notarized builds. Each job uploads its
summary and the window screenshot it captured as an artifact. The update
check swaps the old app bundle for the new one, which proves profile
carry-over and version ordering — it doesn't drive Sparkle's download and
install, which stays the by-hand check under Testing an update
locally.
The developer-switch gate (G6)
Release builds must not contain rac's developer switches
(--rac-agent-debug, --rac-agent-mcp-echo, --rac-window-screenshot,
--rac-launch-in-background). Before signing, the sign job runs
.github/scripts/github_check_release_binary.sh, which reads every Mach-O
file in the app with strings and names each binary and match it finds.
The workflow's Developer switches (G6) input decides what a match does:
warn (the default) annotates the run and carries on; enforce fails the
build. It stays warn until the switches are compiled only into dev builds;
then change the input's default in build.yml to enforce.
Testing an update locally
Dev builds don't include the updater. To try a change to it:
- Make a throwaway key with
/tmp/sparkle-venv/bin/python devutils/rac/sparkle.py keygen(the venv from the one-time setup). - Add to
build/src/out/Default/args.gn:Then build. Remove the lines and rebuild when you're done.1 enable_sparkle=true 2 sparkle_ed_key="<public key>" - Make a newer copy of
rac.app: raiseCFBundleVersionandCFBundleShortVersionStringin itsInfo.plist, runcodesign --force --deep --sign -on it, and put it in a disk image withhdiutil create. - Write a feed for it, with the private key in
RAC_SPARKLE_ED_PRIVATE_KEY:Serve it withsh1 /tmp/sparkle-venv/bin/python devutils/rac/sparkle.py appcast --arch arm64 --dmg <dmg> \ 2 --base-url http://127.0.0.1:8765/ --version <new version> \ 3 --out <folder>/appcast-arm64.xml python3 -m http.server 8765 --bind 127.0.0.1 --directory <folder>. - Copy the current
rac.appto another name insidebuild/src/out/Default(the copy only works there, because the dev build loads its libraries from that folder). Launch it with a throwaway profile and the local feed:sh1 build/src/out/Default/<copy>.app/Contents/MacOS/rac --user-data-dir=/tmp/<profile> \ 2 --custom-update-server-url=http://127.0.0.1:8765/ - Quit the copy once About rac says to relaunch. Don't click Relaunch:
it starts rac without
--user-data-dir, which opens the real profile.
Sparkle stores its state in the me.kainoa.rac defaults and in
~/Library/Caches/me.kainoa.rac. Afterward, remove the SU* keys
(defaults delete me.kainoa.rac SULastCheckTime, and so on) so a later
test starts fresh.