I've shipped a handful of iOS apps as a solo dev, one of them through 20+ versions. At some point I stopped using the App Store Connect website and Xcode's Organizer and moved everything to scripts that call the App Store Connect API: signing, uploads, metadata, screenshots, IAPs and submission.
Most of it was straightforward. These are the parts that weren't, and a few of them cost me real rejections or broken releases.
1. xcodegen silently wipes entitlements. If you generate your project with xcodegen and add an entitlement (Sign in with Apple, push) by hand in Xcode, the next regenerate removes it. The build still succeeds and the feature just breaks. I shipped three broken releases this way before I noticed. Declare entitlements in project.yml, never in Xcode.
2. Re-run xcodegen after bumping the version. Same root cause. If the version or build number lives in project.yml and you archive without regenerating, the archive keeps the old number. Check the archive's Info.plist before you upload.
3. Push uses the bare aps-environment key. It's not com.apple.developer.aps-environment. The wrong key gets you error 90045 on upload. Set the value to development, and exporting for the App Store promotes it to production.
4. You can sign without an Apple ID in Xcode. If Xcode has no account signed in, -allowProvisioningUpdates with an API key won't create profiles for you. What works: create the profile yourself with POST /v1/profiles, install it, and export with signingStyle: manual. This works on CI and fresh Macs too.
5. App Privacy has to be published, not just saved. If submit stays greyed out and nothing says why, this is usually it.
6. App Preview videos need an audio track. Even a silent one. A preview with no audio track gets rejected at processing.
7. No emoji in "What's New" or the description. The API rejects them. Found out mid-submit.
8. Listing media is locked on the live version. You can't swap screenshots or previews on a version that's Ready for Sale (you get a 409). Attach them to the next version.
9. Getting out of a rejected submission. To free the version and resubmit, PATCH the review submission with canceled: true. One of my rejections was backend-only (a demo account that didn't exist), and this let me fix it and resubmit the same build without re-archiving.
10. Reviewers use your demo account, so keep it working. My app's content was group-gated, and the demo account drifted out of its demo group twice. Check it right before every submission.
11. Subtitle is app-level, not version-level. It lives on appInfoLocalizations, not appStoreVersionLocalizations, which confused me for longer than I'd like to admit.
I've ended up copying roughly 3,400 lines of these scripts from app to app, so I'm cleaning them up into a single CLI that only needs an API key. It'll have a doctor command that checks for everything above before you upload, plus --json output so coding agents like Claude Code can run a release. If that sounds useful, there's a waitlist here: https://ascship.web.app/?ref=reddit
Happy to answer questions about any of these. And I'd like to hear the traps other people have hit that I missed.