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:
Let’s begin with a basic 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.
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.
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:
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 should add them to your GitHub Secrets. Congratulations, you are ready to distribute your application.
Using GitHub actions as a continuous delivery tool has provided a comfortable, reliable service for building and deployment of any app. Benefits worth mentioning:
Overall, GitHub Actions helped us deliver apps in different configurations much faster on multiple platforms at a time.