Skip to content

Instantly share code, notes, and snippets.

@dimitribouniol
Last active December 10, 2023 02:00
Show Gist options
  • Save dimitribouniol/24f0406e0878e5880f3a64dea3c528b6 to your computer and use it in GitHub Desktop.
Save dimitribouniol/24f0406e0878e5880f3a64dea3c528b6 to your computer and use it in GitHub Desktop.
Automatically Bump Versions in Xcode

Table of Contents

  1. Introduction
  2. Compatibility
  3. Usage
    1. Internal Builds
    2. Release Builds
    3. Bug-Fix Release
  4. Installation
    1. Switch to Project-based Versioning
    2. Add A Target to Bump the Version
    3. Increment Builds After Every Archive (Optional)

Introduction

Managing build versions in Xcode, especially with multiple interconnected targets where they must all match, is not fun. It's easy to forget to increment build versions until it's too late, and even easier to lose track of the last released version. Fortunately, this process can be easily automated, such that you are always working on the newest version that hasn't yet been released.

This build script will give you the following: - The ability to manually increment the build version every time a special scheme is run. - The ability to automatically increment the build version after every archive.

But that's not all! The script will automatically ensure that every time you want to increment your build versions, you are first in a clean working space, and that you are not in a feature branch that contains commits not yet merged into master. This helps make sure build versions are always incremented from where the master branch currently is, which is especially important if you are working on a team.

The last commit on the master branch will be tagged with the last version, making it easy to find as a branch off point for minor updates in the future. The tag will look like releases/1.5.3-42, corresponding to Version 1.5.3 (42).

A new branch for the version change will also be created. The branch will look like version/1.5.3-43, corresponding to the new Version 1.5.3 (43). It will contain a single commit, Version 1.5.3 (43), that encapsulates exactly the change that was made, and serves as an easy marker when scanning through commit logs when it comes time to write your release notes.

It is up to you to submit a pull request or merge this branch into the master branch (I suggest the Rebase and merge button for PRs on GitHub), but doing so will set a clear point in time when work on the next version is set to begin!

Compatibility

This versioning scheme will only work in situation where every target in a project must share the save versioning information. Although this is common if multiple targets (ie. as extension, watch app, and framework) must all be bundled into one application, it may not be what you want if you have targets that are distributed in other ways. I suggest setting up multiple projects in that scenario, and setting up the following in each project.

Usage

Follow the installation instructions below, and commit them, sharing them in the process via a pull request as you regularly would.

Internal Builds

Alternate working on branches as you usually would with your colleagues, until you are ready to release a new version. Make sure you have an up to date master branch, and that your working directory is clean.

If you only have one main target, and have gone through the steps listed in Increment Builds After Every Archive, then simply archive your project! After submitting it to TestFlight, or distributing it through other means, push your new release tag and version branch, and submit a pull request for the new branch so it could be merged into master, ahead of any other pull requests.

After submitting and merging the version branch, you can safely delete it, update your local master branch to get the change, and start working on any new feature branches.

If you have a more complex project, and cannot automatically increment the build number after every archive, you can do so manually by selecting the Bump Project Version scheme, and running it.

It is important to have the version branch merged as soon as possible after release, so any new changes all occur within builds of the new version. This also helps ensure that TestFlight builds all have a new build number, since the service will reject uploads that have the same version information.

If you do not want to archive a version that increments the build number, you can use the <#Project#> Demo variation scheme you created below.

Release Builds

A release build is one where the marketing version will always change after the release has been made. The steps are largely the same as above, but I suggest doing the following before submitting a pull request for the version branch.

  1. Modify the Marketing Version in your Xcode build settings to reflect the next intended release. If you need to go back and fix issues with the current release, follow the instructions in the next section.
  2. Stage the changes (should just be your project file) and amend the last last commit: $ git commit -a --amend.
  3. Close the file to finish amending the commit.

If you don't like vi, you can specify the editor of your choice:

  • Nano: $ GIT_EDITOR=nano git commit -a --amend (Has instructions on the tin).
  • TextMate: $ GIT_EDITOR="mate -w" git commit -a --amend (Requires command line integration).

Bug-Fix Release

You've been working on your next version 2.0, but then you realize something is deeply broken with the current release, version 1.5.3 (43), and you need to go back in (git) time to fix it! Thankfully, your trusty build script made tags that refer to the last commit of every release!

  1. Create a new branch from the tag of the release you would like to fix: $ git checkout -b hotfix/your-branch-name releases/1.5.3-43.
  2. Update the Marketing Version in your Xcode build settings to reflect the next intended release: 1.5.4.
  3. Update the Current Project Version in your Xcode build settings to reflect the next intended release, with a decimal: 43.1.
  4. Commit this manually as Version 1.5.4 (43.1).
  5. Address the changes that need to be made.
  6. Tag the last commit of the released branch: $ git tag -a releases/1.5.4-43.1 -f -m "Version 1.5.4 (43.1)"
  7. Release that version, but disregard the new version branch that is generated in the process.

Unfortunately, the build script is not smart enough to help here (maybe in the future!), mostly because build 44 already exists, so we can't increment to it. The next best thing is to use 43.1, and eventually 43.2, 43.3, … 43.14 etc… as needed.

Installation

Switch to Project-based Versioning

  1. Navigate to your Project's Build Settings. Note that this is different from your Target's Build Settings.
  2. Set the Versioning System (VERSIONING_SYSTEM) to Apple Generic.
  3. Set the Current Project Version (CURRENT_PROJECT_VERSION) to your current build version: 42 (Usually CFBundleVersion in your Info.plist).
  4. Set the Marketing Version (MARKETING_VERSION) to your current version string: 1.5.3 (Usually CFBundleShortVersionString in your Info.plist).
  5. In each of your Info.plist files:
    • Change the CFBundleVersion key's value to $(CURRENT_PROJECT_VERSION),
    • Change the CFBundleShortVersionString key's value to $(MARKETING_VERSION).

All your targets should now automatically match versions from your project's version settings.

Add A Target to Bump the Version

  1. Add a new Target to your project. The target should be an Aggregate target located under the Cross-platform section.
  2. Name your Target Bump <#project_name#> Version, and add it to your project.
  3. Navigate to the Build Phases for your new target.
  4. Add a new Run Script Phase.
  5. Set the Shell to /bin/sh.
  6. Paste the contents of Bump Project Version.sh below into the script.
  7. Uncheck Show environmental variables in build log.

You should now have a scheme you can run anytime you wish to bump the version number.

Increment Builds After Every Archive (Optional)

If you wish to increment builds after every archive of your app, complete the following steps. This is not recommended for projects with multiple independent targets that all share version numbers.

  1. Navigate to your schemes.
  2. If you wish to sometimes generate an archive without incrementing your build number, duplicate your main project's scheme, renaming it to <#project_name#> Demo. Mark it as Shared.
  3. Edit your main scheme, and open the Archive disclosure triangle.
  4. Select Pre-actions, and choose to add a Run Script Action.
  5. Set the Shell to /bin/sh.
  6. Paste the contents of lines 1-14 (the first two checks) from Bump Project Version.sh below into the script.
  7. Select Post-actions, and choose to add a Run Script Action.
  8. Set the Shell to /bin/sh.
  9. Paste the entire contents of Bump Project Version.sh below into the script.
  10. Mark the scheme as Shared.

Your archive will fail if you forget to commit first, or if you are not on the master branch. You may optionally choose to use the following branch check variation (lines 9-14) for both scheme actions if you don't want to increment the build version when not on the master branch, but you still want the archive to succeed:

# Make sure we are on master (and not a feature branch, for instance)
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$CURRENT_BRANCH" != "master" ]]; then
    echo "warning: Please switch to the master branch before proceeding."
    exit 0
fi
cd "${PROJECT_DIR}"
# Make sure working directory is clean.
if output=$(git status --porcelain) && [ -n "$output" ]; then
echo "error: Please commit any uncommitted files before proceeding:\n$output"
exit 1
fi
# Make sure we are on master (and not a feature branch, for instance)
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$CURRENT_BRANCH" != "master" ]]; then
echo "error: Please switch to the master branch before proceeding."
exit 1
fi
# Get the current marketing version
OLD_VERSION=$(agvtool what-marketing-version -terse1)
if [[ "$OLD_VERSION" == "\$(MARKETING_VERSION)" ]]; then
OLD_VERSION="${MARKETING_VERSION}"
echo "warning: agvtool is still broken, and ignores \$(MARKETING_VERSION). Using \"$OLD_VERSION\" as the marketing version."
fi
# Get the old build number
OLD_BUILD=$(agvtool what-version -terse);
# Tag the last commit on master
TAG_MESSAGE="Version $OLD_VERSION ($OLD_BUILD)"
GIT_TAG="releases/${OLD_VERSION//[^0-9A-Za-z.]/_}-${OLD_BUILD//[^0-9A-Za-z.]/_}"
echo $TAG_MESSAGE | git tag -a "$GIT_TAG" -f -F -
# Bump the build version
agvtool bump
# Get the new build number
NEW_BUILD=$(agvtool what-version -terse)
# Decide the new branch name
NEW_BRANCH="version/${OLD_VERSION//[^0-9A-Za-z]/_}-${NEW_BUILD//[^0-9A-Za-z]/_}"
# Make sure it doesn't already exist, or revert if it does
if output=$(git rev-parse --verify --quiet refs/heads/$NEW_BRANCH) && [ -n "$output" ]; then
agvtool new-version "$OLD_BUILD"
git tag -d "GIT_TAG"
echo "error: The branch $NEW_BRANCH already exists. Please remove it and try again. Reverting to the previous version."
exit 1
fi
# Try to create the new branch, or revert if we can't
if output=$(git checkout -q -b "$NEW_BRANCH") && [ -n "$output" ]; then
agvtool new-version "$OLD_BUILD"
git tag -d "GIT_TAG"
echo "error: An error occurred while branching. Reverting to the previous version."
exit 1
fi
# Commit the changes
COMMIT_MESSAGE="Version $OLD_VERSION ($NEW_BUILD)"
echo $COMMIT_MESSAGE | git commit -a -F -
LOG_MESSAGE="Bumped build from $OLD_BUILD to $NEW_BUILD [Version: $OLD_VERSION Build: $NEW_BUILD]"
echo $LOG_MESSAGE
@StevenAppJob
Copy link

Hi I tried it, but it gives Permission denied.
Any solution for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment