Flutter: Build and Deploy iOS apps using GitHub Actions

image

Anton Muntianu

image

Introduction

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline.

Our focus will be on building and deploying an iOS App.
We will use a machine and environment which GitHub provides us by default.

By the end, you will know how to:

  • Enable GitHub Actions and add the first Workflow
  • Build an iOS Application Archive.
  • Deploy your App to TestFlight

Let’s begin with a basic workflow.

A new Workflow

GitHub Actions can be added to your existing Repository. Still, we will work with a new Flutter project, which has its first commit pushed to a fresh repository.

GitHub Actions are enabled by default, you can start right away by adding a workflow file.
1. Create a .github/workflows directory at the root of your project
2. In the .github/workflows directory, create a file named build.yml
3. Copy the following instructions into the build.yml
4. Change to branch name from “main” if you have a different one
5. Change the Flutter version to the one you use in your project

# Name of the workflow
name: Build

# Controls what will trigger the workflow.
# Change it to your needs.
on:
 # A new push to the "main" branch.
 push:
   branches: [ "main" ]
 # A new pull request to the "main" branch.
   pull_request:
     branches: [ "main" ]
 # Allows to trigger the workflow from GitHub interfaces.
 workflow_dispatch:

# A single workflow can have multiple jobs.
jobs:
 # 'A new job is defined with the name: "build_ios"
 build_ios:
   # Defines what operating system will be used for the actions.
   # For iOS, we have to use macOS GitHub-Hosted Runner.
   runs-on: macos-12
   # Defines what step should be passed for successful run
   steps:
     # Checkout to the selected branch
     - name: Checkout
       uses: actions/checkout@v3
     # Download and install flutter packages
     - name: Install Flutter
       uses: subosito/flutter-action@v2
       with:
         # Define which stable flutter version should be used
        flutter-version: "3.7.3" 
        channel: 'stable'
         # Enables cache for flutter packages              
         # Speed up the process
         cache: true
     # Get Flutter project dependencies
       - name: Get dependencies  
         run: flutter pub get

GitHub workflows include jobs, which work via steps one by one.
We use GitHub-Hosted macOS runner because it is the only way to build iOS apps. You can read more about GitHub Pricing here.
If you have you want to build on your device or some other dedicated one, you can use Self-Hosted Runners.

So, you can push a new commit to the branch and look for results in your repository’s Actions tab.

Press on your first workflow. In my case it’s called Build #1Next, press on “build_ios”

Here you can watch for your workflow in progress, and see its results. When the first workflow is finished, you should see something like this.

Congratulations, it works. Let’s build an iOS Application Archive.

Creating iOS Application Archive

To build iOS Archives we have to take multiple steps:
1. To have to be enrolled in paid Apple Developer Program
2. Create an Apple Distribution Certificate
3. Create a Provision Profile 
4. Create and fill exportOptions.plist
5. Add a workflow step for storing values to a keychain 
6. Build IPA
When you have created a Distribution Certificate and a Provision Profile, you need to add new exportOptions.plist file at ./ios/ folder. 
Add the following to the created file.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
   <dict>
       <key>method</key>
       <string>app-store</string>   
       <key>teamID</key> 
       <string><your_team_id></string>        
       <key>provisioningProfiles</key>      
       <dict>     
          <key>com.<your_company>.<app></key>
          <string><provision_profile_name></string> 
       </dict>
   </dict>
</plist>

Now we will add the certificates that will be used during the workflow. Let’s add the step before getting flutter packages:

- name: Import certificates (iOS)
  env:
       BUILD_CERTIFICATE_BASE64: ${{ secrets.iOS_P12_DISTRIBUTION_CERT_BASE64 }} 
       P12_PASSWORD: ${{ secrets.iOS_P12_DISTRIBUTION_CERT_PASSWORD }}     
       BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.iOS_PROVISION_PROFILE_BASE64 }}       
       KEYCHAIN_PASSWORD: ${{ secrets.iOS_KEYCHAIN_PASSWORD }}
     run: |
         # create variables         
         CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12                PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
         KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
 
         # import certificate and provisioning profile from secrets
         echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
         echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o
$PP_PATH
         # create temporary keychain         security create-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN_PATH
         security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
         security unlock-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN_PATH
         # import certificate to keychain           
security import $CERTIFICATE_PATH -P $P12_PASSWORD -A -t cert -f pkcs12 -k $KEYCHAIN_PATH            
         security list-keychains -d user -s $KEYCHAIN_PATH
         # apply provisioning profile
         mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
         cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
    - name: Install Flutter
      uses: subosito/flutter-action@v2
      with:
        # Define which stable flutter version should be used
        flutter-version: "3.7.3"
        channel: 'stable'
        # Enables cache for flutter packages
        # Speed up the process
        cache: true
    # Get Flutter project dependencies
    - name: Get dependencies
      run: flutter pub get
    - name: Build iOS application archive
      run: flutter build ipa --export-options-plist=ios/exportOptions.plist

Also, we should add a step that will always be called and clean up the keychain.

 - name: Clean up keychain and provisioning profile
     if: ${{ always() }}
     run: |
        security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
        rm ~/Library/MobileDevice/Provisioning\
 Profiles/build_pp.mobileprovision

We need the distribution certificate and provision profile files encoded as base64. To create a new file with base64 encoded string, use the macOS command:

base64 -i ./<file_name> -o ./file-base64.txt

To add new secrets to your repository, go to its webpage > Settings > Secrets and variables > Actions and add the 4 secrets by pressing the New repository secret button. 

As a result, we have IPA built every time a new commit has been pushed.

iOS Application Archive Deployment

At the current stage, we have signed the iOS build and the last step is to deploy it to App Store Connect and the TestFlight.
Let’s look at the steps for the workflow:
First, we need to decode App Store Connect private key file and save it:

 - name: Decode App Store Connect private key file and save it
   env:
       API_KEY_BASE64: ${{ secrets.iOS_APPSTORE_CONNECT_PRIVATE_KEY_BASE64 }}
       API_KEY: ${{ secrets.iOS_APPSTORE_CONNECT_API_KEY_ID }}     
   run: |
     mkdir -p ~/private_keys
     ls ~/private_keys
     echo -n "$API_KEY_BASE64" | base64 --decode -o ~/private_keys/AuthKey_$API_KEY.p8
     echo "After saving: "
     ls ~/private_keys

Then, after the IPA building, we add the following: 

 - name: Upload to App Store Connect
     env:
       ISSUER_ID: ${{ secrets.IOS_APPSTORE_CONNECT_ISSUER_ID }}          
       API_KEY: ${{ secrets.IOS_APPSTORE_CONNECT_API_KEY_ID }}     
   run: |
       echo "Before uploading: "
       ls ~/private_keys
       xcrun altool --upload-app -f build/ios/ipa/flutter_app.ipa -t ios --apiKey $API_KEY --apiIssuer "$ISSUER_ID"
       ls ~/private_keys

As we see, to deploy the app archives we need 3 values: 

  • App Store Connect API key ID
  • App Store Connect Private key file as base64
  • App Store Connect Issuer ID

Let’s begin with the private key file. You should open the App Store Connect > Users and Access > Keys and press the Generate API key button. You should name the key and choose the developer role. Next, you should see all the 3 values

  • You can download the Private key by pressing the Download API Key button
  • You can see the API key ID
  • You can see the Issuer ID on top

You should add them to your GitHub Secrets. Congratulations, you are ready to distribute your application.

Conclusion

Using GitHub actions as a continuous delivery tool has provided a comfortable, reliable service for building and deployment of any app. Benefits worth mentioning:

  • When you use a GitHub Repository, you already have a familiar environment for your CI/CD. You can have everything for your code in one place.
  • With a big community of GitHub, you can have nearly any use case already covered with an Action from the marketplace.
  • At any moment, you can scale to your needs with Microsoft-provided servers or run all your actions on another server or your device with self-hosted runners without paying GitHub anything.

Overall, GitHub Actions helped us deliver apps in different configurations much faster on multiple platforms at a time.

Still thinking if GitHub Actions is a right choice for building
and deploying your application?

Share your idea with us, and we’ll come up with the best development solution for your case.

Get in touch today