Most iOS CI tutorials reach for Fastlane. It's the default assumption. And Fastlane is fine — but it's also another Ruby toolchain to maintain, another layer of abstraction between you and xcodebuild errors, and another thing that breaks when Xcode updates. For a small side project, I wanted zero overhead. So I wrote a release script using plain xcodebuild and xcrun altool , and wired it into GitHub Actions. Here's what I learned. The Setup The app is a no-dependency iOS project (SwiftUI, SwiftData, zero SPM packages). One scheme, one target, distributes via the App Store. The goal: git push → trigger workflow → build, sign, upload to TestFlight. Auto-incrementing build numbers for free: BUILD_NUMBER = " ${ 1 :- $( git -C " $REPO_ROOT " rev-list --count HEAD ) } " Enter fullscreen mode Exit fullscreen mode That's it. Every commit bumps the count. No build number file to commit, no race conditions in CI, no manual tracking. Pass it straight into xcodebuild: xcodebuild archive \ ...…