A version number looks like a label, so it gets typed wherever it is needed: into the bundle metadata, into the download file name, into the release notes, into the changelog. Four copies of one string, none of which checks the others. When they disagree — and eventually they do — the disagreement is discovered by someone downloading a file, not by the build.
The fix is not a convention. It is making the version an input that everything else derives from, and making the build fail when it is malformed.
One file, checked at the door
The repository contains a single VERSION file. The shared build metadata reads it, strips the line ending, and refuses to continue unless it matches a three-part semantic version. That check runs before anything is compiled, which means a typo in a version is a build failure rather than a release with three different numbers in it.
Everything downstream reads that value: the macOS bundle's short version and build version are patched into the property list at build time, the output directory is named after it, the disk image name contains it and the architecture, the source archive is named after it, and the release page is created for the tag that matches it. Nothing accepts a version as a parameter, because a parameter is an opportunity to pass the wrong one.
What an artefact should be able to answer
A downloaded file should be able to answer three questions without a network connection: what is this, what version is it, and is it the file you published?
The first two come from metadata inside the bundle. The third comes from a checksum file published next to the binaries, covering both platforms and the source archive in one list. A checksum is a weak guarantee — it proves the file matches what the publisher had, not that the publisher is trustworthy — but it is a guarantee a user can check, which is more than most download pages offer.
Packaging is where the truth leaks
Two leaks are worth designing against, and neither is caught by compiling successfully.
The first is your own machine. A compiler will happily embed absolute paths from the build directory into a binary, and those paths contain a username. Passing source and debug prefix maps to the compiler rewrites them to a relative path, so the shipped binary does not carry a directory tree that only existed on one laptop. It costs two flags and one line of build configuration.
The second is everything else you did not mean to publish. A small script scans source files and, separately, the built application bundle, for home directory paths, temporary paths, private keys, access tokens, authenticated URLs, private IP addresses and email addresses that are not on an explicit allowlist. It runs as part of the normal test target and again during packaging. Finding a token in a release is not a mistake you want to discover from a stranger's email.
The same step is where the boring obligations live: the licence file and the bilingual installation instructions are copied into the package rather than left in the repository, because the person opening a disk image has not cloned anything.
Say what you did not sign
Both platforms ship without the kind of signature that removes a warning. macOS builds are ad-hoc signed, not notarised; the Windows installer is not Authenticode-signed and will trigger SmartScreen. That is a distribution decision made with open eyes, and the release notes state it in the same section as the feature list, next to the instruction to verify checksums rather than disable a system protection.
A warning that users are told about in advance is a cost. A warning that appears unexpectedly reads as malware, and the user's next move — clicking through the dialogue without reading it — is the habit that makes the whole ecosystem worse.
CI cannot check what needs a screen
Continuous integration here runs source checks, the core tests, both builds, and package validation. It cannot open a window, so the checks that require a desktop session — installation from a path with spaces, four language and appearance combinations, close and reopen, uninstall — are skipped there.
That leaves a gap between “the pipeline is green” and “this is shippable”. The honest way to close it is a checklist that someone runs on a real machine before publishing, in the order the user will experience it: build, verify the disk image, run the integration checks, run the UI checks, read the release notes. The pipeline does not pretend to cover it, and publishing waits for both platform builds rather than racing them.
Why this is worth the ceremony
None of it makes the tool better at launching an application. It makes the release something that can be described accurately in one paragraph, which is what a stranger needs before running an unsigned binary that changes how another program connects to the network.
The version number is the smallest example of the principle: it is not a label added at the end of the process, it is the first input, and every artefact is a consequence of it. A release you cannot describe precisely is a release you cannot ask anyone to trust.