(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-timesecurity set-key-partition-list …command instead of hanging mid-build. On a machine set up once, the probe is instant. notarize.shsubmits WITHOUT--waitand pollsnotarytool infoinstead (the--waitmode hits deadline-exceeded failures), stapling once Apple returns Accepted. Do not “simplify” it back to--wait.- TestFlight processing-wait (opt-in):
xcodebuild -exportArchivereturns 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-processingto instead block until each build reachesprocessingState=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.shtries 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= ghauthenticated fordtubb/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:
- Find every Mach-O file inside the staged app, including the embedded Briefcase engine bundle.
- Sign each Mach-O individually with Developer ID, hardened runtime, and a timestamp.
- Re-seal bundles from innermost to outermost.
- 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 inscripts/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_updateblocks, Daniel must approve Keychain access. - Do not print, export, or paste the Sparkle private key.