Flutter & GitHub Workflows: Deploying to TestFlight
When I wanted my personal Flutter app to distribute iOS builds from a GitHub workflow using TestFlight, I couldn’t find a good cohesive guide to setting it up. The process was a bit nuanced and time consuming to get right.
In the end I created a GitHub workflow that automatically publishes whenever I push a versioned tag to GitHub – it builds the project, signs it, and publishes that build to TestFlight.
Below I’ve provided the comprehensive set of steps you need to set this up from scratch, which I reproduced using a public sample repository for your reference.
Requirements
In order to deploy builds to TestFlight, Apple requires that you have an Apple Developer Program account – these cost $99/year.
I’m also assuming you’ve created a private or public repository on GitHub that you’ll be using for version control where you can run the GitHub Workflow we’ll set up.
Setup with Apple
These are the steps to configure your app in Apple’s systems.
- Create a Bundle ID on Apple Developer portal – it can be an arbitrary bundle ID, and it doesn’t require any specific Capabilities or App Services. Typically these are reverse-domain names, for my example I used
no.tjer.HelloWorld
. - Create an iOS App for your Bundle ID in App Store Connect by clicking the next to Apps
- Configure TestFlight on App Store Connect, by navigating to your app and selecting the TestFlight tab.
- Click the next to Internal Testing to create a new internal group
- Name it whatever you’d prefer (e.g. Developers)
- Click the new group in the sidebar and add yourself by clicking the next to Tester (0)
Create & set up a Flutter project for iOS
First we set up a project with Flutter with the iOS platform enabled and configure the project’s Bundle ID
Create a new Flutter project with iOS enabled, if you haven’t already:
flutter create --platforms ios --org no.tjer --project-name hello_world --description "Test for iOS deploy on GH" flutter_github_example
- Navigate to your project directory (e.g.
flutter_github_example
in the above example) and openios/Runner.xcworkspace
in Xcode. - Make sure the Bundle Identifier for the project matches the one you created above by navigating to the Runner project in the left pane, clicking the Runner target, and then updating the Bundle Identifier under the General tab.
Configure code signing
To help managing code signing certificates and provisioning profiles, as well as the process of building & signing, we’ll use the excellent open source project fastlane. We’ll be using a private GH repository to distribute the signing certificate to the builder (and to the team, if wanted).
Setting up fastlane
for your project
Please refer to the fastlane setup docs for more details, but here’s a quick overview of what to do to configure Fastlane.
Create a text-file called
ios/Gemfile
with the following content:source "https://rubygems.org" gem "fastlane" gem "cocoapods"
From a shell:
brew install rbenv ruby-build # Set up rbenv to control the ruby version rbenv init
Close your terminal and re-open it for
rbenv
to be initialized, then navigate to your Flutter project and run:echo 2.7.5 > .ruby-version # Pick a semi-recent Ruby (you could probably do 3.x instead?) rbenv install # Install the requested version of Ruby gem install bundler cd ios bundle lock --add-platform x86_64-darwin-19 # Make sure the platform list includes the GH Runner platform bundle exec fastlane init # Start the initial configuration of fastlane
- Choose 🚀 Automate App Store distribution when
fastlane init
prompts you for what you’re configuring, and sign in to your Apple Developer account.
- Choose 🚀 Automate App Store distribution when
- Make sure to add the following files to git:
.ruby-version
– the Ruby version we’re using for fastlaneios/fastlane/Appfile
– configuration for the iOS app storeios/fastlane/Fastfile
– where we keep our fastlane recipeios/Gemfile
– the dependencies for fastlaneios/Gemfile.lock
– specific version informatin for the gems specified in the Gemfile
Configuring App Store Connect API access for fastlane
- Set up an App Store Connect API key on the Users & Access section of the site by going to the Keys tab and then:
- Click the next to Active (0).
- Give it an identifying name (E.g. Flutter GH Deploy for the example project
- Choose Developer as the Access for the key, and click Generate
- Refresh the page (navigating back to Keys) and choose to Download API key on the right side of the newly added key
- Add the API key to your GitHub secrets – this allows GitHub Workflows to publish new TestFlight builds:
- Navigate to your Flutter project’s GitHub page (e.g. https://github.com/jorgenpt/flutter_github_example/ for my example repo).
- Go to Settings, and select Secrets > Actions on the left side.
- Add a New repository secret named
APP_STORE_CONNECT_API_KEY_KEY
(yes,KEY
should be there twice) and paste in the contents of the API key file you downloaded from App Store Connect (the file is named something likeAuthKey_KEYID.p8
).
Configuring certificates & provisioning profiles
First, create a private repository on GitHub and mark it as Private. In my case, I named it certificates
. Don’t initialize it with any files or licenses. Make a note of the SSH url for your repository, in my case that’ll be git@github.com:jorgenpt/certificates
.
Navigate to your Flutter project in a shell and then run these commands:
# These all need to be run from the ios platform of your Flutter project cd ios # For the following command, give `match init` the SSH URL for your repository (e.g. # git@github.com:myuser/certificates) and generate a random password (though make a note # of it for the next step). # You should add the generated Matchfile to Git. bundle exec fastlane match init # Create an appstore distribution certificate & provisioning profile bundle exec fastlane match appstore # Create a development certificate & provisioning profile bundle exec fastlane match development
- Make sure that your Xcode project is configured to use the same certificates:
- Open
ios/Runner.xcworkspace
in Xcode - Navigate to the Runner project in the left pane, click the Runner target, and switch to the Signing & Capabilities tab.
- Uncheck Automatically manage signing.
- Select Release in the top bar and set the provisioning profile to the one that starts with match AppStore.
- Select Debug in the top bar and set the provisioning profile to the one that starts with match Development, and repeat this for Profile as well.
- Open
Create a new SSH key that your workflow can use to access your GitHub certificates repository:
ssh-keygen -C github.com:myuser/my_flutter_project_name -f ~/Desktop/id_rsa_build
Add the password you generated above & the SSH key to your GitHub secrets – this allows GitHub Workflows to sign your build:
- Navigate to your Flutter project’s GitHub page (e.g. https://github.com/jorgenpt/flutter_github_example/ for my example repo).
- Go to Settings, and select Secrets > Actions on the left side.
- Add a New repository secret named
MATCH_PASSWORD
and paste in the password you generated above. - Add a second New repository secret named
SSH_PRIVATE_KEY
and paste in the contents ofid_rsa_build
on your Desktop.
Putting it all together
Your GitHub repository’s secrets should have three different secrets configured:
Now all that remains is to set up a fastlane lane that runs our build for us, and configure GitHub Workflows to invoke it:
- Create
.github/workflows/publish_ios.yml
in your Flutter repository from the reference publish_ios.yml in the example project. - Create
ios/fastlane/Fastfile
in your Flutter repository from the reference Fastfile in the example project. - Update
ios/fastlane/Fastfile
with your project details:APP_IDENTIFIER
is the Bundle Identifier we created in Setup with Apple.APPSTORECONNECT_ISSUER_ID
is the Issuer ID from the Keys section of Users and Access on App Store Connect.APPSTORECONNECT_KEY_ID
is the Key ID from the specific key that we created in Configuring App Store Connect API access for fastlane, which you can look up in the Keys section of Users and Access on App Store Connect.
That’s it! Now just create a git tag and push it to GitHub to start a build:
1 2 |
|
You can see an example run in the Actions tab of flutter_github_example
.
These builds will by default be available to Internal Testers – you can use the App Store Connect app or website to release one of these builds to External Testers.