In this post we’ll see a real world scenario where Fastlane can help us automate releasing an Android app to internal testers and publishing on Google Play as well. If you are new to Fastlane, I suggest you to read my previous posts on Fastlane for Android:

  • [Setup and Basics]({{< ref “fastlane-android-1” >}})
  • [Flavors and Tests]({{< ref “fastlane-android-2” >}})
  • [Publishing on Play Store]({{< ref “fastlane-android-3” >}})

Build an APK and upload to Slack

Let’s see a Fastlane script for automating Android APK building and uploading to a Slack channel for internal testing purposes.

desc "Release APK to a Slack Channel"
  lane :release_stage do
    ensure_git_status_clean(show_uncommitted_changes: true)
 
    lastest_uploaded_build_number = google_play_track_version_codes(
      track: 'Beta test track',
      package_name: "com.example"
    )[0]
 
    android_set_version_code(
      version_code: lastest_uploaded_build_number + 1,
      gradle_file: "app/build.gradle"
    )
 
    gradle(project_dir: "android-module", task: "clean")
    gradle(project_dir: "android-module", task: "testDevDebug")
    gradle(project_dir: "android-module", task: "assembleDevRelease")
 
    commit = last_git_commit
    commit_hash = commit[:commit_hash]
    branch = git_branch
    version = android_get_version_name(gradle_file: "app/build.gradle")
    @message = "Android App\nVersion: #{version}\nBranch: #{branch}\nCommit hash: #{commit_hash}"
 
    # Upload to slack
    slack_upload(
        slack_api_token: ENV['SLACK_API_TOKEN'],
        title: "app-dev-release.apk",
        channel: "app-releases",
        file_path: ".//app/build/outputs/apk/dev/release/app-dev-release.apk",
        initial_comment: @message
    )
  end

Snippet Breakdown

1. Ensure Git Status is Clean

ensure_git_status_clean(show_uncommitted_changes: true)

This step verifies that the working directory is clean, with no uncommitted changes. It’s a safeguard to ensure that all changes are accounted for in version control before proceeding with a build.

For workflows where minor, uncommitted changes are expected (e.g., autogenerated files), consider adding conditions to exclude specific files from this check or to commit them automatically. If certain files are expected to change and you want to automatically commit those changes as part of your Fastlane lane, you can script this as follows.

First, identify and commit specific files automatically before proceeding with the clean status check:

autogenerated_files = ["autogenerated_file1.txt", "autogenerated_file2.txt"]
autogenerated_files.each do |file|
    if sh("git status --porcelain #{file}").empty? == false
       sh("git add #{file}")
       sh("git commit -m 'Auto-commit #{file}'")
    end
end

After committing the specified autogenerated files, you can then safely use the ensure_git_status_clean action.

2. Get the Latest Uploaded Build Number

lastest_uploaded_build_number = google_play_track_version_codes(
    track: 'Beta test track',
    package_name: "com.example"
)[0]

This snippt retrieves the version code of the most recent APK from a specific Google Play track. This ensures that each new release increments from the latest version, maintaining version continuity.

This section can be improved by adding an error handling block. We can use Ruby’s begin-rescue block to encapsulate the logic for fetching the latest uploaded build number. In case of an error (e.g., network issues, authentication errors, unexpected API responses), the rescue block can catch the exception and handle it accordingly.

begin
  latest_uploaded_build_number = google_play_track_version_codes(
    track: 'Beta test track',
    package_name: "com.example"
  )[0]
 
  # Proceed only if latest_uploaded_build_number is successfully retrieved and is a valid number
  if latest_uploaded_build_number.nil? || latest_uploaded_build_number.to_s.empty?
    UI.user_error!("Failed to retrieve the latest uploaded build number from Google Play. Check your track and package name.")
  end
rescue => e
  UI.error("Error fetching latest uploaded build number: #{e.message}")
  # Handle the error based on your workflow requirements
  # For example, you might want to retry the operation, send a notification, or abort the lane
  UI.user_error!("Failed to fetch latest build number from Google Play. Aborting...")
end
 

3. Increment and Set New Version Code

android_set_version_code(
    version_code: lastest_uploaded_build_number + 1,
    gradle_file: "app/build.gradle"
)

Updates the version code in the project’s build.gradle file, ensuring the new build is correctly versioned.

4. Execute Gradle Tasks

Clean, test, and assemble tasks are executed in sequence.

gradle(task: "clean")
gradle(task: "testDevDebug")
gradle(task: "assembleDevRelease")

These steps ensure the APK is built from a clean state, passes all tests, and is assembled for release. The snippet specifically builds the “Dev” flavor of the app, indicating that this script is tailored for development or testing purposes rather than a production release. This is evident from the tasks executed: clean, testDevDebug, and assembleDevRelease. The following synthax is also supported:

gradle(
  task: "assemble",
  flavor: "Dev",
  build_type: "Release"
)

In the case of an app without any flavors, the build process simplifies as there’s no need to specify a flavor when running Gradle tasks. Here’s how the script would be adjusted to handle a project without flavors:

gradle(task: "clean")
gradle(task: "testDebug")
gradle(task: "assembleDebug")

or

gradle(tasks: ["clean", "testDebug", "assembleDebug"])

If the root of Android source code is different from the root of the project, use the project_dir parameter to pass the path to the root:

gradle(project_dir: "android-module", task: "clean")
gradle(project_dir: "android-module", task: "testDebug")
gradle(project_dir: "android-module", task: "assembleDebug")

5. Upload APK to Slack

@message = "Android App\nVersion: #{version}\nBranch: #{branch}\nCommit hash: #{commit_hash}"

Collects and formats essential information about the release, such as the app version, branch, and commit hash, for transparency and traceability.

You can include more details in the message, such as the build date/time and a link to the commit on a version control platform (e.g., GitHub) for easy access to the change log.

   slack_upload(
       slack_api_token: ENV['SLACK_API_TOKEN'],
       title: "app-dev-release.apk",
       channel: "app-releases",
       file_path: ".//app/build/outputs/apk/dev/release/app-dev-release.apk",
       initial_comment: @message
)

This step uploads the built APK to a designated Slack channel, providing team members with immediate access to the new release, accompanied by the release information message.

Build an AAB and upload to Google Play

This snippet builds an AAB and upload it to Google Play, targeting users in the ‘Beta test track’.

  desc "Deploy a new version to the Google Play. The update will be available only for 'Beta test track' users. Release to all users must be executed manually from Google Play Console."
  lane :release_prod do
    ensure_git_status_clean(show_uncommitted_changes: true)
 
    gradle(task: "clean")
    gradle(task: "testDevDebug")
    gradle(task: "bundleProRelease")
 
    upload_to_play_store(
      aab: 'android-module/app/build/outputs/bundle/proRelease/app-pro-release.aab',
      track: 'Beta test track',
      skip_upload_apk: true
    )
  end

Snippet Breakdown

We talked about ensure_git_status_clean and gradle actions before. Let’s check the new stuff.

Bundle the Production Release

gradle(project_dir: "android-module", task: "bundleProRelease")

This command generates an Android App Bundle (AAB) for the production release build variant (proRelease). AAB is the recommended publishing format on Google Play, offering benefits like smaller app sizes and streamlined deployment across various device configurations.

Upload to Google Play Store

upload_to_play_store(
    aab: 'android-module/app/build/outputs/bundle/proRelease/app-pro-release.aab',
    track: 'Beta test track',
    skip_upload_apk: true
)

This step uploads the generated AAB to the Google Play Store, specifically targeting the ‘Beta test track’. This allows for testing the app with a limited user base before a wider rollout. The skip_upload_apk option is set to true to indicate that only the AAB file is being uploaded, aligning with Google Play’s preference for AAB over APK files.

Publish App on Google Play Store

The final snippet will help us promoting this version of the app from ‘Beta test track’ to production.

desc "Promote beta to production"
  lane :publish do
 
    latest_uploaded_build_number = google_play_track_version_codes(
      track: 'Beta test track',
      package_name: "com.example"
    )[0]
 
    upload_to_play_store(
      package_name: 'com.example',
      version_code: latest_uploaded_build_number,
      track: 'Beta test track',
      track_promote_to: 'production',
      rollout: '0.5', # 1 means 100%
      skip_upload_metadata: true,
      skip_upload_changelogs: false,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
end

Snippet Breakdown

Retrieve the Latest Uploaded Build Number

latest_uploaded_build_number = google_play_track_version_codes(
    track: 'Beta test track',
    package_name: "com.example"
)[0]

This command fetches the version code of the latest app version uploaded to the ‘Beta test track’. It’s crucial for identifying which version of the app is currently in beta and ready to be promoted to production.

Promote the Beta Version to Production

   upload_to_play_store(
     package_name: 'com.example',
     version_code: latest_uploaded_build_number,
     track: 'Beta test track',
     track_promote_to: 'production',
     rollout: '0.5',
     skip_upload_metadata: true,
     skip_upload_changelogs: false,
     skip_upload_images: true,
     skip_upload_screenshots: true
)

This part of the script is where the promotion happens. Let’s break down the key parameters:

  • package_name: Specifies the unique application ID of the app being promoted.
  • version_code: The version code of the app version to promote, fetched from the beta track.
  • track: The current track of the app version, in this case, ‘Beta test track’.
  • track_promote_to: The target track for promotion, here specified as ‘production’, indicating the app version is being moved to the full release stage.
  • rollout: Sets the percentage of users who should receive the update initially. ‘0.5’ means 50% of users. It’s a strategy used to gradually roll out updates to monitor performance and catch potential issues early.
  • skip_upload_metadata, skip_upload_changelogs, skip_upload_images, skip_upload_screenshots: These parameters control whether to upload metadata, changelogs, images, and screenshots. In this script, metadata, images, and screenshots uploads are skipped, but changelogs are not, indicating an update note or changelog will be provided for the new production release.

Wrapping up

In sharing these Fastlane snippets, my aim was to present a real-world use case that illustrates the practical application of Fastlane in automating Android app development workflows.

To run these lanes from the command line, you would navigate to your project’s root directory and use the following syntax:

fastlane release_stage
fastlane release_prod
fastlane publish

or using bundler (recommended):

bundle exec fastlane release_stage
bundle exec fastlane release_prod
bundle exec fastlane publish

While we’ve covered some specific and highly useful functionalities of Fastlane here, it’s important to note that Fastlane offers a vast array of features beyond what we’ve discussed. To fully leverage the power of Fastlane in your Android projects, we highly recommend consulting the official Fastlane documentation.

References