
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.
- Continuous Integration (CI) automates application builds, testing, and integrating code changes against the core code.
- 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
- https://resources.github.com/ci-cd/#:~:text=CI%2FCD automates your builds,continuous delivery or continuous deployment.
- https://www.matijanovosel.com/blog/deploying-flutter-applications-to-google-play-using-github-actions.
- https://stefma.medium.com/how-to-store-a-android-keystore-safely-on-github-actions-f0cef9413784.
- https://iqan.medium.com/continuously-releasing-flutter-app-to-play-store-using-github-actions-eca2f5f6e996.
github
flutter
cicd
actions