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.

  1. 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.
  2. Create an iOS App for your Bundle ID in App Store Connect by clicking the next to Apps
  3. Configure TestFlight on App Store Connect, by navigating to your app and selecting the TestFlight tab.
    1. Click the next to Internal Testing to create a new internal group
    2. Name it whatever you’d prefer (e.g. Developers)
    3. 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

  1. 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
    
  2. Navigate to your project directory (e.g. flutter_github_example in the above example) and open ios/Runner.xcworkspace in Xcode.
  3. 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.

  1. Create a text-file called ios/Gemfile with the following content:

     source "https://rubygems.org"
    
     gem "fastlane"
     gem "cocoapods"
    
  2. From a shell:

     brew install rbenv ruby-build # Set up rbenv to control the ruby version
     rbenv init
    
  3. 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.
  4. Make sure to add the following files to git:
    • .ruby-version – the Ruby version we’re using for fastlane
    • ios/fastlane/Appfile – configuration for the iOS app store
    • ios/fastlane/Fastfile – where we keep our fastlane recipe
    • ios/Gemfile – the dependencies for fastlane
    • ios/Gemfile.lock – specific version informatin for the gems specified in the Gemfile

Configuring App Store Connect API access for fastlane

  1. Set up an App Store Connect API key on the Users & Access section of the site by going to the Keys tab and then:
    1. Click the next to Active (0).
    2. Give it an identifying name (E.g. Flutter GH Deploy for the example project
    3. Choose Developer as the Access for the key, and click Generate
  2. Refresh the page (navigating back to Keys) and choose to Download API key on the right side of the newly added key
  3. Add the API key to your GitHub secrets – this allows GitHub Workflows to publish new TestFlight builds:
    1. Navigate to your Flutter project’s GitHub page (e.g. https://github.com/jorgenpt/flutter_github_example/ for my example repo).
    2. Go to Settings, and select Secrets > Actions on the left side.
    3. 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 like AuthKey_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.

  1. 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
    
  2. Make sure that your Xcode project is configured to use the same certificates:
    1. Open ios/Runner.xcworkspace in Xcode
    2. Navigate to the Runner project in the left pane, click the Runner target, and switch to the Signing & Capabilities tab.
    3. Uncheck Automatically manage signing.
    4. Select Release in the top bar and set the provisioning profile to the one that starts with match AppStore.
    5. 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.
  3. 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
    
  4. Add the password you generated above & the SSH key to your GitHub secrets – this allows GitHub Workflows to sign your build:

    1. Navigate to your Flutter project’s GitHub page (e.g. https://github.com/jorgenpt/flutter_github_example/ for my example repo).
    2. Go to Settings, and select Secrets > Actions on the left side.
    3. Add a New repository secret named MATCH_PASSWORD and paste in the password you generated above.
    4. Add a second New repository secret named SSH_PRIVATE_KEY and paste in the contents of id_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:

  1. Create .github/workflows/publish_ios.yml in your Flutter repository from the reference publish_ios.yml in the example project.
  2. Create ios/fastlane/Fastfile in your Flutter repository from the reference Fastfile in the example project.
  3. Update ios/fastlane/Fastfile with your project details:
    1. APP_IDENTIFIER is the Bundle Identifier we created in Setup with Apple.
    2. APPSTORECONNECT_ISSUER_ID is the Issuer ID from the Keys section of Users and Access on App Store Connect.
    3. 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
git tag v0.1.0 # This accepts any tag name  starting with "v"
git push --tags

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.

Comments