How BigPay Increased Flutter Developer Velocity: Part 2— CI/CD for Flutter iOS

Bashir Isyaka
bigpay-tech-blog
Published in
18 min readJul 26, 2024

Introduction

Welcome back to our series on enhancing Flutter developer velocity at BigPay. In Part 1, we shared our journey of implementing a CI/CD workflow for our Flutter Android app, streamlining our build, test, and deployment processes using GitHub Actions. This approach has significantly improved our productivity and code quality.

In Part 2, we will continue our exploration by focusing on the CI/CD workflow for our Flutter iOS app. Implementing a robust CI/CD pipeline for iOS comes with its own set of challenges and opportunities. We’ll discuss how we overcame these challenges, optimised our deployment process, and ensured a seamless integration with our existing development workflow.

Whether you’re a mobile developer, tech enthusiast, or industry professional, this guide aims to provide you with practical insights and strategies to enhance your iOS development workflow. Join us as we delve into the specifics of setting up a CI/CD pipeline for Flutter iOS using GitHub Actions, and learn how to automate your build and deployment processes efficiently.

Problem

Similar to our Android development process, we faced several challenges in our iOS workflow. Manual builds, inconsistent environments, and deployment delays were hindering our ability to deliver updates promptly. Additionally, the complexity of iOS code signing and provisioning profiles added to our difficulties. We needed a solution that could streamline our iOS development pipeline and ensure consistent, automated deployments.

Solution

To address these issues, we extended our use of GitHub Actions to set up a CI/CD pipeline for our Flutter iOS app. GitHub Actions provided the automation and flexibility we needed to manage our build, test, and deployment processes effectively. Here’s how we did it:

  1. Code Signing and Provisioning: We automated the management of code signing and provisioning profiles directly within our GitHub Actions workflow.
  2. Automated Builds: We configured GitHub Actions to automatically trigger builds for our iOS app. This ensures that our code is always up-to-date and builds are generated without manual intervention.
  3. Testing: To maintain high code quality and catch issues early, we integrated unit tests, widget tests and integration tests into our CI/CD pipeline. The testing steps ensure that only code that passes all tests makes it into the final build.
  4. Deployment: For deployment, we automated the process of distributing builds using Firebase App Distribution, Diawi and TestFlight. This makes it easy to share builds with testers and stakeholders for feedback.

By implementing this CI/CD pipeline, we achieved a more efficient and reliable development process for our Flutter iOS app, enabling us to deliver high-quality updates to our users faster and with greater confidence.

Prerequisites

Before we dive in, make sure you have the following:

  1. A Flutter app hosted on GitHub.
  2. Basic understanding of GitHub Actions.
  3. GitHub account.
  4. Apple Developer account and App
  5. If you want to deploy to TestFlight: Access to the App Store Connect API key and issuer info will be required
  6. Access to Apple Distribution Certificate file (.p12) with the associated password and provisional profile for initiating the IOS app signing process.
  7. Firebase App Distribution
  8. Diawi Account

Before proceeding, ensure that your Flutter project is properly set up and configured with Flutter flavor if needed as outlined in Part 1 of this series.

Step 1: Setting Up iOS Signing Certificate

The iOS signing process involves several steps to ensure that your app is correctly signed and ready for distribution. This includes storing certificates and provisioning profiles, transferring them to the build environment, importing them into the keychain, and utilizing them in your build process. In this context, the “build environment” refers to the machine or server where your code is built and tested, typically managed by CI/CD tools like GitHub Actions, which runs the automated build and deployment steps.

1.1 Create Certificate

Depending on your app’s setup, you might need only a distribution certificate or both development and distribution certificates.

Certificate Requirements:

  • Distribution Certificate: Required for releasing your app to the App Store or for ad hoc distribution. Make sure you create and install this certificate correctly if you haven’t done so already.
  • Development Certificate: Sometimes necessary if your project has additional targets that require it.

Common Issues:

When the job is run, if you encounter the error No signing certificate "iOS Development" found: No "iOS Development" signing certificate matching team ID, this typically indicates that your project is incorrectly configured. Specifically, it means that:

  • The project or an additional target (such as frameworks) is still configured to use an iOS Development certificate instead of a distribution certificate.
  • Misconfigured Signing Settings: The signing certificate or the additional targets might be incorrectly set up, leading to this error.

Steps to Create P12 Distribution Certificate

You can create and export P12 distribution certificate using Xcode, Keychain Access, or the Apple Developer website.

Below are the instructions for each method:

Using Xcode:

  1. Open Xcode and navigate to Xcode > Preferences > Accounts.
  2. Select your Apple ID, then click “Manage Certificates.”
  3. Right-click your desired distribution certificate and choose “Export Certificate.”
  4. Save the certificate as a P12 file, ensure you use a strong password to protect it.

Using Keychain Access:

  1. Open Keychain Access (Applications > Utilities > Keychain Access).
  2. Select the ‘Certificates’ category on the left sidebar.
  3. Find the desired distribution certificate, right-click on it, and choose “Export.”
  4. Save the certificate as a P12 file, ensure you use a strong password to protect it.

Using the Apple Developer Website:

  1. Log in to the Apple Developer website.
  2. Navigate to ‘Certificates, Identifiers & Profiles.’
  3. Under ‘Certificates,’ click the ‘+’ button to create a new certificate.
  4. Follow the prompts to create and download the distribution certificate.
  5. Use Keychain Access to import the downloaded certificate and export it as a P12 file.

For detailed instructions on exporting your signing certificate, refer to the Xcode documentation.

Once you have your P12 distribution certificate, convert it to Base64 format. Use the following command in your terminal to convert your P12 certificate to Base64 and copy it to your clipboard:

base64 -i BUILD_CERTIFICATE.p12 | pbcopy

Navigate to your project’s GitHub repository settings and go to the “Secrets” section. Add new secrets named DISTRIBUTION_CERT_BASE64 and paste the distribution Base64 string certificate you copied. Additionally, add the certificate password as another secret. In this example, the password secret is named P12_PASSWORD.

1.2 Your Apple provisioning profile

To create your Apple provisioning profiles, follow these steps in the Apple Developer portal:

  1. Log in to the Apple Developer portal.
  2. Navigate to Certificates, Identifiers & Profiles.
  3. Select Profiles from the sidebar.
  4. Click the “+” button to add a new profile.
  5. Choose App Store to create a distribution provisioning profile for submitting your app to the App Store.
  6. Select your App ID (<APP_ID>) and select the associated distribution certificate, follow the prompts to generate, save, and download the provisioning profile.
  7. Repeat these steps to create an Ad Hoc Distribution profile for distributing the app to third-party testing services like Firebase Distribution and Diawi.

Use the following command to convert your provisioning profile to Base64 and copy it to your clipboard:

base64 -i PROVISIONING_PROFILE.mobileprovision | pbcopy

Navigate to your project’s GitHub repository settings and go to the Secrets section. Add a new secret named BUILD_PROVISION_PROFILE_BASE64_APPSTORE and paste the Base64 string App Store provisioning profile string. Then, add another secret named BUILD_PROVISION_PROFILE_BASE64_ADHOC and paste the Base64 string Ad Hoc provisioning profile string.

1.3 A keychain password

To securely store your signing certificate and provisioning profiles on the CI runner, you need to create a new keychain. This keychain will require a password, which can be any random string of your choice.

Steps to Create a Keychain Password

  1. Generate a new password.
  2. Add this password as a secret in your GitHub secrets.
    In this example the password is named KEYCHAIN_PASSWORD

By storing your keychain password as a GitHub secret, you ensure that your CI/CD pipeline can securely create and access the keychain needed for code signing during the build process.

1.4 Configuring ExportOptions.plist

Once you have created your Distribution Certificate and Provisioning Profile, the next step is to configure the ExportOptions.plist file. This file will specify how your app should be packaged and exported, ensuring that the correct settings are applied during the CI/CD process.

Creating the ExportOptions.plist File

  1. Navigate to the ios directory of your Flutter project.
  2. Create a new directory named export_options
  3. Create 2 new files named export_options_adhoc.plist and export_options_appstore.plist in this directory.

Adding Configuration to ExportOptions.plist

The content of export_options_adhoc.plist and export_options_appstore.plist files will depend on your specific distribution needs. Below is an example configuration that includes both App Store and Ad Hoc distribution settings. This setup ensures that your app can be exported for both submitting to the App Store and for distributing to third-party testing services like Firebase Distribution or Diawi.

<?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>
<!-- Specify the distribution method, such as app-store or ad-hoc -->
<key>method</key>
<string>app-store</string> <!-- Change to ad-hoc for Ad Hoc distribution -->

<!-- Enable or disable App Thinning (Slicing) -->
<key>thinning</key>
<string>none</string>

<!-- Set the signing style to manual -->
<key>signingStyle</key>
<string>manual</string>

<!-- Specify the provisioning profiles for your app -->
<key>provisioningProfiles</key>
<dict>
<key>com.yourcompany.yourapp</key>
<string>YourApp_ProvisioningProfileName</string>
</dict>

<!-- Enable on-demand resources if required -->
<key>onDemandResources</key>
<false/>

<!-- Manage app's bitcode inclusion -->
<key>compileBitcode</key>
<true/>

<!-- Team ID associated with your Apple Developer account -->
<key>teamID</key>
<string>YourTeamID</string>
</dict>
</plist>

1.5 Switching to Manual Signing

To ensure a smooth and controlled build process in our CI/CD pipeline, we’ll switch to manual signing. Here’s how to do it and why it’s necessary:

Why Switch to Manual Signing?
When you automate iOS builds through CI/CD pipelines, like with GitHub Actions, you need precise control over the signing process. By switching to manual signing, you gain several benefits:

  • Control Over Certificates and Profiles: Manual signing allows you to explicitly specify the provisioning profiles and certificates used during the build process. This is crucial for ensuring that the correct credentials are used for different build targets (e.g., Ad Hoc distribution, App Store submission).
  • Consistency Across Builds: Automated workflows often involve using various certificates and profiles. Manual signing helps maintain consistency by using predefined profiles and certificates rather than relying on Xcode’s automatic management, which may not align with your CI/CD setup.

Steps to Switch to Manual Signing

  1. Open Your Flutter Project in Xcode
  • Open your Flutter project in Xcode by navigating to the ios directory of your Flutter project and double-clicking on the .xcworkspace file. This will launch Xcode and open your iOS project.

2. Navigate to Signing & Capabilities

  • In Xcode, select the project file from the Project Navigator (the left sidebar). This will open the project settings.
  • Under the PROJECT section (usually at the top), select the target for your app. This is typically named after your app and will be found under the TARGETS section.
  • Click on the Signing & Capabilities tab.

3. Switch to Manual Signing

  • In the Signing & Capabilities tab, look for the Signing section.
  • Uncheck the box labeled Automatically manage signing. This will switch the signing process from automatic to manual.

4. Select the Provisioning Profile and Certificate

  • After unchecking Automatically manage signing, you’ll need to specify the provisioning profile and signing certificate manually.
  • In the Team dropdown, select the appropriate development team. This should match the team associated with the provisioning profile and certificate.
  • In the Provisioning Profile dropdown, choose the provisioning profile you’ve set up. For example, select the App Store or Ad Hoc provisioning profile depending on your build target.
  • Ensure that the Signing Certificate dropdown is set to the appropriate certificate (e.g., Apple Distribution).

5. Verify Your Settings

  • Double-check that the provisioning profile and certificate you’ve selected match your app’s bundle identifier and the provisioning profile you’ve uploaded to your GitHub repository secrets.

Step 2: Creating a GitHub Actions Workflow File for iOS

GitHub Actions workflows are defined in YAML files. Create a directory named .github/workflows in the root of your project if it doesn’t already exist. Inside this directory, create a file named build-ios.yml (you can name it anything).

Your repository structure will look like this:

bigpay_app/
├── .github/
│ ├── workflows/
│ │ ├── build-ios.yml
├── ios/
│ ├── Runner.xcworkspace/
│ ├── …
├── lib/
├── test/
├── …

Open the newly created build-ios.yml and define when the action will start

name: Build iOS
on:
push:
branches: [master]

This basic configuration will trigger the workflow on every push request to the master branch

Now we need to define the os that will be used to run the action

name: Build iOS
on:
push:
branches: [master]

jobs:
build:
runs-on: macos-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

2.1 Install certificate and provisioning profiles

Next let’s install the certificate and provisioning profiles on the workflow runner so they can be used to sign your application.

name: Build iOS

on:
push:
branches: [master]

jobs:
build:
runs-on: macos-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install the Apple certificate and provisioning profile
run: |
# Create variables
DISTRIBUTION_CERTIFICATE_PATH=$RUNNER_TEMP/distribution_certificates.p12
PP_PATH_ADHOC=$RUNNER_TEMP/adhoc.mobileprovision
PP_PATH_APPSTORE=$RUNNER_TEMP/appstore.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

# Import certificate and provisioning profile from secrets
echo -n "${{ secrets.DISTRIBUTION_CERT_BASE64 }}" | base64 --decode -o $DISTRIBUTION_CERTIFICATE_PATH
echo -n "${{ secrets.BUILD_PROVISION_PROFILE_BASE64_ADHOC }}" | base64 --decode -o $PP_PATH_ADHOC
echo -n "${{ secrets.BUILD_PROVISION_PROFILE_BASE64_APPSTORE }}" | base64 --decode -o $PP_PATH_APPSTORE

# Check if the files exist
ls $DISTRIBUTION_CERTIFICATE_PATH
ls $PP_PATH_ADHOC
ls $PP_PATH_APPSTORE

# Create temporary keychain and unlock it
security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" $KEYCHAIN_PATH

# Keychain information
security show-keychain-info $KEYCHAIN_PATH

# Import certificates to keychain
security import $DISTRIBUTION_CERTIFICATE_PATH -P "${{ secrets.P12_PASSWORD }}" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "${{ secrets.KEYCHAIN_PASSWORD }}" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH

# Checking the validity of the code-signing identities
security find-identity -v -p codesigning $KEYCHAIN_PATH

# Check if the certificates were added
security find-certificate -a -p -c $DISTRIBUTION_CERTIFICATE_PATH $KEYCHAIN_PATH

# Apply provisioning profiles
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles

# Copy provisioning profiles
cp $PP_PATH_ADHOC ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH_APPSTORE ~/Library/MobileDevice/Provisioning\ Profiles

# Check if the provisioning profiles were added
ls ~/Library/MobileDevice/Provisioning\ Profiles

Here’s a brief overview of the process:

  1. Setup Variables: Define paths for storing the certificate and provisioning profiles.
  2. Import Certificate and Profiles: Decode and save the distribution certificate and provisioning profiles from GitHub Secrets.
  3. Verify Files: Check that the certificate and profiles are correctly saved.
  4. Create and Configure Keychain: Create and unlock a keychain to manage certificate.
  5. Import Certificate: Add the certificate to the keychain for code signing.
  6. Check Certificate Validity: Ensure that the certificate is valid for code signing.
  7. Apply Provisioning Profiles: Copy the provisioning profiles to the directory used by Xcode.
  8. Verify Profiles: Confirm that the provisioning profiles are correctly installed.

2.2 Set Up Flutter

Next we need to setup flutter and build the flutter code in the CI environment. This is achieved using the subosito/flutter-action GitHub Action, which simplifies the process of installing a specific version of Flutter.

    - name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.1'

2.3 Restore Packages

Next, we will restore the packages of our application. This is done using the flutter pub get command, which fetches the dependencies listed in the pubspec.yaml file.

    - name: Pub get
run: flutter pub get

This step ensures that all necessary packages are available for building the application.

2.4 Generate Adhoc staging ipa

To generate the staging IPA needed by firebase and diawi, we use the flutter build ipa command with specific options. This builds the iOS application for release, using the staging flavor and the specified export_options_adhoc.plist file for Adhoc distribution that we created earlier.

    - name: Build Staging App
run: flutter build ipa --release --flavor staging --export-options-plist=ios/export_options/export_options_adhoc.plist

2.5 Firebase app distribution

After building the Adhoc staging IPA, the next step is to upload this IPA to Firebase App Distribution. This allows your QA team or other testers to easily access and test the latest build.

Note: Before adding firebase action you need to setup FIREBASE_IOS_STAGING_APP_ID and CREDENTIAL_FILE_CONTENT in the secrets just as we did in part 1 of this series.

In this example FIREBASE_IOS_STAGING_APP_ID is added in a GitHub variable and CREDENTIAL_FILE_CONTENT as secrets.

Adding firebase action

    - name: "Install Firebase CLI"
run: sudo npm i -g firebase-tools
- name: Upload staging ipa to Firebase
run: |
echo '${{ secrets.CREDENTIAL_FILE_CONTENT }}' > staging_service_credentials_content.json
firebase appdistribution:distribute ./build/ios/ipa/staging.ipa --app ${{ vars.FIREBASE_IOS_STAGING_APP_ID }} --groups ios-qa
env:
GOOGLE_APPLICATION_CREDENTIALS: ./staging_service_credentials_content.json

Step Breakdown:

  1. Install Firebase
  2. name: Upload staging ipa to Firebase
    This step is named to clearly indicate its purpose: uploading the staging IPA to Firebase App Distribution.
  3. run: |
    The run keyword is used to specify the shell commands to be executed. The | symbol allows you to write a multi-line script.
  4. echo ‘${{ secrets.CREDENTIAL_FILE_CONTENT }}’ > staging_service_credentials_content.json
  • This command creates a new file named staging_service_credentials_content.json and writes the content of the CREDENTIAL_FILE_CONTENT secret into it.
  • ${{ secrets.CREDENTIAL_FILE_CONTENT }}: This is the GitHub secret that stores your Firebase service account credentials in JSON format.
  • > staging_service_credentials_content.json: This part of the command redirects the output to a file named staging_service_credentials_content.json.

5. firebase appdistribution./build/ios/ipa/staging.ipa — app ${{ vars.FIREBASE_IOS_STAGING_APP_ID }} — groups ios-qa

  • This command uses the Firebase CLI to distribute the IPA file.
  • ./build/ios/ipa/staging.ipa: The path to the IPA file that was generated in the previous step.
  • --app ${{ vars.FIREBASE_IOS_STAGING_APP_ID }}: Specifies the Firebase App ID for the staging environment. The App ID is stored in a GitHub variable and not secrets in this case.
  • --groups ios-qa: Specifies the tester group that should receive the distribution. In this case, the group is named ios-qa.

6. env: GOOGLE_APPLICATION_CREDENTIALS: ./staging_service_credentials_content.json

  • This sets an environment variable required by the Firebase CLI to authenticate with your Firebase project.
  • GOOGLE_APPLICATION_CREDENTIALS: The environment variable that points to the JSON file containing your Firebase service account credentials.
  • ./staging_service_credentials_content.json: The path to the credentials file created earlier in this step.

Step 2.6 Diawi Upload

Refer to part 1 on the requirements needed to setup diawi

Adding Diawi action

    - name: Upload staging ipa to Diawi
uses: rnkdsh/action-upload-diawi@v1.5.5
id: diawi-upload-ios-staging
with:
token: ${{ secrets.DIAWI_TOKEN }}
file: ./flutter/build/ios/ipa/staging.ipa

- name: Get Diawi link of uploaded staging ipa
run: echo "Diawi link ${{ steps.diawi-upload-ios-staging.outputs.url }}"

The step above uses the rnkdsh/action-upload-diawi GitHub Action.
Then uploads the staging IPA using the DIAWI_TOKEN secret for authentication and outputs the Diawi download link for the uploaded IPA.

Step 2.7 Build and Deploy to TestFlight

To use App Store Connect API 3 things are required

  1. Issuer ID.
  2. Key ID.
  3. Key content.

Configuring App Store Connect API access
You can skip this steps if you already have an api key with issuer info

  1. Set up an App Store Connect API key on the Users & Access section by going to the integrations tab and then:
  2. The page opens with the App Store Connect API selected.
  3. Click Team Keys.
  4. Click Generate API Key. If you already have an Active API key generated, you can click the add button (+) to add more.
  5. Enter a name for the key. The name is used for your reference only and isn’t part of the key itself.
  6. Under Access, select the role permissions to determine what the API can be used for. Team API keys are applied across all apps, so app access can’t be limited for an API key.
  7. You can choose Developer as the Access for the key, and click Generate.
  8. From here, copy the Issuer ID, Key ID and Click “Download API Key” to download your API private key. The download link appears only if the private key has not yet been downloaded

Once you generate an API key, you won’t be able to edit its name or access level. If you need to make changes, revoke the API key and generate a new one.

Add Key to Github Secrets

Locate the API key file you downloaded from App Store Connect (the file is named something like AuthKey_KEYID.p8) and convert to base64.

Proceed to Github and add a new Github Secret named APPSTORE_API_KEY_BASE64 and paste the base64 AuthKey_KEYID . Also add the key Issuer ID and Key ID as secrets named APPSTORE_API_KEY_ID and APPSTORE_API_ISSUER_ID in this example.

Adding TestFlight action

- name: Build Appstore App
run: flutter build ipa --release --flavor staging --export-options-plist=ios/export_options/export_options_appstore.plist

- name: Upload ipa testflight
run: |
mkdir -p ./private_keys
echo -n ${{ secrets.APPSTORE_API_KEY_BASE64 }} | base64 --decode -o "./private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8"
xcrun altool --validate-app -f ./build/ios/ipa/staging.ipa -t ios --apiKey ${{ secrets.APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.APPSTORE_API_ISSUER_ID }}
xcrun altool --upload-app -f ./build/ios/ipa/staging.ipa -t ios --apiKey ${{ secrets.APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.APPSTORE_API_ISSUER_ID }}

Step Breakdown:

  1. Build the App Store App
    This step compiles your Flutter app into an IPA file configured for release to the App Store. Note we are using export_options_appstore.plist to export to app-store and not adhoc
  2. Upload the IPA to the App Store
  • Prepare the API Key: Creates a directory for private keys, decode the base64 string App Store API key, and save it as a .p8 file.
  • Validate the IPA: Uses xcrun altool to validate the IPA file to ensure it meets App Store requirements.
  • Upload the IPA: Uses xcrun altool to upload the validated IPA file to the App Store.

These steps automate the process of building and distributing your iOS app to the App Store, ensuring it is validated and uploaded correctly.

Clean up the keychain

    - name: Clean up keychain and provisioning profiles
if: ${{ always() }}
run: |
# Check if the keychain exists before attempting to delete it
if security list-keychains | grep -q $RUNNER_TEMP/app-signing.keychain-db; then
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
fi

# Remove private_keys directory if it exists
if [ -d "./private_keys" ]; then
rm -rf ./private_keys
fi

# Remove provisioning profiles if they exist
if [ -d "~/Library/MobileDevice/Provisioning Profiles" ]; then
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision

In the last step of our Workflow, we added a step that will always be called to clean up the keychain.

GitHub-hosted runners are isolated virtual machines that are automatically destroyed at the end of the job execution. This means that the certificates and provisioning profiles used on the runner during the job will be destroyed with the runner when the job is completed.

On self-hosted runners, the $RUNNER_TEMP directory is cleaned up at the end of the job execution, but the keychain and provisioning profile might still exist on the runner.

The summarized Workflow looks like the following:

name: Build iOS
on:
push:
branches: [master]

jobs:
build:
runs-on: macos-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install the Apple certificate and provisioning profile
run: |
# Create variables
DISTRIBUTION_CERTIFICATE_PATH=$RUNNER_TEMP/distribution_certificates.p12
PP_PATH_ADHOC=$RUNNER_TEMP/adhoc.mobileprovision
PP_PATH_APPSTORE=$RUNNER_TEMP/appstore.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

# Import certificate and provisioning profile from secrets
echo -n "${{ secrets.DISTRIBUTION_CERT_BASE64 }}" | base64 --decode -o $DISTRIBUTION_CERTIFICATE_PATH
echo -n "${{ secrets.BUILD_PROVISION_PROFILE_BASE64_ADHOC }}" | base64 --decode -o $PP_PATH_ADHOC
echo -n "${{ secrets.BUILD_PROVISION_PROFILE_BASE64_APPSTORE }}" | base64 --decode -o $PP_PATH_APPSTORE

# Check if the files exist
ls $DISTRIBUTION_CERTIFICATE_PATH
ls $PP_PATH_ADHOC
ls $PP_PATH_APPSTORE

# Create temporary keychain and unlock it
security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" $KEYCHAIN_PATH

# Keychain information
security show-keychain-info $KEYCHAIN_PATH

# Import certificates to keychain
security import $DISTRIBUTION_CERTIFICATE_PATH -P "${{ secrets.P12_PASSWORD }}" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "${{ secrets.KEYCHAIN_PASSWORD }}" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH

# Checking the validity of the code-signing identities
security find-identity -v -p codesigning $KEYCHAIN_PATH

# Check if the certificates were added
security find-certificate -a -p -c $DISTRIBUTION_CERTIFICATE_PATH $KEYCHAIN_PATH

# Apply provisioning profiles
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles

# Copy provisioning profiles
cp $PP_PATH_ADHOC ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH_APPSTORE ~/Library/MobileDevice/Provisioning\ Profiles

# Check if the provisioning profiles were added
ls ~/Library/MobileDevice/Provisioning\ Profiles

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.1'

- name: Pub get
run: flutter pub get

- name: Build Staging App
run: flutter build ipa --release --flavor staging --export-options-plist=ios/export_options/export_options_adhoc.plist

- name: Install Firebase CLI
run: sudo npm i -g firebase-tools

- name: Upload staging ipa to Firebase
run: |
echo '${{ secrets.CREDENTIAL_FILE_CONTENT }}' > staging_service_credentials_content.json
firebase appdistribution:distribute ./build/ios/ipa/staging.ipa --app ${{ vars.FIREBASE_IOS_STAGING_APP_ID }} --groups ios-qa
env:
GOOGLE_APPLICATION_CREDENTIALS: ./staging_service_credentials_content.json

- name: Upload staging ipa to Diawi
uses: rnkdsh/action-upload-diawi@v1.5.5
id: diawi-upload-ios-staging
with:
token: ${{ secrets.DIAWI_TOKEN }}
file: ./flutter/build/ios/ipa/staging.ipa

- name: Get Diawi link of uploaded staging ipa
run: echo "Diawi link ${{ steps.diawi-upload-ios-staging.outputs.url }}"

- name: Build Appstore App
run: flutter build ipa --release --flavor staging --export-options-plist=ios/export_options/export_options_appstore.plist

- name: Upload ipa to TestFlight
run: |
mkdir -p ./private_keys
echo -n ${{ secrets.APPSTORE_API_KEY_BASE64 }} | base64 --decode -o "./private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8"
xcrun altool --validate-app -f ./build/ios/ipa/staging.ipa -t ios --apiKey ${{ secrets.APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.APPSTORE_API_ISSUER_ID }}
xcrun altool --upload-app -f ./build/ios/ipa/staging.ipa -t ios --apiKey ${{ secrets.APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.APPSTORE_API_ISSUER_ID }}

- name: Clean up keychain and provisioning profiles
if: ${{ always() }}
run: |
# Check if the keychain exists before attempting to delete it
if security list-keychains | grep -q $RUNNER_TEMP/app-signing.keychain-db; then
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
fi

# Remove private_keys directory if it exists
if [ -d "./private_keys" ]; then
rm -rf ./private_keys
fi

# Remove provisioning profiles if they exist
if [ -d "~/Library/MobileDevice/Provisioning Profiles" ]; then
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision
fi

The Workflow in Action

Now that we’ve completed our Workflow, accessing it is straightforward. Just head to the “Actions” tab in our GitHub repository, and you’ll find it there!

With the new CI/CD setup, pushing updates is now a breeze — just a single push to the main branch initiates a seamless process that handles everything from build to distribution. This not only reduces manual intervention but also minimizes the stress and complexity associated with deployment.

Results

Implementing a robust CI/CD pipeline for Flutter iOS development has significantly accelerated BigPay’s developer velocity. By integrating GitHub Actions, we’ve streamlined our build and deployment processes, from setting up certificates and provisioning profiles to automating the distribution of builds through Firebase and Diawi, and even pushing to TestFlight. The impact has been just as transformative. Build times have been optimized, deployments are more consistent, and our development team can now dedicate more time to innovating new features rather than managing the complexities of deployment.

Conclusion

By adopting GitHub Actions for our iOS CI/CD pipeline, we’ve refined our development workflow and boosted efficiency, just like we did for Android. The automation of build, test, and deployment processes has proven essential for maintaining high software quality and accelerating deployment. With these advancements, we deliver updates to our users with greater speed and reliability, further enhancing their experience with BigPay.

Stay tuned for Part 3, where we’ll delve into our successful migration from native to Flutter. Our journey towards innovation and excellence continues, and we can’t wait to share the insights and experiences from this next chapter.

References

--

--

Bashir Isyaka
bigpay-tech-blog

I live, breathe and sometimes eat code 24/7. Yes, that is not impossible