We are in a state where companies are releasing software and solutions within minutes, and they are doing so by following the continuous integration (CI) and continuous delivery (CD) set of operating principles.
A CI/CD pipeline makes the automatic delivery of your software more frequent, reliable, and secure. It focuses on higher code quality, and that’s why it is vital for a mobile developer or team.
In this tutorial, you’ll learn how to deploy your Flutter app following CI/CD principles with GitHub Actions as a tool.
This tutorial requires a Google service account, which will be used in GitHub Actions to publish the Android build to the Play Store, so to create a project in GCP, create a service account and select your created project.
N.B., this tutorial assumes you have some prior knowledge of Flutter. If you are new to Flutter, please go through the official documentation to learn about it.
GitHub Actions is a CI/CD tool that helps you build, test, and deploy your changes on production directly from your repository. You can use it to set up app releases on certain events like committing a tag in a certain branch of your repository.
Additionally, one doesn’t have to create a workflow for common things across the projects as GitHub has a marketplace from which you can use existing workflows developed by others.
The GitHub Actions workflow uses .yml
(“YAML Ain’t Markup Language”) files, which will be stored in the .github
directory at the root of your project.
Additionally, GitHub Actions supports different environments and containers like Linux, macOS, Windows, and even VMS.
Follow the below steps for initial setup:
.github
and a new directory called workflows
. The workflows here will contain all your CI/CD workflows as .yml
filesNow, you will create a basic Android workflow to help you understand how GitHub Actions works in building your Flutter app.
Create a .yml
file, android-release.yml
, inside workflows
with the following code:
name: Android Release # 1 on: # 2 push: branches: [ "master" ] pull_request: branches: [ "master" ] # 3 workflow_dispatch: # 4 jobs: # 5 build: # 6 runs-on: ubuntu-latest # 7 steps: # 8 - uses: actions/checkout@v3 # 9 - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: "12.x" # 10 - uses: subosito/flutter-action@v2 with: # 11 flutter-version: "3.0.0" channel: 'stable' # 12 - name: Get dependencies run: flutter pub get # Runs a set of commands using the runners shell - name: Start release build run: flutter build appbundle
The above workflow:
"master"
branch; you can change it according to your requirementbuild
$GITHUB_WORKSPACE
, so your job can access itThe problem with this basic workflow is that whenever you push changes in the master
branch, this workflow will trigger and start setting up the Java SDK and Flutter SDK every time. So eventually, it will lead to the latency of building your application as you have to set up services every time.
You can make your Flutter workflow faster by caching the Java and Flutter SDKs so that on the next run, it won’t fetch the SDK directly before checking for the existence of the SDKs.
In your main.yaml
file, make the following changes:
- uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: "12.x" cache: 'gradle' // 1 - uses: subosito/flutter-action@v2 with: flutter-version: "3.0.0" channel: 'stable' cache: true // 2
You have updated the SDK setup by providing Gradle to be cached with respect to the Java SDK (1
) and enabling caching for the Flutter SDK (2
).
Next time you run the job post, save the above changes and observe the time spent; you will see a time difference from the basic flow.
Now you will be expanding your workflow to create an Android Play Store release.
For any new release, you should have a new release version number, so before the build, you need to create a version number using the below job:
# 1 version: name: Create version number # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: - uses: actions/checkout@v3 # 2 - name: Install GitVersion uses: gittools/actions/gitversion/[email protected] with: versionSpec: "5.x" - name: Use GitVersion id: gitversion uses: gittools/actions/gitversion/[email protected] # 3 - name: Create version.txt with nuGetVersion run: echo ${{ steps.gitversion.outputs.nuGetVersion }} > version.txt # 4 - name: Upload version.txt uses: actions/upload-artifact@v2 with: name: gitversion path: version.txt
In the above code, we did the following:
version.text
fileversion.text
file as an artifact for the actions system with a name gitversion
to be used later in the build jobTo publish the app to Play Store, you need to give your app a digital signature using a keystore. Follow this official Flutter Doc on how to do that depending upon your machine:
keytool -genkey -v -keystore %userprofile%\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
This will store a file with a .jks
extension in your home directory or whatever path you provided.
N.B., make sure to add the store password, key password, and key alias in your GitHub repository secrets (from GitHub repository > Secrets > Actions)
If you are facing “’keytool’ is not recognized as an internal or external command” issue, then add the path of the JDK bin to use environment variables, or else install JDK and repeat the path addition to environment variables.
Next, create a new file key.properties
under the Android directory of your app and provide the reference to your keystore generated before:
storePassword=<password from previous step> keyPassword=<password from previous step> keyAlias=upload storeFile=<location of the key store file, such as /Users/<user name>/your-keystore-file.jks>
To use this key when building your app in release mode, update your Android-level build.gradle
file as below:
keyProperties
variable to refer to the key.properties
file from the filesystem
:
def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) }
buildTypes
and add the signingConfigs
as below:
signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release } }
After this, your new build will be created in release mode using the key.
N.B., don’t commit the keystore
key and the key.properties
file and let them be private.
So you might be wondering, how can our job know whether this key exists in the filesystem and use it as a reference in the key.properties
file?
To resolve this, do the following:
Base64
encode your keystore file in your machine using Git Bash or Bash:
base64 <your-keystore-file.jks>
ANDROID_KEYSTORE_BASE64
in your GitHub repositoryANDROID_KEYSTORE_BASE64
in your GitHub repository; it’ll remain safe thereNow update the build job in your android-release.yml
file:
build: name: Create Android Build # 1 needs: version runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 # 2 - name: Get version.txt uses: actions/download-artifact@v2 with: name: gitversion # 3 - name: Create new file without newline char from version.txt run: tr -d '\n' < version.txt > version1.txt # 4 - name: Read version id: version uses: juliangruber/read-file-action@v1 with: path: version1.txt # 5 - name: Update version in YAML run: sed -i 's/99.99.99+99/${{ steps.version.outputs.content }}+${{ github.run_number }}/g' pubspec.yaml # 6 - name: Download Android keystore id: android_keystore uses: timheuer/[email protected] with: fileName: upload-keystore.jks encodedString: ${{ secrets.KEYSTORE_BASE64 }} # 7 - name: Create key.properties run: | echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > android/key.properties echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/key.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: "12.x" cache: gradle - uses: subosito/flutter-action@v2 with: flutter-version: "3.0.0" channel: 'stable' cache: true - name: Get dependencies run: flutter pub get - name: Start Android Release Build run: flutter build appbundle # 8 - name: Upload Android Release uses: actions/upload-artifact@v2 with: name: android-release path: build/app/outputs/bundle/release/app-release.aab
In the above code, you performed the following:
gitversion
newline char
from version.txt
version1.txt
filepubspec.yml
file with the version ID having the version in itbase64
encoded keystore value saved as a secret to ID android_keystore
key.properties
using the secrets and android_keystore
Now, you need to use the bundle and send it to Play Store. Before that, it is time to make use of the service account that you created at the start of this tutorial. If the service account is created, copy the key for that account and store it in secrets as PLAYSTORE_ACCOUNT_KEY
.
Next, in your Google Play Console > Users & Permissions, invite the user and add the service account user email here.
If you are not seeing your app in App permissions, make sure that the Google Play Developer API in GCP is enabled for your project.
Next, update the permission of the user so that it has the access to release the app like the admin role.
Now, add a new job deploy in your android-release
flow:
deploy: name: Deploy Android Build # 1 needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 # 2 - name: Get Android Build from artifacts uses: actions/download-artifact@v2 with: name: android-release # 3 - name: Release Build to internal track uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }} packageName: <YOUR_PACKAGE_NAME> releaseFiles: app-release.aab track: alpha status: completed
Here, you did the following:
android-release
PLAYSTORE_ACCOUNT_KEY
secret, your app package name, the track in which you want to upload the build and its statusAfter this, push your changes to GitHub and see the workflow deploy your app to the Play Store.
Here’s the complete workflow:
name: Android Release on: push: branches: [ "master" ] pull_request: branches: [ "master" ] workflow_dispatch: jobs: version: name: Create version number runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install GitVersion uses: gittools/actions/gitversion/[email protected] with: versionSpec: "5.x" - name: Use GitVersion id: gitversion uses: gittools/actions/gitversion/[email protected] - name: Create version.txt with nuGetVersion run: echo ${{ steps.gitversion.outputs.nuGetVersion }} > version.txt - name: Upload version.txt uses: actions/upload-artifact@v2 with: name: gitversion path: version.txt build: name: Create Android Build needs: version runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Get version.txt uses: actions/download-artifact@v2 with: name: gitversion - name: Create new file without newline char from version.txt run: tr -d '\n' < version.txt > version1.txt - name: Read version id: version uses: juliangruber/read-file-action@v1 with: path: version1.txt - name: Update version in YAML run: sed -i 's/99.99.99+99/${{ steps.version.outputs.content }}+${{ github.run_number }}/g' pubspec.yaml - name: Download Android keystore id: android_keystore uses: timheuer/[email protected] with: fileName: upload-keystore.jks encodedString: ${{ secrets.KEYSTORE_BASE64 }} - name: Create key.properties run: | echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > android/key.properties echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/key.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: "12.x" cache: gradle - uses: subosito/flutter-action@v2 with: flutter-version: "3.0.0" channel: 'stable' cache: true - name: Get dependencies run: flutter pub get - name: Start Android Release Build run: flutter build appbundle - name: Upload Android Release uses: actions/upload-artifact@v2 with: name: android-release path: build/app/outputs/bundle/release/app-release.aab deploy: name: Deploy Android Build needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Get Android Build from artifacts uses: actions/download-artifact@v2 with: name: android-release - name: Release Build to internal track uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }} packageName: <YOUR_PACKAGE_NAME> releaseFiles: app-release.aab track: alpha status: completed
Note:
Now create a new web-release.yml
workflow and paste the following code:
name: Web Release on: push: branches: [ "master" ] pull_request: branches: [ "master" ] workflow_dispatch: jobs: build: name: Create Web Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: "12.x" cache: gradle - uses: subosito/flutter-action@v2 with: flutter-version: "3.0.0" channel: 'stable' cache: true - name: Get dependencies run: flutter pub get - name: Start Web Release Build run: flutter build web --release - name: Upload Web Build Files uses: actions/upload-artifact@v2 with: name: web-release path: ./build/web deploy: name: Deploy Web Build needs: build runs-on: ubuntu-latest steps: - name: Download Web Release uses: actions/download-artifact@v2 with: name: web-release - name: Deploy to gh-pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./
The above workflow is quite similar to the Android workflow, but here you are using the Flutter web build
command and later using the peaceiris/actions-gh-pages@v3 workflow to deploy the web build to GitHub Pages.
Note:
GITHUB_TOKEN
is not a personal access token. It gets automatically created to authenticate in your workflowgh-pages
In this tutorial, you learned about how to set up a GitHub Actions workflow to deploy your Flutter app across the Web and Android. For the next step, you can copy and modify the workflow to directly release the app to the app store or learn about other alternatives of GitHub Actions like CircleCI, GitLab CI, Jenkins, and more.
We hope you enjoyed this tutorial. Feel free to reach out to us if you have any queries. Thank you!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Flutter CI/CD using GitHub Actions"
where exactly is main.yml file is?