DEV Community

Cover image for Effortlessly Deploy Your Flutter App on Google Play Store
Fredianto
Fredianto

Posted on

Effortlessly Deploy Your Flutter App on Google Play Store

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Image description

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 {
...
}
Enter fullscreen mode Exit fullscreen mode

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 {
...
}

Enter fullscreen mode Exit fullscreen mode

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
 }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

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.

Image description

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

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

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)