CI/CD for iOS in GitHub actions using Fastlane: Part — 3
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.
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.