CI/CD for iOS in GitHub actions using Fastlane: Part — 3

shafayat hossain
3 min readJul 7, 2023

--

After successfully integrating the initial Fastlane setup from part — 1 and AppStore Connection API from part — 2, you’re ready to build and upload the app to test flight.

Photo by Fabian Albert on Unsplash

Building And Uploading App to TestFlight

This step can be divided into 4 parts:

  • Install pods
  • Run unit test
  • Update version number
  • Archive And Upload The Build

Let’s finish these steps one by one

Install Pods

To install pods, there’s a plugin named “cocoapods” for Fastlane. At first add the plugin to the Pluginfile.

gem "cocoapods"

Now run bundle update to install the gem dependency. Then add a lane in Fastfile to install pods using cocoapods plugin:

lane :pod_install do
cocoapods(
clean_install: true,
)
end

On the terminal, run bundle exec fastlane pod_install to clean install pods.

Update Version Number

Updating the version number is one of the things we usually forget to do. That’s why I felt like automating this process. Safest way to do that is fetching the current version number from AppStore and then plus 1 with that.

lane :update_version_number do
version_number_at_testflight = latest_testflight_build_number(
api_key: api_key,
app_identifier: ENV["BUNDLE_ID"]
)
increment_build_number(build_number: version_number_at_testflight + 1)
end

Now you can run bundle exec fastlane update_version_number command from the terminal to update version number locally also.

Archive And Upload The Build

Both archiving and uploading the build can be done by “gym” tools provided by Fastlane. Lane for this can be as follows:

lane :archive do
gym(
build_path: "fastlane/build/",
export_method: "app-store",
export_options: {
provisioningProfiles: {
ENV["BUNDLE_ID"] => ENV["sigh_"+ ENV["BUNDLE_ID"] + "_appstore_profile-name"]
}
},
include_symbols: true,
output_directory: "fastlane/build/",
scheme: ENV["SCHEME"],
silent: false,
)
end

Running bundle exec fastlane archive command will generate the .ipa file in fastlane/build folder and will upload the build into TestFlight.

Till now, you’ve integrated everything to build and upload the iOS build into TestFlight. You can call all lanes from a single lane to everything in one command. Let’s do that:

lane : release do

certificate_info = certificate_update(
apiKey: api_key,
appIdentifiers: [ENV["BUNDLE_ID"]],
profileType: "appstore",
)

pod_install()

test()

update_version_number()

archive_and_distribute()

end

Now if you put everything together, your Fastfile will looks like as following:

fastlane_require 'dotenv'

api_key = nil
keychain_name='fastlane_tmp_keychain'

before_all do |lane|
Dotenv.overload '.env.secret'
setup_ci()
api_key = get_api_key()
end

lane :release do
certificate_info = certificate_update(
apiKey: api_key,
appIdentifiers: [ENV["BUNDLE_ID"]],
profileType: "appstore",
)

pod_install()

test()

update_version_number()

archive_and_distribute()

end

desc "Responsible for fetching API key using AppStore Connect API"
lane :get_api_key do
issuer_id = ENV['ISSUER_ID']
key_id = ENV['KEY_ID']
api_key_file_content = ENV['API_KEY_FILE_CONTENT']
puts api_key_file_content
app_store_connect_api_key(
is_key_content_base64: true,
issuer_id: issuer_id,
key_content: Base64.strict_encode64(api_key_file_content),
key_id: key_id,
)
end

desc "Responsible for syncing code signing certificates and profiles."
desc "Required parameters:"
desc "- profileType : Define the profile type, e.g. appstore, adhoc, development etc"
lane :certificate_update do |options|
match(
api_key: api_key,
app_identifier: ENV['BUNDLE_ID'],
derive_catalyst_app_identifier: false,
force_for_new_devices: true,
git_url: ENV['MATCH_GIT_URL'],
keychain_name: keychain_name,
platform: "ios",
team_id: ENV['APP_STORE_CONNECT_TEAM_ID'],
type: options[:profileType],
username: ENV['APP_STORE_CONNECT_USERNAME'],
)
end

lane :pod_install do
cocoapods(
clean_install: true,
)
end

lane :test do
scan(
scheme: ENV["SCHEME"]
)
end

lane :update_version_number do
version_number_at_testflight = latest_testflight_build_number(
api_key: api_key,
app_identifier: ENV["BUNDLE_ID"]
)
increment_build_number(build_number: version_number_at_testflight + 1)
end

lane :archive_and_distribute do
gym(
clean: true,
export_method: "app-store",
export_options: {
provisioningProfiles: {
ENV["BUNDLE_ID"] => ENV["sigh_"+ ENV["BUNDLE_ID"] + "_appstore_profile-name"]
}
},
include_symbols: true,
scheme: ENV["SCHEME"],
silent: false,
)
end

In the next part, I’ll explain how to integrate this with Github actions.

--

--

shafayat hossain

Lazy enough to write a bio. LinkedIn - shafayat-hossain-khan