Archive, upload, get rejected because the build number didn't change. Automate the bump and stop hand-editing it before every TestFlight push.
Auto-increment
On Xcode 13+, the build and marketing versions live in build settings (CURRENT_PROJECT_VERSION and MARKETING_VERSION) and Xcode generates the Info.plist at build time. The simplest auto-bump uses Apple's agvtool. Run it as a pre-archive step or wire it into a Build Phase:
1cd "${SRCROOT}"
2xcrun agvtool next-version -allThis updates CURRENT_PROJECT_VERSION across every target in the project. Tick For install builds only on the Build Phase so it doesn't bump on every debug run.
If you try to edit the plist directly on a modern project template, the change silently no-ops — Xcode regenerates the plist from build settings and your edit is gone before the archive ships.
Timestamp
Sometimes you want a build number that's obviously time-ordered without keeping a counter:
1cd "${SRCROOT}"
2xcrun agvtool new-version -all "$(date +%Y%m%d%H%M)"Monotonic, human-readable, no shared state across machines. The downside is it depends on the wall clock; rebuilding within the same minute produces the same number.
A third option: git rev-list HEAD --count gives you a deterministic, monotonic-across-rebases build number that doesn't depend on the clock and never produces dirty diffs against checked-in files. Useful when several developers archive the same commit and you want them to agree on the number.
Legacy: PlistBuddy
If your project is old enough to keep Info.plist checked in (no auto-generated plist, GENERATE_INFOPLIST_FILE = NO), agvtool won't help and you're back to editing the plist by hand:
1buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")
2buildNumber=$((buildNumber + 1))
3/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}"Or the timestamp variant:
1/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $(date +%Y%m%d%H%M)" "${PROJECT_DIR}/${INFOPLIST_FILE}"${INFOPLIST_FILE} points at the source plist (relative to ${PROJECT_DIR}), which is what you want for a pre-build edit that gets committed back. For post-build edits to the bundled plist inside the built product, use ${INFOPLIST_PATH} against ${TARGET_BUILD_DIR} instead.
If a CI system (Fastlane lanes, Xcode Cloud, etc.) is also bumping the build number, coordinate which side owns it — two scripts fighting over the same value will produce off-by-one builds and confusing diffs.
The longer-term answer is to migrate to the auto-generated plist and let build settings own the version numbers. Less Xcode-project churn, fewer merge conflicts, and the bump is one line.