Flutter CI/CD with Github Actions & Fastlane — Part 2(iOS)
This is the part 2 of my series on CI/CD for your flutter project. You can check here for part-1 for CI/CD on android platform of flutter project.
If you’ve followed my previous article, I assume that you already have .github/workflow files in your project, otherwise you just need to refer that portion before you proceed further.
Let’s Do it.
I’m going to divide this setup into following steps.
- Fastlane setup for iOS platform.
- Github repo setup for Fastlane match.
- Fastfile & Appfile setup and details.
- Github secrets.
- Github workflow file setup.
- Upload to TestFlight with Github actions.
Step 1 : Fastlane setup for iOS platform
Let’s configure Fastlane for your iOS platform. Go to your flutter project and then run this commands
cd ios
fastlane init
Now Fastlane would ask you few questions for your setup. Select option 2 because we want to upload our app on TestFlight.
In next step, fastlane would ask you for your apple developer credentials.
Fastlane will check if your package name(com.xyz) is available in your Developer Portal or not. If you’re doing it for your fresh app and haven’t registered your App ID just like me then say yes and proceed next. You’ll be prompted to enter an App name and then hit Enter.
So fastlane just created an app for us. You can check it on your App Store connect and you’d find your newly created app. Also your Fastfile & Appfille will be generated inside ./ios/fastlane/ folder & Gemfile inside your ./ios/ root folder.
Open your Gemfile and add below code, install this plugin via fastlane install plugin
command in your terminal.
gem "fastlane-plugin-flutter_version", git: "https://github.com/tianhaoz95/fastlane-plugin-flutter-version"
This plugin helps Fastlane to fetch build’s version code from your pubspec.yml. This version_code will be used to create build number of our app.
Note: You may be prompted with some fastlane instruction to modify gem file. Say yes and move ahead, you’ll see a message of “successfully installed plugin”.
Step 2: Github repo setup for Fastlane match
Fastlane is a collection of tools to automate the development & release process, match is one of those tools. It’s a new approach iOS & macOS code signing.
Question : How match helps you in your iOS setup?
Answer : When you want to release any iOS app, you generate CA from local machine and then generate all certificates, keys and provision profiles manually on developer.apple.com. Then you’ve to save this certs & profiles secretly somewhere safe on local or cloud. And then assume that you’re working in a team of many developers then you’ve to share this with each of them. Match helps you to save from all this manual setup and share your secrets conveniently with your team.
First you need to create an empty private repo on your github account. This will be used by match to save your certificate on this repo. Once you create this repo, now you need to initialise match for iOS app.
cd ios
fastlane match init
match will ask you to provide storage mode where you want to store certificates & profiles. We’ll select the option “git” for cloud storage and provide link as below.
A Matchfile will be created inside your ./ios/fastlane/ folder.
Open Matchfile, add type("appstore")
below line type(“development”).
Now we’ll ask match to create distribution certificate for us. So the command would be as below. Note down password that you enter in this step.
fastlane match appstore
Fastlane would do following things now.
- Create distribution certificate (if not available already).
- Download & install these certificate on your local machine.
- Create matching provisioning profile (if not found).
- Download & install it locally.
- Push these files on your Github repo.
That’s the magic of match. You can see this certificates & profiles created on developer.apple.com on your behalf, also those files are saved on your github repo.
Lastly, open your Runner.xcworkspace in Xcode, and select the provisioning profile for distribution that we’ve just created. If you can’t find it, login into your xcode first.
Step 3 : Fastfile & Appfile setup and details
Open your Fastfile and comment out everything, replace all with following code.
# Replace evverything in Fastfile with below codedefault_platform(:ios)APPLICATON_ID = ENV["APPLICATON_ID"]
BUNDLE_IDENTIFIER = ENV["BUNDLE_IDENTIFIER"]
PROVISIONING_PROFILE_SPECIFIER = ENV["PROVISIONING_PROFILE_SPECIFIER"]
TEMP_KEYCHAIN_USER = ENV["TEMP_KEYCHAIN_USER"]
TEMP_KEYCHAIN_PASSWORD = ENV["TEMP_KEYCHAIN_PASSWORD"]def delete_temp_keychain(name)
delete_keychain(
name: name
) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db")
enddef create_temp_keychain(name, password)
create_keychain(
name: name,
password: password,
unlock: false,
timeout: false
)
enddef ensure_temp_keychain(name, password)
delete_temp_keychain(name)
create_temp_keychain(name, password)
endplatform :ios do
lane :closed_beta do
keychain_name = TEMP_KEYCHAIN_USER
keychain_password = TEMP_KEYCHAIN_PASSWORD
ensure_temp_keychain(keychain_name, keychain_password) match(
type: 'appstore',
app_identifier: "#{BUNDLE_IDENTIFIER}",
git_basic_authorization: Base64.strict_encode64(ENV["GIT_AUTHORIZATION"]),
readonly: true,
keychain_name: keychain_name,
keychain_password: keychain_password
) gym(
configuration: "Release",
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
export_options: {
provisioningProfiles: {
APPLICATON_ID => PROVISIONING_PROFILE_SPECIFIER
}
}
) pilot(
apple_id: "#{APPLICATON_ID}",
app_identifier: "#{BUNDLE_IDENTIFIER}",
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
ipa: "./Runner.ipa"
) delete_temp_keychain(keychain_name)
end
end
What this code do, Let me explain you each function.
Keychain creation
MATCH function
Gym Function
Pilot Function
Now open your Appfile and you should replace your existing code with below code, only if you’re doing it on your open github repo. Skip this incase you’re doing inside your private repo.
app_identifier(ENV["BUNDLE_IDENTIFIER"])
apple_id(ENV["FASTLANE_APPLE_ID"])itc_team_id(ENV["ITC_TEAM_ID"])
team_id(ENV["DEVELOPER_PORTAL_TEAM_ID"])
As you can see our file already contains all information that we need but it is going to be visible in case of open repo, so we’re just fetching this values from ENV variable instead of adding it directly here.
Step 4 : Github Secrets
Now we’re going to configure all ENV variables inside our github secrets.
- APPLICATION_ID is apple id of your application. You can get it from
App Store Connect > App Information > General Information > Apple Id - BUNDLE_IDENTIFIER is just your bundle identifier like com.nividata.expense_manager for me.
- PROVISIONING_PROFILE_SPECIFIER is what you selected in your xcworkspace provisioning profile. It would be like “match AppStore com.nividata.expense_manager”
- TEMP_KEYCHAIN_USER any username that would be used to install certs on virtual machine.
- TEMP_KEYCHAIN_PASSWORD any password that would be used to install certs
- GIT_AUTHORIZATION is used to give access of your github certificate repo to match. It’s a Personal access tokens of your Github. To generate it
Github > Your Profile > Settings > Developer Settings > Personal access tokens. Now you need to save it as <github_username>:<personal_access_token>. So mine looked like below “jaysavsani07:ghp_imnotgoingtodeclaremytokenhere” - FASTLANE_APPLE_EMAIL_ID is email id you used to manage this app, or while creating all match certificates.
- ITC_TEAM_ID you’ll get it from Appfile itself. If not login into your developer account and go to this link, contentProviderId is your ITC_TEAM_ID.
- DEVELOPER_PORTAL_TEAM_ID is your team id. You can get it under MEMBERSHIP > Team ID
- MATCH_PASSWORD is password that you used during match setup. This will be used to decrypt certificate.
- APP_SPECIFIC_PASSWORD needed by Fastlane to upload your build on your behalf. You can generate it by following this steps.
Your github secret should look something like this screenshot.
Step 5 : Github workflow file setup.
Create a file named ios-deploy.yml inside your ./github/workflows/ and add the following code inside your file
name: Appstore Deployment
on:
push:
tags:
- master
jobs:
deploy_ios:
name: Deploy build to TestFlight
runs-on: macOS-latest
steps:
- name: Checkout code from ref
uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
- name: Run Flutter tasks
uses: subosito/flutter-action@v1
with:
flutter-version: '2.0.6'
- run: flutter clean
- run: flutter pub get
- run: flutter build ios --release --no-codesign
- name: Deploy iOS Beta to TestFlight via Fastlane
uses: maierj/fastlane-action@v1.4.0
with:
lane: closed_beta
subdirectory: ios
env:
ITC_TEAM_ID: '${{ secrets.ITC_TEAM_ID }}'
APPLICATON_ID: '${{ secrets.APPLICATON_ID }}'
BUNDLE_IDENTIFIER: '${{ secrets.BUNDLE_IDENTIFIER }}'
DEVELOPER_PORTAL_TEAM_ID: '${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}'
FASTLANE_APPLE_ID: '${{ secrets.FASTLANE_APPLE_EMAIL_ID }}'
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: '${{ secrets.APP_SPECIFIC_PASSWORD }}'
MATCH_PASSWORD: '${{ secrets.MATCH_PASSWORD }}'
GIT_AUTHORIZATION: '${{ secrets.GIT_AUTHORIZATION }}'
PROVISIONING_PROFILE_SPECIFIER: '${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}'
TEMP_KEYCHAIN_PASSWORD: '${{ secrets.TEMP_KEYCHAIN_PASSWORD }}'
TEMP_KEYCHAIN_USER: '${{ secrets.TEMP_KEYCHAIN_USER }}'
Step 6 : Upload to TestFlight with Github actions.
Before you proceed for this step, just go to your Runner.xcworkspace and build your app locally to check if you’re not getting any error.
Commit every file and push it on your master branch and go to your actions tab to check your workflow. If everything goes well then you’ll see your build in your TestFlight with status “processing”.
Conclusion : In this series of article I tried to keep workflow file & Fastfile as simple as possible. You can find many actions in github marketplace and can integrate into your flow as you need. Similarly you can leverage many features of Fastlane like automating screenshots, metadata, release notes etc. But I assume at the end of this two article, you’d be able to deploy your flutter app on both Playstore & Appstore.
If you face any query regarding any steps, feel free to comment. I’ll always happy to help.
I’ve implemented this complete flow in one of my open source app Expense-Manager.
Github repo : https://github.com/jaysavsani07/expense-manager
Android App : https://play.google.com/store/apps/details?id=com.nividata.expense_manager
iOS App : https://apps.apple.com/us/app/expense-manager-money-saver/id1556849821