Effortlessly Deploy Your Flutter App on Google Play Store
Tags: flutter, cicd, github
Published: 2023-09-11 03:26:19

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
> 1. APK Signing
> 2. Workflow Setup
> 3. Versioning
> 4. APK Building
> 5. Preparing for Release
> 6. Google Play Deployment
> 7. Implementation Plan
> 8. 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`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qfpsgndr720cu08x971m.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6q0vnysso09ycxbfl5xy.png)

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](https://github.com/settings/tokens). 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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jh3q7z5vr19y94ayzgua.png)

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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pk2don406arpsfdvccu6.png)

To start the preparation, create a project and service account on [Google Cloud](https://console.cloud.google.com/). 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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e8wyme6fifp70matbl41.png)

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:

1. Google Play Android Developer API
2. Google Play Developer Reporting API
3. Google Play Games Services Publishing API.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xisvnotn6q5ahgzy6ke6.png)

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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/urm4i1kxyufa2y32b63x.png)

Additionally, in this menu, you have the option to configure Account permissions. Below is an example of the permissions that I typically utilize.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n3dn3iw0sgkogjz6c5mt.png)

**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**
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.