CI/CD, which stands for Continuous Integration/Continuous Delivery, involves automating the app release process. This streamlines the traditionally manual and error-prone steps, making it more efficient.
Continuous Integration (CI) automatically handles tasks like building applications, testing, and merging code changes with the main codebase. Continuous Delivery (CD) manages the deployment of these code changes to the production environment. Continuous Deployment (CD) takes it a step further by automatically releasing applications to end-users.
By incorporating CI/CD, we aim to enhance the efficiency of the application release process while maintaining its quality and integrity.
In this study, I opted for GitHub Actions due to its widespread usage and extensive support. I will outline the steps involved in integrating GitHub Actions with Google Play services for Android.
Though the initial setup may seem a bit complex, it will significantly streamline the application delivery process to our customers.
Table of Contents
- APK Signing
- Workflow Setup
- Versioning
- APK Building
- Preparing for Release
- Google Play Deployment
- Implementation Plan
- Conclusion
Signing the APK
Assuming you have a finalized app ready for release stored on GitHub, the initial step in the release process is to sign the APK. This involves creating a keystore for the upload process, which serves as proof of your app's authenticity.
For Windows users, execute the following command:
keytool -genkey -v -keystore <path>\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
In the designated <path>
, you'll store the keystore file. Leaving this section blank will result in the keystore being stored in your project's root folder. When prompted, provide and remember the storePassword and keyPassword for subsequent steps. Running the above command will generate a new file named upload-keystore.jks
in your project's root directory. Move this file to the android > app
folder.
Next, within the android directory, generate a new file named key.properties
. Make sure to add this file to your .gitignore
to prevent it from being tracked by Git. Populate the file as follows:
storePassword=#{STORE_PASSWORD}#
keyPassword=#{KEY_PASSWORD}#
keyAlias=upload
storeFile=./upload-keystore.jks
Afterwards, navigate to your GitHub repository and access Settings > Secrets and variables > Actions
. Click on the "New repository secrets" button and create two secrets: one named STORE_PASSWORD and the other KEY_PASSWORD. Populate them with the passwords you established during the creation of upload-keystore.jks
.
To enable Gradle to utilize the keystore for the APK signing process, open the android/app/build.gradle
file and insert the following code above the android code block. This additional code instructs Gradle to read the key.properties
file that you generated earlier.
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
...
}
Certainly, please provide the specific code that you'd like to add before the buildTypes code block in the android/app/build.gradle file.
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
...
}
Afterwards, update the contents of the buildTypes
code block with the following code. This modification ensures that our application is signed during a release build.
buildTypes {
release {
signingConfig signingConfigs.release
}
}
In order for version numbering to be done automatically, you also need to change the contents of the pubspec.yaml
file to be as follows.
name: app_name
description: App description
publish_to: "none"
version: 99.99.99+99
Please be aware that we've adjusted the version variable to 99.99.99+99
. We've set this as a placeholder, allowing for convenient modification of the version number during the deployment process.
Preparing the Workflow
Generate a YAML file with a name of your choice in the .github/workflows/
directory. Subsequently, populate the file as follows:
name: Flutter Workflow
on:
push:
branches: [main]
Versioning
The initial step in this workflow involves generating version numbers based on git tag numbers. To access repository information, it's necessary to include a GitHub token in the secrets. You can create a GitHub token by following this link. Please keep in mind that secret names cannot start with the term GITHUB
. For instance, you might save your GitHub token as TOKEN_GITHUB
.
Once you've added the GitHub token to the secrets, we can proceed with the version number generation process. Append the following line below 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
The result of this stage will be a version.txt
file, which will hold the tag count that will serve as the version number for our application in subsequent steps.
Moving forward, we'll configure the action to generate an APK for uploading to the Google Play Store. Prior to proceeding with the APK building stage, make sure to save upload-keystore.jks
, and if applicable, .env
and key.properties
, in the secrets repository.
First, we'll encrypt the file using gpg and store its contents securely in the secrets repository. Additionally, we'll create secrets to securely store the passwords associated with each file used during encryption.
You can encrypt the file with the following command:
gpg -c --armor file_name
The command will generate a file with the extension .asc. Open this file, copy its contents, and then paste them into the secrets repository. Repeat this process for the other files as well. Below is a completed example for your reference.
After you've completed the encryption and storage process, include the following build block beneath 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 depicted, we echo the secrets to create a .asc
file with its corresponding name. We then decrypt the .asc
file, reverting it to its original state. This enables us to safely utilize the files without the need to include them in the Git repository.
Within this build
block, we also substitute the version number placeholder with the one we generated in the previous step. Additionally, we replace the placeholders for STORE_PASSWORD
and KEY_PASSWORD
with the credentials stored in the secrets repository.
Furthermore, we execute the --obfuscate
parameter to build both the APK and AAB with enhanced security measures. However, it's important to note that at this stage, we are unable to include the symbols file in the obfuscated APK or AAB, as we haven't taken any specific steps to preserve it.
Release Preparation
Congratulations on reaching this milestone! In the previous stage, we successfully established version numbering and conducted an APK build with obfuscation. Furthermore, we securely stored the configuration files and keystore in the secrets repository. With these preparations completed, we're now ready to establish the connection between the Google Cloud Platform and Google Play Developer, ensuring a seamless release process.
To start the preparation, create a project and service account on Google Cloud. You can create a service account through the IAM & Admin > Service Accounts
menu. After the service account is created, click the option button on the right and open the Manage Keys menu. Create a new key and use JSON format. Copy the contents of the JSON file and save it in the secrets repository, for example with the name PLAYSTORE_ACCOUNT_KEY
as in the example below.
Next, establish a connection between the project in Google Cloud and the corresponding project in the Google Play Console. Ensure that you enable the following APIs in both the Google Cloud Platform and Google Play Console:
- Google Play Android Developer API
- Google Play Developer Reporting API
- Google Play Games Services Publishing API.
For added security, navigate to the User and Permissions
menu in the Google Play Console. Then, choose the email associated with the service account you created. In the App permissions
section, select the specific app project you're working on. This step helps ensure proper access and permissions for the service account.
Additionally, in this menu, you have the option to configure Account permissions. Below is an example of the permissions that I typically utilize.
Google Play Release
Notes: Before running this entire workflow, it's imperative that you manually upload the APK/AAB file to the Google Play Console.
In this final stage of the workflow, you'll be uploading the AAB file to the Play Store through your chosen track. Currently, there are two tracks available: production and internal. To proceed, include the release
block below the existing blocks.
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
By implementing the CI/CD pipeline outlined above, companies can significantly reduce the manual effort required for app deployment. Additionally, the use of Google Play's internal track provides a structured approach to APK distribution, facilitating testing phases such as System Integration Testing (SIT) or User Acceptance Testing (UAT). This streamlined process ultimately leads to more efficient and reliable app releases.
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.
Top comments (0)