Last week I wrote about how the version number in all 40 of my app repos was a lie. The release tool passed the version to xcodebuild as a command-line argument and never wrote it back, so the store showed 1.9.5 while the committed file said 1.0 — for years.
Today I fixed it. One function: after a successful upload, write the version back into the project files and leave a chore: bump commit. I figured 40 lines would do it.
It was 40 lines. There were just two traps.
In your project, exactly how many files hold the value called "the version"? Can you answer without looking?
Trap 1: the file the build reads is not the file the regenerator reads
xcodebuild reads project.pbxproj, so that is the obvious place to write back to.
But 27 apps in this fleet use xcodegen. To xcodegen, project.pbxproj is an artifact; the real seed is project.yml. Patch only the pbxproj and the next time anyone runs xcodegen generate, your write-back silently evaporates — reverted to whatever stale version the yml still held.
This is not hypothetical. It already happened once in this fleet: a regeneration read the stale seed, the build number reset to 1, and Apple rejected the upload as build 1 DUPLICATE.
So the write-back updates both files. If a project.yml exists, the seed gets patched too.
Trap 2: "the store is the truth" — also not always
Write-back only prevents future drift. The 40 existing gaps needed a one-time backfill, and the method seems obvious: fetch each app's live store version and write it into the repo. The store is the truth, after all.
Scanning the dry-run output, one line caught my eye:
BUMP brewra: 1.2 -> 1.1Every other app moved forward. This one moved backward. The repo said 1.2; the store's live version was 1.1. It turned out 1.2 was sitting in review. The repo was holding the future, and the store's "live" was showing the past.
Had I just hit --apply, the tool would have rolled a mid-review version backward in the repo, and the next release would have started from that stale seed — the exact bug I was fixing, reproduced by the fix.
What would you have done? Query the review state via API and branch on it? I picked a cheaper rule: versions never move backward. If the repo value is greater than or equal to the store value, skip. In review or not, forbidding regression removes the worst accident this tool can cause.
The result
40 apps backfilled, 2 skipped (one ahead of the store because it is awaiting review, one with no live version because its first release is still in review). Immediately after applying, I ran the same script again: zero changes. Idempotency confirmed by looking, then committed.
Three self-checks:
- How many files hold your "version"? Which of them is the seed a code generator reads?
- Does your backfill/migration tool have a regression guard, or does it lean entirely on "the source is the truth"?
- If you run the tool twice in a row, does the second run change zero files?
The honest part
I did not foresee either trap. One was blocked by the written record of a past incident; the other was caught because exactly one line in a dry-run looked odd. Design didn't catch them — a habit did: printing the full plan before any destructive batch operation and actually scrolling through it.
If you have a bulk fix coming up, scroll the dry-run output to the end before you --apply. There may be exactly one line pointing the wrong way. What was your brewra? Tell me in the comments.