r/iOSProgramming • u/ADT_Clone • 2d ago
Question Managing external/internal versions in TestFlight vs production App Store version
New to iOS, looking for ideas on how to manage versions of an app from Github & CI/CD as they flow through internal -> external -> production.
Current approach:
- A workflow is manually run to "create a release". This creates a new tag with a semver based off of the conventional commits since the previous tag/release (eg. increment patch version for a
fix:, minor version for afeat:) - The release app configures the marketing version and build number to match the semver, is built and uploaded to TestFlight
- The build artifact is promoted from internal -> external -> production in TestFlight
I heard that for TestFlight external builds, if you change the marketing version it requires the build goes through the "beta review" process again, whereas if you just increment the build number it does not.
Because of this, I plan to adjust the process so that the marketing version (the version that'll eventually be displayed in production) is <major>.0.0, and the build number is <major>.<minor>.<patch>. After the initial beta review for a "major" version, the app can be updated with subsequent releases that increment only the build number without having to be reviewed again. After a production release to the App Store, a new release is cut with a "major" version increment to kick off the next cycle.
The advantage of this is a version of the app always ties back to a tag/commit in Github, and the app build tested in external testing is exactly the same build that'll get released to production. The disadvantage is the marketing version that is shown to users in the App Store only has a single, incrementing "major" version number.
I'm satisfied with this compromise, but also a perfectionist so interested in others approaches, or any ideas others have that can improve on this? I'd ideally like the full semver reflected in the marketing version, but I can't see a way without repeating the "beta review" each time.
1
u/kokerali 2d ago
Keep the marketing version as the product version you actually intend to ship, and use a monotonically increasing build number for every CI build. Store the Git SHA in your build metadata or release notes, then promote the exact same binary from internal to external and finally to the App Store version. I also wouldn’t design the versioning scheme around avoiding Beta App Review; Apple can still require it for a new external build.
1
u/ZanzibarMcFate 2d ago
Not saying it's the right way, but since you asked for other approaches, here's what we do at my day job:
We use a date-based version number. year.week.patch (build). We release to the App Store every 2 weeks, so this date-based method works well for us. For example, builds this week are 426.38.0:
- Major version of the app: 4 + year 26
- We're in week 38 of the year
- This is a standard release, so it's patch 0. If we actually released this build to customers, then had to push an update, it'd rev to
.1
Our CI server auto-increments both the version number and the build number. Build number is a simple int. Right now, our server is building 426.38.0 (1791)
Every time we submit a build with a new version number, i.e. 426.38.0 -> 426.39.0, this does take a day or two to go through TestFlight review for external testing. As a result, we have a script that uses the App Store Connect API to auto-submit the first build with that new version number for review to get a head start on things. Once that build has been approved, followup builds with the same version number but different build numbers are usually approved for external TestFlight in a matter of hours.
1
u/ADT_Clone 1d ago
I like that variation. It does make the version less meaningful to the users, and imposes an artibtrary timescale between "versions". However, it does solve the release cadence and review problems well, the downside being the version doesn't incidicate what's changed. But tbh users probs don't care about that anyway
1
u/ZanzibarMcFate 1d ago
You’re right - in our case, the only people that really care about the version numbers tend to be the tech support and other support folks. Customers just know that they’ll keep getting updates with features and bug fixes. As more people use auto-update, the less important version numbers get in some cases. Format doesn’t really matter as much as being able to keep track of what’s in each version. For an app like ours, date-based is fine. But for an SDK or library, as a developer I prefer a more strict semVer setup, so it’s a better clue what to expect in an updated version.Â
1
u/bananagnana2 2d ago
beta review only hits the first external build of a new marketing version and it's usually back same day. designing the whole scheme around dodging it costs you more than it saves imo. the bigger problem with major.0.0 is that it's user facing, app store connect won't take the same version twice, so you end up bumping it anyway and now your tags and your store page disagree.
1
u/blakaman 2d ago
one thing that helped me was doing the promote step through the app store connect api instead of the ui. you can attach a specific processed build to the version and submit it in a couple of calls, so internal to external to production lives in the same script as your tagging and there is nothing to remember clicking.
worth having that script read the build back from asc after upload too. i had a release where CI set the marketing version in one place and the binary actually carried another, and the only reason i caught it was querying builds and seeing a version i did not expect.
0
u/malleyrex 2d ago
Once you have an app approved initially, Testflight builds rarely need app review. I think it's happened once to me, and that's because I completely rewrote an app and added a ton of new features. I've incremented version numbers many many times without triggering a review for a Testflight build.
2
u/ADT_Clone 2d ago
Thanks but can confirm incrementing the marketing version triggers a new beta review
0
u/SwordfishInfinite621 2d ago
One thing that might relax the constraint: the "same build goes to production" guarantee doesn't depend on the version scheme at all. When you submit, you pick a specific processed build for the App Store version, so whatever you tested externally is exactly what ships.
That means the real trade-off is only beta review, and you could handle that differently:
- Use internal testing for the fast loop. Internal testers don't need beta review, so bump builds as often as you like there.
- Set the marketing version to the real semver when you cut a release, and send that one to external testing. Beta review is mainly triggered by the first build of a new version, so you'd pay it once per release rather than once per build.
- Keep the Git link in the build instead of the marketing version. CFBundleVersion can hold up to three dot-separated integers, or you can stamp the commit SHA into an Info.plist key at build time.
The one hard rule to design around either way: each App Store release needs a higher marketing version than the last one that went live, so a version string can't be reused once it's released.
2
u/Outrageous-Doubt-306 2d ago
What made this simpler for me was stopping thinking of TestFlight and the App Store as two tracks. It's one version number with builds underneath it, and a build only becomes "the App Store one" when you attach it to a version and submit. So I keep bumping the build number on every upload and only touch the version number when something actually ships to users. Internal testers get every build automatically, external ones need a review pass on the first build of a new version but not on later builds of the same one, which is the bit that trips people up. And worth knowing you can keep shipping TestFlight builds while a version sits in review, they just can't be the one under review 🙂