Set Xcode build numbers to incremented or timestamps

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. One prerequisite: VERSIONING_SYSTEM must be set to apple-generic in build settings, or agvtool refuses with a "does not support AppleGeneric versioning" error — new project templates don't always set it.

1cd "${SRCROOT}" 2xcrun agvtool next-version -all

This updates CURRENT_PROJECT_VERSION across every target in the project. Run it as a scheme pre-action on Archive, a CI step, or a manual pre-archive script — not as a Build Phase. agvtool rewrites project.pbxproj, and mutating the project file mid-build is something Apple explicitly warns against; Xcode can re-resolve or cancel the build under you.

If you wire it into a Build Phase anyway, two more things bite. Since Xcode 15, User Script Sandboxing is on by default (ENABLE_USER_SCRIPT_SANDBOXING = YES), which blocks script phases from writing inside SRCROOT — the script dies with a deny file-write in the build log until you flip that setting to NO for the target. And tick For install builds only so it doesn't bump on every debug run.

Don't try to edit the plist directly on a modern project template — there usually isn't one to edit. With GENERATE_INFOPLIST_FILE = YES, Xcode produces the Info.plist from build settings at build time, so there's no source file for your script to patch.

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 build number that's deterministic per commit, 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. It's not bulletproof, though — squashing or rebasing history lowers the count, and shallow CI clones break it entirely (fetch full history if you go this route).

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}"

The arithmetic assumes CFBundleVersion is a plain integer — if yours is dotted (1.0.3), $((buildNumber + 1)) blows up and you'll need to bump the last component yourself. The sandboxing caveat from above applies here too, since this writes into PROJECT_DIR.

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's increment_build_number, Xcode Cloud's CI_BUILD_NUMBER, 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.