The first macOS run died after 88 seconds on `cd scripts/monero_c: No such file
or directory`. That was the cheap failure. The workflow had been written fresh
rather than ported, and behind that missing clone it had also skipped the step
that pins the crypto salts. Had it got further it would have shipped a build
that mints random salts, which is the 2026-08-29 corrupted-seeds incident on a
new platform: wallets created on it would not decrypt after the next upgrade.
So the prerequisites are now ported step for step from build-linux.yml, the
desktop build known to pass: torch_dart, reown_flutter, bitbox (the Apple
variant, without the gomobile Android bindings), prepare_moneroc.sh, configure,
secrets generation, salt pinning with the same refusal, Trocador injection,
codegen, localization, and compile_graphics.sh. curl for wget and BSD sed's
-i.bak form, because the runner is a Mac.
Two more things the fresh version got wrong, found while porting:
configure was run without sourcing app_env.sh, and app_config.sh writes
CFBundleName, the bundle id and the version into Info.plist from variables only
app_env.sh sets. The app would have shipped with an empty name. The step now
prints the three values it wrote.
And app_env.sh hardcoded 1.0.0 (1) on macOS, 6.1.0 (79) on Linux and 1.9.0 (29)
in the fish variant, the last two being Cake Wallet's own version numbers. On
macOS PlistBuddy writes that into the bundle, so every macOS build would have
called itself 1.0.0. Linux only escaped because its substitution looks for a
version: 0.0.0 placeholder that pubspec_description.yaml does not contain. All
three now derive from pubspec_description.yaml the way Android already does, and
the two sh variants resolve to 1.0.4 (14). The fish one is unverified: fish is
not installed here and CI does not use it.
Separately, four workflows verified their secret injection by grepping
lib/.secrets.g.dart into the log, which prints the values. Android's printed the
Trocador, WalletConnect, MoonPay and exchange-helper keys. Gitea likely masks
registered secrets, but masking is a best-effort layer and values never belong
in output. Each now reports every key's name and length and nothing else.
Windows is not in this commit. Its run refused correctly, on the guard added in
c11c79b0c: the runner has Flutter 3.38.5, not the pinned 3.32.0. That is fixed on
the runner, not in the workflow.
Three desktop targets, three different states, one destination.
macOS did not build at all. There was no workflow, only Cake's from-source
scripts and an Xcode project. It turns out most of the work was already done:
monero_c ships aarch64-apple-darwin and x86_64-apple-darwin, and
Runner.xcodeproj already references both dylibs with CodeSignOnCopy. So this
fetches the same prebuilt bundle the iOS and Android legs use, lipos the two
slices into universal libraries, and builds. One download serves Apple Silicon
and Intel, because a single-arch build looks correct on the machine that made
it and fails on half of everyone else's.
It signs with a Developer ID Application certificate, which is NOT the Apple
Distribution certificate the TestFlight workflow uses. They are not
interchangeable: an App Store cert on a directly downloaded app is refused by
Gatekeeper. Signing is inside-out by hand rather than --deep, which Apple
deprecated and which skips nested code it does not recognise. Then notarytool
with the App Store Connect key this repository already has, then stapler, so
Gatekeeper clears the app without a network round trip.
Every credential is optional and the preflight says, before spending forty
minutes, exactly which stages the run can reach and what the user will see if
it cannot reach them. Absent secrets downgrade the build loudly; they do not
fail it and they do not pass silently.
Windows and Linux already built. Windows already Authenticode-signs through the
Fleet broker. What both lacked was a way to get the artifact to anybody:
upload-artifact is the only publication and Gitea Actions artifacts here upload
and then list as zero, so the signed Windows zip has never been reachable. Both
now publish to the GitHub release, on the smirk pattern: tag-triggered, the
release must already exist so a half-populated one never goes public, and a
same-named asset from an earlier attempt is replaced rather than duplicated.
Windows does its API work in PowerShell because git-bash there has neither
python nor node guaranteed and ConvertFrom-Json is native.
Tags now build automatically on all three. A tag is the thing a release is cut
from, and the artifact that gets signed should come from it rather than from
whatever was dispatched by hand.
scripts/release/sign-release.sh detach-signs the collected artifacts and signs
SHA256SUMS, which is the signature that matters when a download reaches someone
from a mirror without its .asc. It verifies even immediately after signing: a
signature written but never checked invites trust it has not earned.
Not done here, and both need a human: GH_RELEASE_TOKEN does not exist in this
repository's Gitea secrets, and the release key's user ID still says Smirk.