If you're looking to streamline your Flutter app's deployment to the AppStore and PlayStore, you've come to the right place. We'll be using Fastlane and GitHub Actions to automate our workflow. By the end of this tutorial, you'll have an automated pipeline that builds and deploys your Flutter app to your preferred app stores. In this guide we will skip all the setup and configuration of Fastlane and GitHub Actions, and will focus on the Flutter and workflow specific parts of the setup.
Repository: Flutter App CI/CD with Fastlane and GitHub Actions
- A Flutter app
- Flavors and environment Variables set up in your Flutter app. You can follow the official Flutter guide to set this up.
- You should be able to build a release version for Android and iOS. You can follow the Android guide and iOS guide to set this up.
- A GitHub account with your app repo.
- Fastlane installed on your local machine. You can follow the official Fastlane installation guide.
- AppStore and PlayStore accounts ready for deployment with your app project setup.
- Follow the initial CD Flutter Guide to setup the Fastlane folders in your Flutter project.
When you have completed all the prerequisites, you should have the following project structure with Fastlane folders in both iOS and Android folders:
- android
- fastlane
- Appfile
- Fastfile
- ios
- fastlane
- Appfile
- Fastfile
Note: In this guide I will use the basic Fastlane commands. It is highly recommended to use Fastlane with
bundle
. Read more about it in this Fastlane documentation.
Now we will setup the Fastfiles for both iOS and Android. We will define lanes for deploying the app to the AppStore and PlayStore.
-
Open
android/Gemfile
and add the flutter version plugin we need to extract the version from the pubspec.yaml file:# ... source "https://rubygems.pkg.github.com/tianhaoz95" do gem "fastlane-plugin-flutter_version", "1.1.15" end
-
Open
android/fastlane/Appfile
and set the path to your service account JSON file and the package name of your app:json_key_file("path/to/your/service-account.json") package_name("com.example.yourapp")
You have created the service account JSON file in the CD Flutter Guide. Make sure to exclude the file from your git repository.
-
Open
android/fastlane/Fastfile
and define thedeploy
lane (delete the existing content):default_platform(:android) platform :android do desc "Deploy a new version to Google Play" lane :deploy do version = flutter_version() upload_to_play_store( track: 'internal', # Can be 'internal', 'alpha', 'beta', 'production' skip_upload_metadata: true, # Skip uploading metadata skip_upload_images: true, # Skip uploading screenshots skip_upload_screenshots: true, # Skip uploading screenshots release_status: "completed", # Can be 'draft', 'completed', 'halted' aab: '../build/app/outputs/bundle/release/app-release.aab', # Path to your AAB file version_code: version["version_code"], # From pubspec.yaml version_name: version["version_name"] + version["version_code"], ) end end
More information about the
upload_to_play_store
action can be found here. -
Run
bundle install
in theandroid
directory to install the required gems. -
Run
fastlane supply init
to initialize the PlayStore metadata. This will allow you to update the metadata like app description or screenshots without leaving your IDE. You can skip this step if you don't want to upload any metadata. -
Build the app with
flutter build appbundle --release
and make sure the AAB file is located in the path you defined in the Fastfile and to use the release signing keys. Follow the Android guide to set up release signing. -
Before deploying your first release you need to have at least one release already uploaded and published manually.
-
Run
fastlane deploy
to start the lane and deploy your app to the PlayStore.
Depending on your setup, you might need to adjust the upload_to_play_store
action to match your requirements.
Google Play Store Release Dashboard
-
Open
ios/Gemfile
and add the Flutter version plugin again:# ... source "https://rubygems.pkg.github.com/tianhaoz95" do gem "fastlane-plugin-flutter_version", "1.1.15" end
-
Open
ios/fastlane/Appfile
and define app the identifier, the Apple ID of your Apple developer account, and the team ID:app_identifier("com.example.yourapp") apple_id("[email protected]") itc_team_id("123456")
-
Open
ios/fastlane/Fastfile
and define thedeploy
lane (delete the existing content):platform :ios do lane :deploy do pilot( skip_waiting_for_build_processing: true, # Skip waiting so we don't waste precious build time changelog: "This build was uploaded using fastlane", ipa: "../build/ios/ipa/flutter_github_actions.ipa" # Path to your IPA file ) end end
More information about
pilot
can be found in the Fastlane documentation. Depending on your setup you might need to use actions likegym
ormatch
to build the IPA file. -
Run
bundle install
in theios
directory to install the required gems. -
Run
fastlane deliver init
to initialize the AppStore metadata. With this you can update the metadata like app description or screenshots without leaving your IDE. You can skip this step if you don't want to upload metadata. -
Make sure you have already created an app in AppStore Connect.
-
Depending on the account you are using you might need to further authenticate with an app-specific password. Refer to the Fastlane documentation for more information.
-
Run
fastlane deploy
to start the lane and deploy your app to the AppStore.
Depending on your setup, you might need to customize the actions to suit your requirements. Also consider to use the actions.
AppStore TestFlight Builds Dashboard
Now that Fastlane is set up and you have successfully run the lanes manually on your local machine, we can automate the deployment process using GitHub Actions.
Note: For simplicity, we will use a self-hosted runner in this guide. You can also use the GitHub hosted runners. Make sure to adjust the paths and the Fastfile accordingly and also add any necessary tools to the runner.
Now, let's automate these processes with GitHub Actions:
-
In your GitHub repo, create a new directory
.github/workflows
. -
Create a new file in this directory, e.g.
deploy.yml
. -
We need to create some secrets. Go to your repository settings and add the following secrets:
STORE_PASSWORD
: The password for your keystore file.KEY_JKS
: The base64 encoded keystore file.SEC_JSON
: The base64 encoded service account JSON file.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
: The app-specific password for your Apple developer account.
To encode the files you can use the following command:
cat path/to/your/file | openssl base64
Copy the output and add it as a secret.
-
Add the following content to the
deploy.yml
file:name: Build and Deploy on: workflow_dispatch: jobs: build_android: concurrency: group: ${{ github.workflow }}-${{ github.ref }}-android cancel-in-progress: true name: Build and Deploy Android runs-on: self-hosted steps: - uses: actions/checkout@v4 - uses: actions/[email protected] with: distribution: "zulu" java-version: "17" - name: Create Key properties file run: | cat << EOF > "./android/key.properties" storePassword=${{ secrets.STORE_PASSWORD }} keyPassword=${{ secrets.STORE_PASSWORD }} keyAlias=upload storeFile=./key.jks EOF - name: Decode key file run: echo "${{ secrets.KEY_JKS }}" | openssl base64 -d -out ./android/app/key.jks - name: Decode sec json file run: echo "${{ secrets.SEC_JSON }}" | openssl base64 -d -out ./android/sec.json - uses: subosito/flutter-action@v2 - run: flutter packages pub get - run: flutter build appbundle --release - name: Fastlane Action uses: maierj/[email protected] with: lane: deploy subdirectory: android build_ios: concurrency: group: ${{ github.workflow }}-${{ github.ref }}-ios cancel-in-progress: true name: Build and Deploy iOS runs-on: self-hosted env: FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 - run: flutter packages pub get - run: flutter build ipa --release - name: Deploy iOS Beta to TestFlight via Fastlane uses: maierj/[email protected] with: lane: deploy subdirectory: ios
Let me break down what this workflow does. There are two jobs,
build_android
andbuild_ios
. Each job builds and deploys the app to the PlayStore and AppStore respectively. The jobs are manually triggered by theworkflow_dispatch
event. To build the Android app we need to create thekey.properties
file and decode the keystore and service account JSON file. Then we build the app bundle and run the Fastlane action with thedeploy
lane. The iOS job is simpler, we just build the IPA file and run the Fastlane action with thedeploy
lane. -
Adjust the
flutter-version
as per your project's requirements. -
Trigger the workflow manually by going to the Actions tab in your GitHub repository and selecting the
Build and Deploy
workflow. Click on theRun workflow
button and select the branch you want to deploy. -
Sit back and watch the magic happen! ✨
Whenever you want to deploy a new version of your app, just edit the version in pubspec.yaml
, push your changes to the main branch and trigger the workflow manually. The workflow automatically will build and deploy your app to the AppStore and PlayStore automatically.
For some projects it makes sense to use the build number currently used on the respective platform. There are actions to retrieve the build number from the AppStore and PlayStore. You can use the app_store_build_number
and google_play_track_version_codes
actions to get the build number. You can then use this build number to set the version code in the Fastfile.
And that's it! You've now set up a CI/CD pipeline for your Flutter app using Fastlane and GitHub Actions. This setup will automatically build and deploy your app to the AppStore and PlayStore. Time to sit back, relax, and let automation take care of the repetitive tasks.
We are still not done here. The next guide will cover how to automate the versioning, testing and of your app with Fastlane. Stay tuned! Happy coding! 🚀
Dario is a passionate and innovative Senior Flutter Developer at NTT Data with a keen interest in crafting seamless user experiences using cutting-edge technology. You can find him on LinkedIn and GitHub.
In the previous article, we saw how to setup Fastlane and GitHub Actions for the sample app. In this article, we will see how to further automate the release process further with versioning, GitHub Releases and promotion using Fastlane and GitHub Actions.
Repository: Flutter App CI/CD using Fastlane and GitHub Actions
- Follow the first part of the guide to setup Fastlane and GitHub Actions.
We will create a new workflow that will bump the pubspec.yaml
version, create a new tag and pushes the changes to the repository.
- Create a new file in the
.github/workflows
directory, e.g.tag.yml
. - Add the following content to the
tag.yml
file:This workflow can be triggered manually and accepts an input on what action you want to perform. There are four options:name: Tag Release on: workflow_dispatch: inputs: action: type: choice description: Action type default: none options: - major - minor - patch - none jobs: tag: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true name: Tag runs-on: self-hosted permissions: # Give the default GITHUB_TOKEN write permission to commit and push the # added or changed files to the repository. contents: write steps: - uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} fetch-depth: 0 # Fetch the complete history for tags and branches - uses: stikkyapp/update-pubspec-version@v2 id: update-pubspec-version with: strategy: ${{ github.event.inputs.action }} bump-build: true - uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "Bump version to ${{ steps.update-pubspec-version.outputs.new-version }}" commit_user_name: GitHub Actions tagging_message: release/v${{ steps.update-pubspec-version.outputs.new-version }}
major
,minor
,patch
andnone
. Each action will bump the version accordingly. See this for more information on semantic versioning. We use theupdate-pubspec-version
action to bump the version in thepubspec.yaml
file and thegit-auto-commit-action
to commit the changes and create a new tag with the formatrelease/v1.0.0
. Each run of the workflow will always bump the build number. - Trigger the workflow manually by going to the Actions tab in your GitHub repository and selecting the
Tag Release
workflow. Click on theRun workflow
button and select the branch you want to tag. - Sit back and watch the magic happen! ✨ You should see a new commit and tag in your repository.
- Create a new file in the
.github/workflows
directory, e.g.release.yml
. - Add the following content to the
release.yml
file:This workflow is triggered whenever a new tag with the formatname: Create Release on: push: tags: - 'release/*' jobs: release: concurrency: group: ${{ github.workflow }}-${{ github.ref }}-release cancel-in-progress: true runs-on: self-hosted steps: - uses: actions/checkout@v4 - name: Create Release uses: ncipollo/release-action@v1 with: name: ${{ github.ref_name }} tag: ${{ github.ref }} generateReleaseNotes: true
release/*
is pushed to the repository. The action will create a new release with the tag name and generate the release notes based on the commit messages and merged branches since the last tag. - We are not done yet. Triggering the Tag workflow doesn't trigger the release workflow yet. This is because due to a limitation of GitHub. Follow this guide to create a token with the scope
repo
scope. Add the token to your repository as a secret with the namePAT
. Now we need to add the token to thecheckout
action in thetag.yml
file:Since the# Remove from here permissions: # Give the default GITHUB_TOKEN write permission to commit and push the # added or changed files to the repository. contents: write # Until here steps: - uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} fetch-depth: 0 # Fetch the complete history for tags and branches token: ${{ secrets.PAT }}
checkout
action uses theGITHUB_TOKEN
by default, we need to add thePAT
token to the action to trigger the Release workflow. We can then remove thepermissions
part. It is better to trigger the workflow manually only, otherwise the workflow will run in a loop. - Now trigger the
Tag
workflow again and you should see a new release in your repository after theRelease workflow
has finished successfully. - To trigger the
Build and Deploy
workflow whenever a new tag is created. Add this to yourdeploy.yml
file:This will trigger theon: workflow_dispatch: push: tags: - 'release/*'
Build and Deploy
workflow whenever theTag workflow
creates a new tag. This way you can automate the whole process from versioning to deployment.
The idea is to have a workflow that promotes a release to a specific track in the PlayStore. This can be useful if you want to promote a release from the internal track, to the production track or from TestFlight to the AppStore for example.
-
Add this lane to
android/fastlane/Fastfile
:platform :android do # ... desc "Promote version" lane :promote do |options| skip = options[:skip] || true version = flutter_version() upload_to_play_store( track: "internal", track_promote_to: "production", skip_upload_metadata: false, skip_upload_images: skip, skip_upload_screenshots: skip, track_promote_release_status: "draft", version_code: version["version_code"], version_name: version["version_name"], ) end end
Add this lane to
ios/fastlane/Fastfile
:platform :ios do # ... desc "Promote version" lane :promote do |options| skip = options[:skip] || true version = flutter_version() deliver( submit_for_review: false, automatic_release: true, force: true, skip_screenshots: skip, skip_binary_upload: true, overwrite_screenshots: true, app_version: version["version_name"], precheck_include_in_app_purchases: false ) end end
We added a promote lane for both Android and iOS. The lane will promote the release from the internal track or TestFlight to the production track in the PlayStore and AppStore respectively. We also added an option to skip the screenshots upload.
-
Before you can promote a release, you must have uploaded and published at least one release manually already. Make sure you have a release on the production track in the PlayStore and the app published in the AppStore.
-
Run
fastlane promote skip:true
to start the lane and promote your the version to the respective track. Make sure to run the command in the respective platform folder. -
After successfully promoting the release you can now automate the process using GitHub Actions.
-
Create a new file in the
.github/workflows
directory, e.g.promote.yml
. -
Paste this content into the
promote.yml
file:name: Promote Release on: workflow_dispatch: inputs: skip: type: boolean description: skip screenshots default: true jobs: android: concurrency: group: ${{ github.workflow }}-${{ github.ref }}-android cancel-in-progress: true name: Promote Android runs-on: self-hosted steps: - uses: actions/checkout@v4 - name: Decode sec json file run: echo "${{ secrets.SEC_JSON }}" | openssl base64 -d -out ./android/sec.json - name: Promote Release on Play Store uses: maierj/[email protected] with: subdirectory: android lane: promote ios: concurrency: group: ${{ github.workflow }}-${{ github.ref }}-ios cancel-in-progress: true name: Promote iOS runs-on: self-hosted env: FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} steps: - uses: actions/checkout@v4 - name: Promote Release on App Store uses: maierj/[email protected] with: subdirectory: ios lane: promote skip:${{ github.event.inputs.skip }}
This workflow can be triggered manually and accepts an input if you want to upload screenshots as well. The workflow will take a release and promote it to the respective track in the PlayStore or AppStore.
-
Trigger the workflow manually by going to the Actions tab in your GitHub repository and selecting the
Promote Release
workflow. Click on theRun workflow
button and select the branch or tag you want to promote. Make sure that the version you want to promote has already been uploaded and published to the respective track. You can do this with theDeploy
workflow.
So how does the whole process look like now? Whenever you want to release a new version of your app, you can manually trigger the Tag Release
workflow. You can choose between major
, minor
and patch
. The workflow will bump the version in the pubspec.yaml
file, create a new commit and tag and push the changes to the repository. This will trigger the Release workflow
which will create a new release with the tag name and generated release notes. This will also trigger the Build and Deploy
workflow which will build and deploy the app to the AppStore and PlayStore. After you have successfully deployed the app, you can manually trigger the Promote Release
workflow. This will promote the release to the respective track in the PlayStore and AppStore.
And that's it! You've now set up a CI/CD pipeline for your Flutter app using Fastlane and GitHub Actions. This setup will automatically build and deploy your app to the AppStore and PlayStore. You've also automated the versioning, tagging and release process with GitHub Actions. Time to kick back, relax, and let automation take care of the repetitive tasks.
Dario is a passionate and innovative Senior Flutter Developer at NTT Data with a keen interest in crafting seamless user experiences using cutting-edge technology. You can find him on LinkedIn and GitHub.