Skip to content

(AI generated. Not reviewed.)

Release Lane Runbook

This is the current release path for Fichero. It covers three separate outputs:

  • DMG/Sparkle/GitHub: Developer ID signed app in a DMG, notarized by Apple, Sparkle-signed, then attached to a GitHub release.
  • Mac TestFlight: macOS archive uploaded to App Store Connect for internal TestFlight. This is not the DMG path.
  • iPhone/iPad TestFlight: one universal iOS archive uploaded to App Store Connect for internal TestFlight. iOS is remote-only; it does not embed the Python engine and does not use Sparkle.

Do not run xcodebuild test or scripts/verify_all.sh on Daniel’s desktop for this lane. Build/archive only.

One Command

Default full lane (builds the embedded engine once, then DMG + Mac TestFlight + iOS TestFlight):

scripts/release-all.sh

--skip-backend is internal plumbing, not a flag you pass: release-all.sh rebuilds the Briefcase engine once at the top, then hands --skip-backend to the DMG sub-script so it reuses that engine instead of rebuilding it a second time. The engine is always embedded (Xcode’s Embed phase copies Fichero Server.app into the app). The top-level parser does not accept --skip-backend — passing it exits with unknown argument.

The lane runs unattended end-to-end — kick it off as one background task and check back when it exits; no per-step polling:

  • Codesign preflight runs first: it test-signs a throwaway binary with each signing identity the active lanes use (Developer ID for the DMG, Apple Distribution for TestFlight). If the login keychain isn’t partitioned to let codesign: access those keys, the lane fails fast with the one-time security set-key-partition-list … command instead of hanging mid-build. On a machine set up once, the probe is instant.
  • notarize.sh submits WITHOUT --wait and polls notarytool info instead (the --wait mode hits deadline-exceeded failures), stapling once Apple returns Accepted. Do not “simplify” it back to --wait.
  • TestFlight processing-wait (opt-in): xcodebuild -exportArchive returns on upload completion, not when Apple finishes processing the build into TestFlight. By default the lane stops at a successful upload and lets Apple email the team when processing completes/fails — no polling, no JWT, no ASC-key dependency. Pass --wait-for-processing to instead block until each build reaches processingState=VALID (polls the App Store Connect REST API with a signed ES256 JWT, like fastlane/pilot). That wait is non-fatal: if the poll can’t confirm (auth, timeout, transient ASC error), it warns loudly and the lane still finishes, because the upload already succeeded and Apple processes the build server-side.

Useful partial lanes:

# Mac + iOS TestFlight only
scripts/release-all.sh --skip-dmg --skip-notarize

# DMG build + notarize only
scripts/release-all.sh --skip-testflight

# Mac TestFlight only
scripts/release-all.sh --skip-dmg --skip-notarize --mac-only

# iPhone/iPad TestFlight only
scripts/release-all.sh --skip-dmg --skip-notarize --ios-only

# GitHub/Sparkle only, after the DMG is already notarized
scripts/release-all.sh --skip-dmg --skip-notarize --skip-testflight --github --draft

The script writes artifacts to build/releases/.

Required Local Assets

Developer ID DMG:

  • Developer ID Application certificate for team QAPB6CWYR6.
  • Notarytool credentials. scripts/notarize.sh tries the keychain profile first and falls back to the App Store Connect API key.

Sparkle/GitHub:

  • Sparkle private Ed25519 key in Keychain:
  • service: https://sparkle-project.org
  • account: ed25519
  • Sparkle public key baked into the app: z3UPbmGi74NGSqTQL25E2WFD1yulIzYRvtDitbIZvNY=
  • gh authenticated for dtubb/fichero.

Mac TestFlight:

  • Apple Distribution certificate for team QAPB6CWYR6.
  • Mac App Store Connect provisioning profile for bundle id app.fichero.fichero.
  • Current profile path expected by scripts/release-all.sh: $MAC_APP_STORE_PROFILE_PATH (default $HOME/Downloads/Mac_App_Store_Connect.provisionprofile)

That profile currently decodes as:

Name: Mac App Store Connect
UUID: fe5c4814-a644-4d7a-a00a-ea93937a589e
App ID: QAPB6CWYR6.app.fichero.fichero
Team: QAPB6CWYR6

The matching Apple Distribution identity SHA-1 is:

7CD87BA09F2DA8A79652710DE0F5E3C5DCD2CC35

release-all.sh copies the profile into ~/Library/MobileDevice/Provisioning Profiles/ on every TestFlight run. Xcode’s Signing & Capabilities pane may still show Provisioning Profile: None Required for local My Mac builds; that is not the TestFlight export path.

iPhone/iPad TestFlight:

  • Apple Distribution certificate for team QAPB6CWYR6.
  • iOS App Store Connect provisioning profile for bundle id app.fichero.fichero.
  • Current profile path expected by scripts/release-all.sh: $IOS_APP_STORE_PROFILE_PATH (default $HOME/Downloads/App_Store_Connect.mobileprovision)

That profile currently decodes as:

Name: App Store Connect
UUID: 601c4e9f-9315-456c-a55a-0dd538c6e564
App ID: QAPB6CWYR6.app.fichero.fichero
Team: QAPB6CWYR6

The iOS upload uses manual signing on export. Do not add -allowProvisioningUpdates to the export step; that forces the cloud-signing path the App Store Connect API key cannot use.

DMG Details

scripts/build-release-dmg.sh is the canonical DMG builder. Its [2b/6] step is load-bearing:

  1. Find every Mach-O file inside the staged app, including the embedded Briefcase engine bundle.
  2. Sign each Mach-O individually with Developer ID, hardened runtime, and a timestamp.
  3. Re-seal bundles from innermost to outermost.
  4. Verify before creating the DMG.

Do not replace this with codesign --deep; Apple previously rejected the loose .so/.dylib files inside the embedded engine when they stayed ad-hoc signed.

Spot-check before notarizing:

STAGE="build/releases/dmg-stage/Fichero.app/Contents/Resources/Fichero Server.app"
codesign -dv --verbose=4 "$STAGE/Contents/MacOS/Fichero Server" 2>&1 | grep -E 'Authority|flags|Timestamp'
codesign -dv --verbose=4 "$STAGE/Contents/Resources/app_packages/_duckdb.cpython-312-darwin.so" 2>&1 | grep -E 'Authority|flags|Timestamp'
codesign -dv --verbose=4 "build/releases/dmg-stage/Fichero.app/Contents/Frameworks/Sparkle.framework" 2>&1 | grep -E 'Authority|flags|Timestamp'

Expected: Developer ID authority, flags=0x10000(runtime), and a timestamp.

Notarization

scripts/notarize.sh build/releases/Fichero.dmg

A successful notarization run should end with status: Accepted, staple the DMG, and pass local staple validation.

Validate:

xcrun stapler validate build/releases/Fichero.dmg

Sparkle And GitHub

The current appcast URL is:

https://tubb.ca/apps/fichero/appcast.xml

Do not change SPARKLE_FEED_URL without a rebuild; it is baked into the app Info.plist.

Create the GitHub/Sparkle release:

scripts/create-github-release.sh --prerelease

or as a draft through the wrapper:

scripts/release-all.sh --skip-dmg --skip-notarize --skip-testflight --github --draft

If it hangs at Sparkle signing, unlock/approve Keychain access for the Sparkle private key. Do not print or export the private key.

TestFlight

Run:

scripts/release-all.sh --skip-dmg --skip-notarize

The wrapper converts the project marketing version to a numeric App Store version for TestFlight. For example:

Project MARKETING_VERSION: 2026.06.26-beta
TestFlight MARKETING_VERSION: 2026.6.26
CURRENT_PROJECT_VERSION: 20260626

The wrapper currently archives arm64 only and uses conservative Swift archive settings to avoid Xcode 26 archive stalls:

ARCHS=arm64
ONLY_ACTIVE_ARCH=YES
SWIFT_COMPILATION_MODE=singlefile
SWIFT_ENABLE_BATCH_MODE=NO
SWIFT_OPTIMIZATION_LEVEL=-Onone

This is for internal TestFlight builds. Revisit before a public Mac App Store submission.

The iOS leg archives with -destination generic/platform=iOS and uses the same integer-only App Store version conversion. It does not pass Sparkle settings into the iOS archive/export path.

If codesign hangs waiting for a Keychain approval dialog, pre-authorize the private key once in Terminal:

security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <login-pw> ~/Library/Keychains/login.keychain-db

Do not run that command in the script; it needs the local login password.

Troubleshooting

DMG notarization:

  • If notarization returns Invalid, fetch the notary log, identify the flagged binaries, fix the [2b/6] signing step in scripts/build-release-dmg.sh, and rebuild before submitting again.
  • If any spot-check is ad-hoc signed, missing hardened runtime, or missing a timestamp, do not notarize. Rebuild and fix signing first.

Mac TestFlight:

  • If the archive fails before export, parse the latest log and fix only the build-blocking compile/archive issue:
scripts/release-all.sh --skip-dmg --skip-notarize 2>&1 | tee build/releases/testflight-final-$(date +%Y%m%d-%H%M%S).log
rg -n "error:|ARCHIVE FAILED|EXPORT FAILED|uploaded|Upload|Done" build/releases/testflight-final-*.log
  • If export fails with signing or provisioning errors, confirm the provisioning profile App ID, profile name, team ID, and Apple Distribution SHA-1 in this runbook still match the installed profile and certificate.
  • Do not debug unrelated product behavior in this lane. Fix build, archive, export, upload, and release-script failures only.

Sparkle/GitHub:

  • If sign_update blocks, Daniel must approve Keychain access.
  • Do not print, export, or paste the Sparkle private key.