Blog
How Beacon ships a release, and how this site proves it
Beacon is a keyboard-first desktop API client for macOS, and I build it on my own. That constraint shapes the release process more than anything else: if shipping a version takes an afternoon of careful clicking, sooner or later I will click something wrong on a Friday evening and someone will download it.
So here is what happens between a tagged build and the app opening on your Mac — and then the part I find more interesting: what has to agree before this site may describe the release at all.
Three ways in, in one deliberate order
There are three, and the order they appear in on the download page is not cosmetic.
First, the install script:
curl -fsSL https://raw.githubusercontent.com/quizuncle/beacon-releases/HEAD/install.sh | bash
Second, Homebrew:
brew tap quizuncle/beacon && brew install --cask beacon
Third, downloading a disk image by hand and dragging Beacon into Applications.
The script leads because it needs nothing from you: it picks the right build for your machine and clears the quarantine flag itself, so the app opens the first time you double-click it. Homebrew does the same two things through the cask. The manual route is the one that leaves you with a job to finish, which is why it is last.
If your reaction to that first command is “I am not piping that to bash unseen” — good. The download page links the script next to the command for exactly that reason, and the next section is what it does, line by line.
What the install script actually does
None of this needs taking on trust: install.sh is about eighty lines of plain bash and lists what it does in a comment at its own top. Read the steps below against the script, not against me. In order, it:
- detects your Mac’s chip with
uname -m— Apple Silicon or Intel; - asks the GitHub releases API for the latest release and picks the
arm64disk image or the non-arm64one to match; - downloads it into a temporary directory that it deletes on exit;
- mounts it with
hdiutil attach; - removes any
Beacon.appalready in/Applications, copies the new one in, and unmounts; - runs
xattr -cron what it just installed.
That last step is the whole reason the script exists. Everything else is a download you could have done yourself.
Why macOS calls a fresh download “damaged”
Apple charges $99 a year for the Developer ID needed to notarize apps distributed outside the Mac App Store. Beacon is a free, independent project without that subscription, so macOS shows its standard warning for any unsigned app from the internet — usually “Beacon.app is damaged and can’t be opened”. The app is not damaged; that is simply what macOS says about anything unsigned.
Both the script and Homebrew clear the quarantine attribute as part of installing, so most people never see it. The manual disk-image route is the one that hits it, because dragging an app out of a mounted image leaves the attribute attached. One command fixes it, once:
xattr -cr /Applications/Beacon.app
It does not disable a security check. It tells macOS you have looked at this and trust it, which is the same thing the right-click-Open flow does through the interface. The download page has the longer version.
Two builds, two checksums
Every release publishes two disk images — one for Apple Silicon, one for Intel — because they are genuinely different files, and each carries its own SHA-256. Version 0.2.0 is current as I write this, and it needs macOS 12 (Monterey) or later.
They are published with the release on GitHub and repeated on the download page, so verifying a manual download is one command:
shasum -a 256 ~/Downloads/Beacon-0.2.0-arm64.dmg
The release checks this site runs against itself
Publishing a release and updating this site are two different acts, and the second one is where a lie can creep in without anyone lying. A checksum on a marketing page is a claim about a file somewhere else. Nothing about publishing the page checks that the claim is still true.
So the site’s release step is a script — npm run sync:site -- --version 0.2.0 — that
pulls the release data, verifies it, and only then builds and tests, stopping at the first
hard failure. Two of its checks exist because of specific ways this goes quietly wrong.
The version assertion. The sync reads GitHub’s /releases/latest, which excludes
drafts and prereleases. Publish a release as either one and the sync still succeeds —
cheerfully writing the previous release’s version, URLs and hashes into the site. Nothing
throws. The only way to detect it is to compare against a version supplied from outside,
which is why --version is required and has no default. Typing the number is the check.
The checksum cross-check. The Homebrew cask carries a SHA-256 per architecture,
computed locally with shasum when the images were built. This site carries a SHA-256 per
architecture, taken from GitHub’s own digest for the uploaded asset. Two values, derived
independently, so agreement between them is real evidence rather than a copy of a copy.
They are compared per architecture, never as a set. A cask with its two hashes transposed holds exactly the same set of digests as the site, so a set comparison passes it — while every user, on either architecture, gets a checksum failure. The cask is bumped from positional arm64-then-intel arguments, so that transposition is one slip, not a hypothetical. For the same reason, an architecture whose digest cannot be located on either side counts as a failure: falling back to a looser comparison would restore the blind spot the pairing exists to remove.
Only if all of that passes does it build the site and run the test suite, in that order. That ordering is load-bearing too: the first thing the suite checks is that the built output is newer than every source file, so the tests cannot grade a stale build and report green about the previous version of the site.
And the comparison functions themselves have unit tests, including the transposed-hash case. A check nobody has ever watched fail is a check nobody knows works.
What that actually buys you
When you copy a checksum off the download page and it matches the file you downloaded, that is not because I was careful. It is because two values produced by different machines in different ways were compared before the deploy was allowed to proceed, and it would have stopped if they disagreed.
None of this is clever. It is a few hundred lines of scripts whose only real principle is that a failed check must never read as a clean result. That is worth more to me than any amount of release-day care, because care is the thing that runs out on a Friday evening.
The app is on the download page; what changed in each version is on the changelog.