Bump app version and build number using Fastlane

Introduction

We’re nearing the end of our “Fastlane from zero to hero”, and in this post we’re going to automate just a little bit more of the housekeeping work it takes to release an app to the app store. Increasing the version number and build number is something that we all have to do, but it’s also something that we all forget to do. So let’s automate it!

Pre-requisites

Build numbers

The first thing that we’re going to do is compare the current build number with the one that we have in our project. This is important because we don’t want to bump the build number if it’s already the same as the one in the project. We’re going to be getting the latest build numbers from 3 different sources:

Getting the latest build number from TestFlight

To get the latest build number from TestFlight, we’re going to use the latest_testflight_build_number action. This action will return the latest build number for the app in TestFlight. It’s a built-in action from Fastlane, so we don’t need to worry about installing any additional gems, or writing the complex logic ourselves.

desc 'Compare the local build number to the latest test flight on ASC'
lane :compare_build_numbers do
  test_flight_version = latest_testflight_build_number(
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  ).to_i
end

Getting the latest build number from the App Store

To get the version from App Store, we’re going to use the same approach - using the built-in Fastlane actions.

desc 'Compare the local build number to the latest test flight on ASC'
lane :compare_build_numbers do
  test_flight_version = latest_testflight_build_number(
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  ).to_i

  live_app_version = app_store_build_number(
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  ).to_i
end

Getting the latest build number from the local project

Can you guess what we’re going to be using? That’s right - a built-in Fastlane action!

desc 'Compare the local build number to the latest test flight on ASC'
lane :compare_build_numbers do
  test_flight_version = latest_testflight_build_number(
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  ).to_i

  live_app_version = app_store_build_number(
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  ).to_i

  local_build_number = get_build_number.to_i
end

Comparing and bumping

Now that we’ve got all 3, we only really need 2; the local version, and which ever one of the other build numbers is highest. So we can write a simple lane to compare the two, and bump the local version if it’s lower than either of the other two.

lane :bump_build_number_if_needed do |options|
  local_build_number = options[:local_build_number]
  remote_build_number = options[:remote_build_number]

  if remote_build_number.to_i > local_build_number.to_i
    new_build_number = remote_build_number.to_i + 1
    increment_build_number(build_number: new_build_number)
  elsif remote_build_number.to_i == local_build_number.to_i
    increment_build_number
  end
end

This might look a bit scary, but let’s break it down:

Putting it all together

Now that we’ve got all the pieces, we can put them together in a lane.

desc 'Compare the local build number to the latest test flight on ASC'
lane :compare_build_numbers do
  test_flight_version = latest_testflight_build_number(
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  ).to_i

  live_app_version = app_store_build_number(
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  ).to_i

  local_build_number = get_build_number.to_i

  bump_build_number_if_needed(
    local_build_number: local_build_number,
    remote_build_number: test_flight_version > live_app_version ? test_flight_version : live_app_version
  )
end

lane :bump_build_number_if_needed do |options|
  local_build_number = options[:local_build_number]
  remote_build_number = options[:remote_build_number]

  if remote_build_number.to_i > local_build_number.to_i
    new_build_number = remote_build_number.to_i + 1
    increment_build_number(build_number: new_build_number)
  elsif remote_build_number.to_i == local_build_number.to_i
    increment_build_number
  end
end

And that’s it! Now we can run the compare_build_numbers lane, and it will automatically bump the build number if it’s lower than the latest build number on TestFlight or the App Store. This is a great way to make sure that we never forget to bump the build number again, and it saves us a lot of time in the long run. Now let’s do the same, but for your version number.

Version numbers

The version number is the same approach, but is even a little bit easier, because we can use the same built in Fastlane action to get both the App Store version and the TestFlight version.

Getting remote versions

First thing we need is the remote version, which we can get using the app_store_build_number action, which returns the version in it’s SharedValues. Let’s see how that looks:

desc 'Compare the local version number to the live and TF ones, and bumps it if needed.'
lane :compare_version_numbers do
  app_store_build_number(
    live: true,
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  )
  live_version_number = lane_context[SharedValues::LATEST_VERSION]

  app_store_build_number(
    live: false,
    app_identifier: 'com.example.app',
    api_key_path: 'iTunesConnectKey.json'
  )
  tf_version_number = lane_context[SharedValues::LATEST_VERSION]
  tf = Gem::Version.new(tf_version_number)
  live = Gem::Version.new(live_version_number)
end

Getting the local version

To get the local version, we can use the get_version_number action, which will return the version number from the Info.plist file.

lane :project_version_number do
  version_number = get_version_number(
    xcodeproj: "some_project.xcodeproj",
    target: 'SomeTarget'
  )
  version_number
end

Comparing and bumping

So just like before, now we’ve got 3 versions, but we really only need 2. so we can write a lane that expects 2 versions, compare them, and bump them if we need to.

lane :bump_version_number_if_needed do |options|
  local_version_str = options[:local].to_s
  remote_version_str = options[:remote].to_s
  local = Gem::Version.new(local_version_str)
  remote = Gem::Version.new(remote_version_str)
  puts "Remote version: #{remote}, local version: #{local}"

  if remote > local
    puts 'Remote is higher than local'
    increment_version_number(version_number: options[:remote])
    puts 'Bumping to remote + 1'
    increment_version_number(bump_type: 'patch')
  elsif remote == local
    increment_version_number
  elsif local > remote
    puts 'Local version is higher than remote, no need to bump'
  end
end

Once again, this may seem a bit scary, but let’s break it down:

Conclusion

In this post, we’ve seen how to automate the process of bumping the build number and version number using Fastlane. This is a great way to make sure that we never forget to bump the build number again, and it saves us a lot of time in the long run. You can just pick and drop this code into your Fastfile, and it will work out of the box. If you have any questions, or if there’s anything else you’d like to see, let me know!