joshuaaron
joshuaaron

Reputation: 906

How to handle versioning with fastlane for iOS/android

digging into fastlane for the first time on a react native project, and in the process of getting a demo version up for internal testing on both the google play store, and testflight.

After following some tutorials for getting the android bundle up successfully, I ended up with some lanes that do increment both the versionCode and versionName, then bundle and push to the google play store via the supply command.

Now I'm moving onto some iOS lanes, and I'm wondering the best way to sync these up. Ideally, I'd like to use the package.json version as the one source of truth for this, but I'm lost a little on the best way to tie it all in, so I'm asking for some advice/workflows that others have found successful with fastlane and building/versioning their ios/android apps. Thanks!

Here are my current android lanes.

  desc "Build a version of the app, that allows params for task and type"
  lane :build do |options|
    build_task = options.fetch(:build_task, "bundle")
    build_type = options.fetch(:build_type, "Release")

    gradle(task: "clean")
    gradle_params = {
      task: build_task,
      build_type: build_type,
    }

    gradle(gradle_params)
  end

  ##### ---------------------------------------------
  desc "Build and push a new internal build to the Play Store"
  lane :internal do
    build()
    supply_params = {
      track: "internal",
      release_status: "draft",
    }
    supply(supply_params)
  end
   
  ##### ---------------------------------------------
  desc "Increment build number and push to repository"
  lane :inc_build_number do |options|
    params = {
        :app_project_dir => 'app'
    }

    # Specify a custom build number to be passed in
    if options[:build_number]
        params[:version_code] = options[:build_number].to_i
    end

    android_increment_version_code(params)
    new_version_code = Actions.lane_context[Actions::SharedValues::ANDROID_VERSION_CODE]
    UI.important("Incremented android version code to #{new_version_code}")
  end

  ##### ---------------------------------------------
  desc "Increment version number and push to repository"
  lane :inc_version_number do |options|
    should_commit = options.fetch(:should_commit, true)
    commit_message = options.fetch(:commit_message, "android: bump version code & number[skip ci]")
    should_push = options.fetch(:should_push, true)

    ensure_git_status_clean if should_commit

    # increment build number first
    inc_build_number

    increment_type = options.fetch(:increment_type, "patch")
    new_version_params = options[:version]

    params = {
        app_project_dir: 'app',
        increment_type: increment_type,
    }

    unless new_version_params.nil?()
        params[:version_name] = new_version_params
    end

    android_increment_version_name(params)
    new_version_name = Actions.lane_context[Actions::SharedValues::ANDROID_VERSION_NAME]
    UI.important("Incremented android version name to #{new_version_name}")

    if should_commit
        path = "android/app/build.gradle"
        git_add(path: path)
        git_commit(path: path, message: commit_message)
        push_to_git_remote if should_push
    end
  end

Upvotes: 3

Views: 14272

Answers (1)

ridvanaltun
ridvanaltun

Reputation: 3020

I think you should not use a single source for Android and iOS while versioning. Because versioning presents how your app changes in time.

Anyway, creating a good Fastlane configuration not an easy job, so I decide to share what I have got.

For Android, I created a file called version.properties, I increment version number in this file before submitting a new version to Google Play Store and a Gradle script automatically generates build number in build time. I'm just increment version number (following Semver) and the script handles the rest. When I use Fastlane I'm doing same thing, Fastlane ask me new version, I give a version number to it then it changes the version.properties file all after then it compiles the app.

Follow Below Steps:

  • Create a file named version.properties under android/version folder
  • Write your app current version in it like: VERSION=7.0.5
  • Create a file named versioning.gradle under android folder and write below code in this file:
ext {
    buildVersionCode = {
        def versionName = buildVersionName()
        def (major, minor, patch) = versionName.toLowerCase().tokenize('.')
        (major, minor, patch) = [major, minor, patch].collect { it.toInteger() }
        (major * 10000) + (minor * 100) + patch
    }
    buildVersionName = {
        def props = new Properties()
        file("../version/version.properties").withInputStream { props.load(it) }
        return props.getProperty("VERSION")
    }
}
  • Add below marked lines in your android/app/build.gradle file
// ...

apply from: "../../node_modules/react-native/react.gradle"
apply from: '../versioning.gradle' // <- add this line

// ...
// ...
// ...

android {

  // ...

  defaultConfig {
    // ...
    versionCode buildVersionCode() // <- add this line
    versionName buildVersionName() // <- add this line
    // ...
  }

  // ...
}

// ...

You can change the version from android/version/version.properties file now. Version code will automatically create while building. For an example, if you typed version like 7.3.5 your build number will be 70305.

Now let's make Fastlane integration,

Install property_file_read plugin to ability read property files: fastlane add_plugin property_file_read

You can use below Fastlane configuration for Android:

# CONSTANTS
NOTIFICATION_TITLE = "FOO APP FINISHED!"

platform :android do
  desc "Choose release name"
  private_lane :determine_release_name do |options|
    versions = google_play_track_release_names(track: options[:track])

    if versions.empty?
      UI.user_error!("Whoops, current version not found!")
    else
      current_version = versions[0]
    end

    parts = current_version.split(".")

    major = parts[0]
    minor = parts[1]
    patch = parts[2]

    target_major = (major.to_i + 1).to_s + ".0.0"
    target_minor = major + "." + (minor.to_i + 1).to_s + ".0"
    target_patch = major + "." + minor + "." + (patch.to_i + 1).to_s

    properties = property_file_read(file: "android/version/version.properties")
    file_version = properties["VERSION"]

    target_version_label = UI.select("What version do you want to use?", [
      "Bump patch (#{target_patch})",
      "Bump minor (#{target_minor})",
      "Bump major (#{target_major})",
      "KEEP EXISTING (#{file_version})",
      "CUSTOM",
    ])

    next target_major if target_version_label.match(/major/)
    next target_minor if target_version_label.match(/minor/)
    next target_patch if target_version_label.match(/patch/)
    next file_version if target_version_label.match(/FILE/)

    custom_version = prompt(text: "\nEnter New Version Number:")

    custom_version
  end

  desc "Build and Deploy to Google Play Internal App Sharing"
  lane :beta do
    newVersion = determine_release_name(track: "internal")

    # update version
    File.open("../android/version/version.properties", "w") do |file|
      file.write("VERSION=#{newVersion}")
    end

    # gradle(task: "clean", project_dir: "./android/")
    gradle(task: "bundle", build_type: "Release", project_dir: "android")
    upload_to_play_store(track: "internal", aab: "android/app/build/outputs/bundle/release/app-release.aab")

    notification(title: NOTIFICATION_TITLE, subtitle: "Google Play - Internal App Sharing", message: "Finished!!")
  end
end

I think it is the best system you can find for Fastlane integration on Android, I use these settings myself.

In iOS, I'm asking new version number then I compile and send app to the TestFlight, there is nothing special. You can check my iOS configuration below:

# CONSTANTS
NOTIFICATION_TITLE = "FOO APP FINISHED!"

IOS_XCWORKSPACE = "ios/Foo.xcworkspace"
IOS_XCODEPROJ = "ios/Foo.xcodeproj"
IOS_SCHEME = "Foo"
IOS_TARGET = "Foo"

APP_STORE_KEYFILE = "fastlane/app-store-auth-key.p8"
APP_STORE_KEY_ID = "xxxxxxxxxx"
APP_STORE_ISSUER_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

platform :ios do
  desc "Choose release name"
  private_lane :determine_release_name do
    current_build_number = latest_testflight_build_number(api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY])
    current_version = lane_context[SharedValues::LATEST_TESTFLIGHT_VERSION]

    parts = current_version.split(".")

    major = parts[0]
    minor = parts[1]
    patch = parts[2]

    target_major = (major.to_i + 1).to_s + ".0.0"
    target_minor = major + "." + (minor.to_i + 1).to_s + ".0"
    target_patch = major + "." + minor + "." + (patch.to_i + 1).to_s

    selected_version_ok = false

    while selected_version_ok === false
      target_version_label = UI.select("What version do you want to use?", [
        "KEEP EXISTING (#{current_version})",
        "Bump patch (#{target_patch})",
        "Bump minor (#{target_minor})",
        "Bump major (#{target_major})",
        "CUSTOM",
      ])

      if target_version_label.match(/CUSTOM/)
        custom_version = prompt(text: "\nEnter New Version Number:")

        if custom_version < current_version
          UI.important "Wahaha, version (#{custom_version}) can't lower than the current version (#{current_version})"
        else
          selected_version_ok = true
        end
      else
        selected_version_ok = true
      end
    end

    next { version: target_major, type: "major" } if target_version_label.match(/major/)
    next { version: target_minor, type: "minor" } if target_version_label.match(/minor/)
    next { version: target_patch, type: "patch" } if target_version_label.match(/patch/)
    next { version: current_version, type: "current" } if target_version_label.match(/KEEP/)

    { version: custom_version, type: "custom" }
  end

  desc "Push a new beta build to TestFlight"
  lane :beta do
    api_key = app_store_connect_api_key(
      key_id: APP_STORE_KEY_ID,
      issuer_id: APP_STORE_ISSUER_ID,
      key_filepath: APP_STORE_KEYFILE,
      duration: 1200, # optional (maximum 1200)
      in_house: false, # optional but may be required if using match/sigh
    )

    new_version = determine_release_name

    if new_version[:type] === "current"
      # get latest build number from App Store
      build_num = app_store_build_number(
        initial_build_number: 1,
        live: false,
        version: get_version_number(xcodeproj: IOS_XCODEPROJ, target: IOS_TARGET),
        api_key: api_key,
      )

      increment_build_number(build_number: build_num + 1, xcodeproj: IOS_XCODEPROJ)
    else
      increment_version_number(version_number: new_version[:version], xcodeproj: IOS_XCODEPROJ)
    end

    # 🏗️ Build app, this method has other options few we will explore in next section
    build_app(
      silent: true,
      workspace: IOS_XCWORKSPACE,
      scheme: IOS_SCHEME,
    )

    # ⏫ Its time to upload
    upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)

    # 🧽 Clear artifacts
    clean_build_artifacts
    sh "rm -rf \"#{lane_context[SharedValues::XCODEBUILD_ARCHIVE]}\""

    notification(title: NOTIFICATION_TITLE, subtitle: "Testflight", message: "Finished!!")
  end
end

Let me what you think in comments! (Btw sorry for my poor England :p)

Upvotes: 5

Related Questions