Sail Manifest

The build was green.
The release was still broken.

mac-release-verify checks a macOS release artifact before you publish it — the signature, the notarization ticket on the app and on the disk image, the appcast, the build number Sparkle actually compares, the dSYM. It only reads. It never signs, uploads, publishes or modifies anything, and it never asks for credentials.

$ brew install cyber937/tap/mac-release-verify
Source on GitHub →

Why this exists

None of these fail a build. The archive succeeds, the upload goes through, CI stays green. You find out from a user, or from a crash you cannot read, or from an update that never arrives.

What shippedWhat the user got
The .app was notarized, then wrapped in a DMG. The DMG never was. Gatekeeper inspects the disk image at download time. Nobody could open it.
The marketing version was bumped. CFBundleVersion was not. Sparkle compares the build number. Every existing user was told they were already up to date.
The appcast was generated from build settings instead of from the built artifact. The feed advertised a version that did not match the binary behind it.
The dSYM was not kept. Every crash report from that version arrived as raw addresses, for the whole life of the release. A dSYM cannot be produced afterwards.
A config file stayed in Copy Bundle Resources. The key was inside every copy downloaded, and a released build cannot be recalled.

What it looks like

$ mac-release-verify MyApp-2.1.1.dmg --appcast appcast.xml \ --dsym MyApp.xcarchive --previous-build 200 target MyApp-2.1.1.dmg app MyApp.app 2.1.1 (201) PASS code signature PASS Gatekeeper assessment (app) PASS notarization ticket (app) FAIL notarization ticket (disk image) The app is stapled but the disk image is not. Gatekeeper checks the disk image when the user opens the download, so this blocks every download even though the app inside is fine. fix: xcrun stapler staple "MyApp-2.1.1.dmg" PASS build number increased FAIL appcast agreement sparkle:version is 200 but the shipped build is 201. Sparkle trusts the appcast, so users get told about a version that is not what you built.

Every failure states the consequence, not just the rule. Exit code is 0 when everything passed and 1 when something failed, so it gates a release script directly.

What it checks

  1. Code signature, --deep --strict
  2. Gatekeeper accepts the app
  3. Hardened runtime and secure timestamp
  4. Notarization ticket on the app
  5. Notarization ticket on the disk image
  6. The disk image itself is signed
  7. Version fields present and numerically orderable
  8. Build number actually increased
  9. Appcast agrees: version, short version, enclosure length vs real bytes, signature present
  10. SUPublicEDKey present, so Sparkle can verify at all
  11. Appcast download URL returns 200
  12. dSYM exists and its UUIDs match the binary
  13. Architectures and minimum system version
  14. Embedded frameworks and XPC services are signed
  15. No credential-shaped files inside the bundle

Why you can run it on a signed release

It only reads.

No signing, no notarizing, no uploading, no publishing, no tagging, no modifying. Disk images are mounted read-only and unmounted on exit — there is a test that asserts nothing is left mounted. It never asks for an Apple ID, an App Store Connect key, or any other credential, and it reports nothing anywhere. It is one file of shell script using only tools that already ship with macOS, so you can read the whole thing before you run it.

It is tested against broken artifacts

A checker that only ever reports PASS is indistinguishable from one that does nothing. So every check is exercised against a deliberately broken artifact built on the fly — an unsigned app, an unstapled disk image, an appcast that disagrees with the binary, a build number that did not move, a missing dSYM, a credential left in Resources. 18 assertions, no signing identity or Apple account required, run in CI on every push.

What is next

Shipping a Mac app is one action in your head and four or more separate switches in reality — the release, the appcast, the download page, the release notes. Forget one and nothing turns red. This checker is the first piece of a tool for that whole problem.

A written playbook — shipping to the Mac App Store and directly at the same time, and every way it goes wrong — is in progress. If you want it when it lands, or you have a failure mode that should be a check, say hello or open an issue.