Documentation
TestFlight
Ship a Release IPA to App Store Connect so the build shows up in TestFlight.
Local device install stays on device.md. Full path through App Review and release: app-store.md.
Nothing in the toolchain hardcodes your Team ID, Apple ID, or .p8 path — those come from
walnut device login, the app manifest, or env vars (WALNUT_DEVELOPMENT_TEAM,
WALNUT_ASC_*).
First-try checklist
Do these once per Apple team / bundle ID, then walnut testflight should work without
one-off fixes.
| # | Do this | Why |
|---|---|---|
| 1 | Paid Apple Developer Program | Free teams cannot distribute to TestFlight |
| 2 | Register an App ID for your bundle_id (Certificates, Identifiers & Profiles) | Automatic signing needs it |
| 3 | Create an app in App Store Connect with that same bundle ID | Upload target |
| 4 | Create a Team App Store Connect API key (see below) | Individual keys fail export with a fake “password incorrect” / -20101 |
| 5 | walnut device login <app> — pick team + paste Team key | Saves team + key into the manifest (or use WALNUT_* env) |
| 6 | App has App Store–ready ios/ (icons + orientations) | walnut new installs this; testflight also auto-heals missing AppIcon assets |
Then:
# if you changed the walnut CLI / UIKit
walnut testflight --preflight . # shell + key check
walnut testflight .
Dry-run IPA (no upload):
walnut testflight . --ipa-only
# → ios/build/walnut-export/*.ipa
Team API key (required)
In App Store Connect:
- Users and Access → Integrations → App Store Connect API
- Open Team Keys (not Individual)
- Generate with Access Admin or App Manager
- Download
AuthKey_<KEYID>.p8— keep that name - Issuer ID = the UUID shown at the top of the Keys page (shared by the org). It is not the Key ID.
walnut testflight copies the .p8 to ~/.appstoreconnect/private_keys/AuthKey_<KEYID>.p8 for xcodebuild / altool.
| Manifest / env | Meaning |
|---|---|
authentication_key_path / WALNUT_ASC_KEY_PATH | Path to AuthKey_….p8 |
authentication_key_id / WALNUT_ASC_KEY_ID | Key ID |
authentication_key_issuer_id / WALNUT_ASC_ISSUER_ID | Issuer ID (top-of-page UUID) |
What walnut testflight does
- Preflight — ensure AppIcon assets (install from
Templates/ios/Assets.xcassetsif missing); validateios/project.yml(AppIcon setting, orientations /UIRequiresFullScreen,Main.olinked only viaOTHER_LDFLAGS); optionally verify the ASC key is readable viaaltool --generate-jwt - Compile Walnut for the device LLVM triple
- Bump
build_number/CFBundleVersion(default;--no-build-bumpto skip) xcodebuild archive(Release, Automatic signing — do not force a Distribution identity by hand)- Export IPA (
ExportOptions.plistmethodapp-store-connect) - Upload with
xcrun altool --upload-app
Processing in App Store Connect / TestFlight is async (often a few minutes).
Versions
In walnut.toml:
[ios]
marketing_version = "1.0"
build_number = "12"
# false (default) → Info.plist ITSAppUsesNonExemptEncryption = false
# skips App Store Connect “App Encryption Documentation” each upload.
# Set true only if you use non-exempt encryption (then answer ASC docs).
uses_non_exempt_encryption = false
JSON manifests use marketingVersion / buildNumber / usesNonExemptEncryption.
Each successful run increments build_number unless you pass --no-build-bump.
walnut testflight syncs the encryption flag into ios/Info.plist (and project.yml when present)
before archive — same idea Fastlane often sets via that plist key, without needing Fastlane.
Flags
| Flag | Role |
|---|---|
--preflight | Check ios/ + ASC key; do not archive |
--team ID | Team ID override |
--account email | Prefer a team linked to this Apple ID |
--ipa-only | Stop after IPA export |
--no-build-bump | Keep current build number |
--bump-build | Explicit bump (default) |
New apps
walnut new writes an App Store–ready shell:
ios/Assets.xcassetsAppIcon set (placeholder icons including 1024 marketing)ASSETCATALOG_COMPILER_APPICON_NAME: AppIconUIRequiresFullScreen+ all four interface orientationsITSAppUsesNonExemptEncryption: false(override via[ios] uses_non_exempt_encryption)Main.olinked only viaOTHER_LDFLAGS(never also as aframeworks:entry)
Older apps missing icons get them auto-installed on the next testflight run. If project.yml is still missing the settings above, preflight fails with a pointed error — align with the toolchain / the Counter example project.yml.
Edition (Community / Commercial)
Same as simulator/device: a valid ~/.walnut/license selects Commercial
(-DWALNUT_COMMERCIAL); otherwise Community (splash + credits). TestFlight does
not require Commercial — Community builds may upload with branding.
Troubleshooting
| Symptom | Likely fix |
|---|---|
Export: ASC -20101 / HTTP 401 / “password entered incorrectly” | Team key, not Individual. Issuer ID = top-of-page UUID. File as AuthKey_<KEYID>.p8. Re-run walnut device login |
“No profiles” right after -20101 | Red herring — fix API key auth first, then App ID / ASC app |
Missing app icon / CFBundleIconName | ios/Assets.xcassets + ASSETCATALOG_COMPILER_APPICON_NAME (auto-healed / walnut new) |
| Orientations / iPad multitasking (90474) | UIRequiresFullScreen: true + four orientations in project.yml |
Duplicate Main.o symbols | Link only via OTHER_LDFLAGS, not frameworks: |
| Stale Apple ID in keychain logs | Harmless if ASC Team key works; set development_account via login |
| ASC asks encryption questions every build | Default uses_non_exempt_encryption = false writes ITSAppUsesNonExemptEncryption; set true only if you need ASC docs |
| “Version already used” | Let the default bump run |
| altool upload failed | Key role; ASC app exists; export compliance; or Transporter |
Non-goals (v1)
- Creating App Store Connect app records via API (bundle id must already exist)
- App Review Submit for Review / Release buttons (walkthrough: app-store.md; listing assets: screenshots.md
walnut store upload) - Ad Hoc / Enterprise export
Related
- app-store.md — end-to-end publish → release
- device.md — physical install + login
- screenshots.md — App Store screenshot capture +
store/ - licensing.md — Community vs Commercial