cover-img

Publish Your Flutter App to the Google Play Store Automatically

21 September, 2023

0

0

0

Contributors

CI/CD (Continuous Integration/Continuous Delivery) refers to automating the process of releasing programs to users, which simplifies the lengthy manual stages and reduces the possibility of errors.

  1. Continuous Integration (CI) automates application builds, testing, and integrating code changes against the core code.
  2. Continuous distribution (CD) handles the distribution of code changes to the production environment, whereas Continuous Deployment (CD) immediately distributes programs to users.

The application release process is intended to become more efficient by introducing CI/CD, without sacrificing the quality and integrity of our product.

I chose GitHub Actions for this study because of its widespread support and usage. I'll go over the steps involved in integrating GitHub Actions with Google Play services for the Android platform.

This setup process will be rather hard at first, but it will considerably facilitate the application delivery process to clients.

Table of Contents

Signing APK

Workflow Preparation

Version Number

Build APK

Release Preparation

Google Play Release

Implementation Strategy

Conclusion

Sign APK


Assume you already have a release-ready app on GitHub. The first step in publishing any mobile app is to sign the APK. I'll construct a keystore for the upload procedure; a keystore will indicate that your program is genuine.

Run the following command (for Windows):

keytool -genkey -v -keystore <path>\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload

path specifies the directory in which the keystore will be stored. If you leave this option blank, the keystore will be saved in your project's root folder. You will be prompted to fill out numerous sections, including storePassword and keyPassword; remember these two passwords for the following step. After running the following command, a new file with the name upload-keystore.jks will be created in the root folder of your project; move this file to the android > app folder.

Create a new file called key.properties in the android folder. Put this file in .gitignore to prevent Git from indexing it. Fill out the file as follows:

storePassword=#{STORE_PASSWORD}#
keyPassword=#{KEY_PASSWORD}#
keyAlias=upload
storeFile=./upload-keystore.jks

Then navigate to Settings > Secrets and variables > Actions in your GitHub repository. Create two secrets, STORE_PASSWORD and KEY_PASSWORD, by pressing the New repository secrets button. Fill them with the passwords you generated while creating upload-keystore.jks.

To use the keystore that you built for the APK signing process, edit the android/app/build.gradle file and add the following code above the android code block. This extra code will read the key.properties file you produced earlier.

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
...
}

Then add the following code before the buildTypes code block (still in the same file). This code will read the value in the key.properties file.

signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}

buildTypes {
...
}

Then, modify the buildTypes code block's contents to the following code. This section is useful for signing our application when performing a release build.

buildTypes {
release {
signingConfig signingConfigs.release
}
}

You must also alter the contents of the pubspec.yaml file to the following to enable automated version numbering.

name: app_name
description: App description
publish_to: "none"
version: 99.99.99+99

Take note that the version variable's values have been updated to 99.99.99+99. You've made the number a placeholder, which allows us to easily adjust the version number during the distribution process.

Workflow Preparation

In the .github/workflows/file.yaml directory, create a YAML file with an unrestricted name. Then fill out the paperwork as follows.

name: Flutter Workflow
on:
push:
branches: [main]

Version Number

The first part of the workflow is to generate version numbers from git tag numbers. To gain access to the information in the repository, you must first add a GitHub token to the secrets. Using this link, you can generate a GitHub token. Keep in mind that the secret name cannot begin with the phrase GITHUB; for example, save your GitHub token as TOKEN_GITHUB.

We'll proceed to the version number generation process if you've added the GitHub token to secrets. Substitute the following line for the existing one.

jobs:
version:
name: Version Number
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Retrieve Tags and Branch History
run: |
git config remote.origin.url @github.com/${{github.repository">https://x-access-token:${{secrets.TOKEN_GITHUB}}@github.com/${{github.repository}}
git fetch --prune --depth=10000
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0.9.7
with:
versionSpec: "5.x"
- name: Use GitVersion
id: gitversion
uses: gittools/actions/gitversion/execute@v0.9.7
- name: Creating 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

This stage produces a version.txt file with the number of tags that will be used as the version number of our application later on.

The next step is to configure the action to build an APK for upload to the Google Play Store. You must save upload-keystore.jks, .env (if any), and key.properties to the secrets repository before going to the APK build stage.

First, we must encrypt the file with gpg and save its contents to the secrets repository. Furthermore, we must construct secrets to store the password for each file, which is utilized when the file is encrypted.

With the following command, we may encrypt.

gpg -c --armor file_name

The command creates a file with the extension .asc, open the file and copy the contents before pasting them into the secrets repository. Repeat for the remaining files. The completed example is shown below.

When you're finished, add the build block underneath the version block.

build:
name: Build APK and Creating Release
needs: [version]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: |
echo "${{secrets.RELEASE_KEYSTORE}}" > upload-keystore.jks.asc
echo "${{secrets.RELEASE_ENV}}" > .env.asc
echo "${{secrets.RELEASE_PROP}}" > key.properties.asc
gpg -d --passphrase "${{secrets.RELEASE_KEYSTORE_PASSWORD}}" --batch upload-keystore.jks.asc > android/app/upload-keystore.jks
gpg -d --passphrase "${{secrets.RELEASE_ENV_PASSWORD}}" --batch .env.asc > .env
gpg -d --passphrase "${{secrets.RELEASE_PROP_PASSWORD}}" --batch key.properties.asc > android/key.properties
- 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: Update Keystore Password in Gradle Properties
run: sed -i 's/#{STORE_PASSWORD}#/${{secrets.STORE_PASSWORD}}/g' android/key.properties
- name: Update Keystore Key Password in Gradle Properties
run: sed -i 's/#{KEY_PASSWORD}#/${{secrets.KEY_PASSWORD}}/g' android/key.properties
- uses: actions/setup-java@v1
with:
java-version: "12.x"
- uses: subosito/flutter-action@v1
with:
channel: "stable"
- run: flutter clean
- run: flutter pub get
- run: flutter build apk --release --split-per-abi --obfuscate --split-debug-info=symbols
- run: flutter build appbundle --release --obfuscate --split-debug-info=symbols
- name: Create a Release in GitHub
uses: ncipollo/release-action@v1
with:
artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/app-release.aab"
token: ${{secrets.TOKEN_GITHUB}}
tag: ${{steps.version.outputs.content}}
commit: ${{github.sha}}
- name: Upload App Bundle
uses: actions/upload-artifact@v2
with:
name: appbundle
path: build/app/outputs/bundle/release/app-release.aab

As you can see, we echo the contents of secrets to create an .asc file with the same name. The .asc file is then decrypted, restoring it to its original state. This manner, we can use the files without needing to commit them to git.

In this build block, we also change the placeholder for the version number with the version number obtained in the previous step. The values of the STORE_PASSWORD and KEY_PASSWORD placeholders are likewise replaced with the credentials saved in the secrets repository.

For security considerations, we also use the --obfuscate argument while building the APK and AAB. The disadvantage at this point is that we cannot save the symbols file to the obfuscated APK or AAB because we did not take any measures to save the symbols file.

Release Preparation

Congratulations on making it this far. We set the version numbering and APK build with obfuscate in the previous stage. We also kept the configuration files and keystore in the secrets repository for safekeeping. All of the APK build preparations have been completed. We will prepare the path between Google Cloud Platform and Google Play Developer at this time so that the release process runs smoothly.

Create a project and service account on Google Cloud to begin the preparation. A service account can be created via the IAM & Admin > Service Accounts menu. After creating the service account, open the Manage Keys menu by clicking the option button on the right. Make a new key in JSON format. Copy the JSON file's contents and save it in the secrets repository, for example, with the name PLAYSTORE_ACCOUNT_KEY, as seen below.

Connect the project to the Google Cloud project and enable the following APIs in both Google Cloud Platform and Google Play Console: Google Play Android Developer API, Google Play Developer Reporting API, and Google Play Games Services Publishing API.

For further security, go to Google Play Console's User and permissions menu, choose the email service account you created, and then select the app project you're working on under the App permissions area.

In this option, we can also set Account permissions; here is an example of the permissions I use.

Once all of the preparations are completed, we will be ready to move on to the next stage, which is the Google Play Store release.

Google Play Release

Note: Before you can perform this workflow, you must manually upload the APK/AAB file to Google Play Console.

The final step in this process is to upload the AAB to the Play Store using your favorite track. So far, two tracks are available: production and internal. To proceed, place the release block beneath the other block.

release:
name: Release app to production track
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Get appbundle from artifacts
uses: actions/download-artifact@v2
with:
name: appbundle
- name: Release app to production track
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{secrets.PLAYSTORE_ACCOUNT_KEY}}
packageName: com.app.package_name
releaseFiles: app-release.aab
track: production
status: completed

Those are the stages of Play Store CI/CD using GitHub Actions. The following is the content of the YAML script that we have written as a whole.

name: Flutter Stream

on:
push:
branches: [main]

jobs:
version:
name: Create Version Number
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Feth Histories for All Tags and Branches
run: |
git config remote.origin.url @github.com/${{github.repository">https://x-access-token:${{secrets.TOKEN_GITHUB}}@github.com/${{github.repository}}
git fetch --prune --depth=10000
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0.9.7
with:
versionSpec: "5.x"
- name: Use GitVersion
id: gitversion
uses: gittools/actions/gitversion/execute@v0.9.7
- 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: Build APK and Create Release
needs: [version]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: |
echo "${{secrets.RELEASE_KEYSTORE}}" > upload-keystore.jks.asc
echo "${{secrets.RELEASE_ENV}}" > .env.asc
echo "${{secrets.RELEASE_PROP}}" > key.properties.asc
gpg -d --passphrase "${{secrets.RELEASE_KEYSTORE_PASSWORD}}" --batch upload-keystore.jks.asc > android/app/upload-keystore.jks
gpg -d --passphrase "${{secrets.RELEASE_ENV_PASSWORD}}" --batch .env.asc > .env
gpg -d --passphrase "${{secrets.RELEASE_PROP_PASSWORD}}" --batch key.properties.asc > android/key.properties
- 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: Update Keystore Password in Gradle Properties
run: sed -i 's/#{STORE_PASSWORD}#/${{secrets.STORE_PASSWORD}}/g' android/key.properties
- name: Update Keystore Key Password in Gradle Properties
run: sed -i 's/#{KEY_PASSWORD}#/${{secrets.KEY_PASSWORD}}/g' android/key.properties
- uses: actions/setup-java@v1
with:
java-version: "12.x"
- uses: subosito/flutter-action@v1
with:
channel: "stable"
- run: flutter clean
- run: flutter pub get
- run: flutter build apk --release --split-per-abi --obfuscate --split-debug-info=symbols
- run: flutter build appbundle --release --obfuscate --split-debug-info=symbols
- name: Create a Release in GitHub
uses: ncipollo/release-action@v1
with:
artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/app-release.aab"
token: ${{secrets.TOKEN_GITHUB}}
tag: ${{steps.version.outputs.content}}
commit: ${{github.sha}}
- name: Upload App Bundle
uses: actions/upload-artifact@v2
with:
name: appbundle
path: build/app/outputs/bundle/release/app-release.aab
release:
name: Release App to Production Track
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Get Appbundle from Artifacts
uses: actions/download-artifact@v2
with:
name: appbundle
- name: Release App to Production Track
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{secrets.PLAYSTORE_ACCOUNT_KEY}}
packageName: com.package.name
releaseFiles: app-release.aab
track: production
status: completed

Conclusion

Companies will save labor for deployment if the CI/CD pipeline is implemented as described above. Google Play has an internal track that may be utilized for SIT or UAT, thus APK distribution will be more structured.

References

  1. https://resources.github.com/ci-cd/#:~:text=CI%2FCD automates your builds,continuous delivery or continuous deployment.
  2. https://www.matijanovosel.com/blog/deploying-flutter-applications-to-google-play-using-github-actions.
  3. https://stefma.medium.com/how-to-store-a-android-keystore-safely-on-github-actions-f0cef9413784.
  4. https://iqan.medium.com/continuously-releasing-flutter-app-to-play-store-using-github-actions-eca2f5f6e996.

github

flutter

cicd

actions

0

0

0

github

flutter

cicd

actions

Fredi
Software Engineer at KB Bank

More Articles

Showwcase is a professional tech network with over 0 users from over 150 countries. We assist tech professionals in showcasing their unique skills through dedicated profiles and connect them with top global companies for career opportunities.

© Copyright 2025. Showcase Creators Inc. All rights reserved.