6.4 KiB
Releasing and updates
Frame Control checks for updates itself. The desktop app offers a new version only once it's GitHub's latest release, and drafts and pre-releases never count. So a build reaches people only when you publish it, after testing it.
Steps
-
Bump
versioninapp/package.json, commit, and push a tag:git tag v0.4.0 && git push origin v0.4.0.github/workflows/release.ymlbuilds macOS, Windows and Linux, and attaches everything to a draft release for that tag. After all ten installers upload, the workflow also attachesSHA256SUMSbuilt from GitHub's SHA-256 digests of those uploaded bytes. Nobody is offered a draft. Editing a draft while installers are uploading can make GitHub temporarily show its tag asuntagged-…. The workflow finds that draft by its exact version title, restores the tag association and retries the affected upload; it refuses ambiguous matches. The Linux build runsscripts/check-appimage-launch.pybefore upload. It starts the built x86_64 AppImage on an isolated Xvfb display, checks that its bundled server answers, and checks that the server stops on exit. The check disables telemetry and update requests, and keeps extraction in its temporary profile. This CI test does not test a physical display, FUSE mount or a headset. -
Independently download and inspect the draft's installers on an x86_64 Linux host with
gh,dpkg-debandxvfb-runinstalled:scripts/audit-release-assets.py v0.4.0 \ --report /tmp/frame-v0.4.0-assets.json --launch-x86-appimageThe command downloads each of the ten installers by asset ID, verifies its size and SHA-256 against GitHub metadata, matches the downloaded
SHA256SUMSbyte for byte, checks package formats and versions, and starts the x86_64 AppImage with a temporary profile and isolated Xvfb display. It writes the report only after all checks pass. This does not prove that the Mac or Windows installers launch on physical machines. Test those packages separately when the required machines are available. An installed copy of the previous version won't offer the draft, so install it directly. -
Write the release notes on the draft. The update banner links to them.
-
Publish:
Confirm the exact tag commit's main CI and e2e, the locked real Frame smoke, release verification, independent package checks, and all four tagged installer builds have passed. Hold the release if any gate is failed, cancelled, skipped, still running, or unverified. The publisher also reads the exact commit's GitHub CI, native Decky runtime gate, and tagged build job records and refuses to publish unless every required job succeeded; it cannot prove physical headset or installer behavior on its own.
scripts/publish-release.sh --dry-run v0.4.0 # checks and shows update.json, changes nothing scripts/publish-release.sh v0.4.0The script checks that the tag is on GitHub, that exact-commit main CI (including e2e), native amd64/ARM64 Decky runtime checks, and the tagged release workflow are green with no failed, cancelled, skipped or running jobs in any run attempt, and that all ten installers are attached, each with the SHA-256 digest GitHub records. It also checks that the draft does not begin with a
WITHDRAWNnotice, so the preserved v0.4.48 draft cannot accidentally be offered again. It checks that the uploadedSHA256SUMSdigest matches a fresh list of those ten installer digests. It attachesupdate.json(the version, the notes, the release page and each installer's digest), verifies the uploaded asset's SHA-256 and downloads its bytes for an exact readback before publishing the release and marking it latest. It refuses an already published release before replacing its live update manifest; use a new tested release for changes after publication. It uses only the REST API:gh release viewcan't find a draft whosetag_namestill readsuntagged-…, and GraphQL is often rate-limited. Such a draft is found by its exact title (Frame Control 0.4.0, or that followed by: subtitle) and tied to the tag when it's published. The release page inupdate.jsonis alwaysreleases/tag/<tag>, because a draft's own address (releases/tag/untagged-…) stops working once it's published. From then on, running copies see the update. They check about 8 seconds after starting, then every 6 hours, and anyone can use Check for Updates… (the app menu on macOS, the Help menu elsewhere).After publication, rerun
audit-release-assets.pywith a new report path. For a published release it also downloadsupdate.json, verifies its GitHub SHA-256, and checks that the offered version, release page, notes, installer names, sizes, and digests match that release. This checks the updater channel's published bytes, which the draft audit cannot inspect.
To pull a bad release, mark the previous one as latest
(gh release edit v0.3.9 --latest) or turn the bad one back into a draft.
Copies that already updated stay on it. Nothing downgrades them.
How a copy updates itself
app/updater.js reads update.json from
github.com/saphid/frame-control/releases/latest/download/. It falls back to the
REST API only when a release has no manifest, because the API allows just 60
unauthenticated requests an hour per IP address, shared by a whole household.
Then it downloads the installer for its platform and checks it
against the SHA-256 digest GitHub publishes for the asset. It refuses if the
digest is missing or doesn't match. Then:
| Installed from | Update |
|---|---|
macOS .dmg, app in a writable folder such as Applications |
The .zip is unpacked next to the app and its version checked. After the app quits, a small script swaps the new app in, putting the old one back if that fails, and reopens it. Updates don't get the download quarantine, so there's no xattr step. |
| Windows installer | The new Setup runs silently over the install (/S --force-run) and reopens the app. |
| Linux AppImage | The new AppImage replaces the old file and is started. |
macOS app still on the disk image or translocated, Windows .zip, Linux .deb |
The banner opens the release page instead. |
Version 0.3.1 and earlier have no updater, so people on them have to download the new version once by hand.