"The number is reused, not burnt."
Two Numbers With Two Jobs
An iOS build carries two versions. CFBundleShortVersionString is the marketing version people see, like 1.4.0. CFBundleVersion is the build string, and App Store Connect uses it to identify the build uniquely throughout the system, so an upload that reuses one is refused. In an XcodeGen project the two come from MARKETING_VERSION and CURRENT_PROJECT_VERSION. The share extension's Info.plist refers to both through $(…), so the extension can never carry a different pair from the app that contains it. The export options set manageAppVersionAndBuildNumber to false, because by default Xcode may rewrite the build number at upload, and then the number in the source no longer names what shipped.
Where the Number Lives
The number lives in the source. An app whose project is generated bumps CURRENT_PROJECT_VERSION in project.yml, every occurrence of it, then regenerates. An edit to the generated .xcodeproj lasts until the next xcodegen generate. The terminal app takes the other valid route and uses the committed git revision count, which is also in the source by construction.
The order around the bump is what makes the number mean something. The pipeline refuses to start while the app's inputs are uncommitted, so the number names a commit that contains exactly what is built. It checks after xcodebuild that HEAD did not move and no input changed, because a build started before an edit and uploaded after it would carry the wrong label, and the label is the only thing that ties an installed build back to code. It commits and pushes the bump only after the export or upload succeeded. A refused upload leaves the bump uncommitted, the retry reuses the same number, and nothing about a failed run ever lands in history. One pipeline once committed a build number for an upload that never happened.
The Rolling 24-Hour Cap
App Store Connect limits uploads per app, and the window rolls. On a long day of small slices the native Pippa app uploaded 21 builds in a burst; the next two were refused at export with "Upload limit reached"; the one after was accepted as soon as the first build of the burst was 24 hours old. A calendar-day reset at any midnight from UTC−7 to UTC+7, Pacific midnight included, would already have let the second refused upload through, which is how the rolling shape was proved. The practical rules: note each upload's time, expect the next slot 24 hours after the oldest one still in the window, schedule the retry for that minute with a launchd StartCalendarInterval instead of a sleeping shell, and pace a busy day as one build per few slices rather than one per slice.