Android uses Github Actions to continuously integrate and automatically upload apk to the Dandelion App internal test distribution platform (including certificate password desensitization)

Table of contents

1 Introduction

2. Github Actions continuous integration

3. Upload apk to Dandelion

4. Gradle is used with the Secret of Github Actions

4.1 Set up Github Actions Secrets

4.2 Modify the signingConfigs of app.gradlede to read the Github Actions Secrets set

5. YAML, YML online editor (validator)


1 Introduction

The author of the article about continuous integration has written several cases of using Jenkins and Travis CI before:

Docker+Jenkins realizes continuous integration of Android (1)

Docker+Jenkins realizes continuous integration of Android (2)

Android's continuous integration practice based on Travis CI

Travis CI was originally divided into two versions, paid and free. It charges for private projects and provides free Credits for all free and open source (OSS) projects. In January 2019, Travis CI was acquired by Idera . Although the official guarantee at that time would continue to provide free services for open source projects, it has changed its business strategy now. Check out the latest official pricing scheme:

Limited credits, instantly unsavory?

• 10,000 credits

• Compatible with Assembla, Bitbucket, GitHub & GitLab

Comparing with Circle CI, it is a judgment call.

Needless to say, Jenkins, if you have money and willfulness, you can set up a server yourself.

Take a look at Github Actions’ offer of free meals:

Supported Runners and Hardware Resources
Hardware specifications for Windows and Linux virtual machines:

2-core CPU
7 GB RAM memory
14 GB SSD disk space
Hardware specifications for macOS virtual machines:

3-core CPU
14 GB RAM memory
14 GB SSD disk space

The above information comes from the official documentation of Github Actions:

About GitHub-hosted runners - GitHub Docs

High performance and free, just ask you whether Github Actions is good or not?

2. Github Actions continuous integration

There is no need to rewrite the repeated content. Here are two better-written articles for reference:

【 Continuous Integration 】Android uses Github Action to automatically package and release Fir.im internal test

Xue Xuan 's Github Actions User Guide and Android Continuous Integration Example

3. Upload apk to Dandelion

Why is it dandelion and not fir.im (betaqr.com) I won’t say much, first look at the full picture of the android.yml file:

name: Android CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: '11'
        distribution: 'temurin'
        cache: gradle

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew app:assembleRelease

    - name: Upload apk to pgyer.com
      run: find ${
   
   { github.workspace }}/app/build/outputs/apk/release/ -name "*.apk*" -type f -exec curl -F "file=@{}" -F "uKey=${
   
   { secrets.PGYER_UKEY }}" -F "_api_key=${
   
   { secrets.PGYER_API_KEY }}" https://upload.pgyer.com/apiv1/app/upload  \;

    - name: Upload apk to artifact
      uses: actions/upload-artifact@master
      if: always()
      with:
        name: lottery_app
        path: ${
   
   { github.workspace }}/app/build/outputs/apk/release/*.apk

The above is from the author's open source project:

https://github.com/xiangang/RecyclerViewLoopScrollAnimation/blob/main/.github/workflows/android.yml

I won’t explain too much about other parts of android.yml. Here I mainly introduce how to upload an apk with an uncertain file name. Most articles on the Internet describe this part of the apk name, but the actual project will generally give The apk name plus the channel name, version number, compilation time, etc., so it is obviously inappropriate to hardcode the apk name. The author tried to use *.apk to match at the beginning, but it kept prompting that the file could not be opened, because curl does not support the use of wildcards in this way.

what to do? *.apk cannot be matched, then find out the apk name first, and it can be completed by the find command.

Using the following command, you can easily query the apk file name. Of course, if there are multiple apks, you must modify the matching conditions to be more precise.

find ./ -name "*.apk*" -type f

If the file name is found, wouldn't it be a matter of course to use it with the curl command? Absolutely. The usage of the find command is not introduced here. If you are interested, you can use the man find command to view the help documentation.

Briefly explain the meaning of the following commands:

run: find ${
   
   { github.workspace }}/app/build/outputs/apk/release/ -name "*.apk*" -type f -exec curl -F "file=@{}" -F "uKey=${
   
   { secrets.PGYER_UKEY }}" -F "_api_key=${
   
   { secrets.PGYER_API_KEY }}" https://upload.pgyer.com/apiv1/app/upload  \;

find ${ { github.workspace }}/app/build/outputs/apk/release/ -name "*.apk*" -type f

Find the file named *.apk* in the ${ { github.workspace }}/app/build/outputs/apk/release/ directory. Among them, -type represents the specified file type, and f represents the common document type.

find xxx -exec xxxx {} \; : Represents executing a certain command on the found file, the command can use the set of files found by find;

-exec : represents the execution of the xxxx command

{} : Represents the variable saved to the collection of qualified files found by the find command

\; : The scope of the find command ends at

After understanding the above usage, it will be very simple to upload or send emails when encountering other build products in the future . New skills Get!

4. Gradle is used with the Secret of Github Actions

For general commercial projects, confidentiality measures are usually in place. This means that the password or other confidential content of the certificate used for signature when compiling the apk should not be directly submitted to the code repository in plain text. In a continuous integration environment, usually we can use environment variables to replace the exposed plaintext passwords. Of course, if it is an internal company server or other authorized integration environment, we can also write an encryption algorithm to encrypt the plaintext passwords, and then Compile and then decrypt.

This article only introduces the effect of desensitization by setting environment variables. This part of the content will also be explained in "Android Continuous Integration Practice Based on Travis CI" , and you can read it if you are interested.

4.1 Set up Github Actions Secrets

Enter the Github repository Settings page, click the Secrets menu on the left, click New repository secret in the upper right corner, fill in the corresponding Name Value on the redirected page, and click the add secret button. Note that Name will be automatically converted to uppercase.

It is also very simple to modify the Value corresponding to Name, just click Update in the list to modify. Note that only the Value can be modified, but the Name cannot be modified. If the Name is wrong, just Remove and add again. As shown, the author has set up the three Secrets required for signing.

4.2 Modify the signingConfigs of app.gradlede to read the Github Actions Secrets set

First look at how the original certificate password is read:

static def getAppReleaseTime() {
    return new Date().format("yyyyMMdd_HHmm", TimeZone.getTimeZone("Asia/Shanghai"))
}

// Remove private signing information from your project
// 创建一个名为keystorePropertiesFile的变量,并将其初始化为rootProject文件夹中的keystore.properties文件。
def keystorePropertiesFile = rootProject.file("keystore.properties")
// 初始化一个名为keystoreProperties的新Properties()对象
def keystoreProperties = new Properties()
// 将keystore.properties文件加载到keystoreProperties对象中
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))


android {
    compileSdk 31

    defaultConfig {
        applicationId "com.nxg.app"
        minSdk 21
        targetSdk 31
        versionCode 1
        versionName "1.0.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    signingConfigs {
        release {
            storeFile keystoreProperties['keyAlias'] != null ? file(keystoreProperties['storeFile']) : file('../demo.jks')
            storePassword keystoreProperties['storePassword'] != null ? keystoreProperties['storePassword'] : System.getenv("storePassword")
            keyAlias keystoreProperties['keyAlias'] != null ? keystoreProperties['keyAlias'] : System.getenv("keyAlias")
            keyPassword keystoreProperties['keyPassword'] != null ? keystoreProperties['keyPassword'] : System.getenv("keyPassword")
        }
    }

    buildTypes {
        debug {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            shrinkResources true //是否清理无用资源,依赖于minifyEnabled
            zipAlignEnabled true //是否启用zipAlign压缩
            signingConfig signingConfigs.release
            manifestPlaceholders = [RELEASE_TIME: getAppReleaseTime()]
            multiDexEnabled = true
            versionNameSuffix = ''
        }

        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            shrinkResources true //是否清理无用资源,依赖于minifyEnabled
            zipAlignEnabled true //是否启用zipAlign压缩
            signingConfig signingConfigs.release
            manifestPlaceholders = [RELEASE_TIME: getAppReleaseTime()]
            multiDexEnabled = true
            versionNameSuffix = ''
            // 自定义apk名称
            applicationVariants.all { variant ->
                variant.outputs.all { output ->
                    def fileName = "lucky_cube_app_release_${variant.versionName}_${appReleaseTime}.apk"
                    def outFile = output.outputFile
                    if (outFile != null && outFile.name.endsWith('.apk')) {
                        outputFileName = fileName
                    }
                }
            }
        }

    }
}

In the root directory of the project, there is a keystore.properties file:

# 线上版本持续集成使用环境变量,本地开发时手动填上对应的值
storePassword =
keyAlias = 
keyPassword =
storeFile= 

If it is compiled locally, the usual practice is to fill in the corresponding password and certificate file path in keystore.properties. If it is online compilation, use the environment variables set by the online compilation environment.

Earlier we set up the Secrets required for signature through Github Actions Serects, but how to use it? Of course, use environment variables, because Github Actions Serects are actually stored on the server provided by Github Actions, and we have no way to read them directly in gradle, so we can only indirectly read the corresponding Github Actions Serects by reading environment variables.

Go directly to the code:

static def getAppReleaseTime() {
    return new Date().format("yyyyMMdd_HHmm", TimeZone.getTimeZone("Asia/Shanghai"))
}

// Remove private signing information from your project
// 创建一个名为keystorePropertiesFile的变量,并将其初始化为rootProject文件夹中的keystore.properties文件。
def keystorePropertiesFile = rootProject.file("keystore.properties")
// 初始化一个名为keystoreProperties的新Properties()对象
def keystoreProperties = new Properties()
// 将keystore.properties文件加载到keystoreProperties对象中
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
println "keystoreProperties->$keystoreProperties"

def getStoreFile = {
    def storeFile = keystoreProperties['storeFile']
    if (storeFile == null || storeFile.isEmpty()) {
        storeFile = '../demo.jks'
    }
    return storeFile
}

def getStorePassword = {
    def storePassword = keystoreProperties['storePassword']
    if (storePassword == null || storePassword.isEmpty()) {
        storePassword = System.getenv("storePassword")
    }
    return storePassword
}

def getKeyAlias = {
    def keyAlias = keystoreProperties['keyAlias']
    if (keyAlias == null || keyAlias.isEmpty()) {
        keyAlias = System.getenv("keyAlias")
    }
    return keyAlias
}

def getKeyPassword = {
    def keyPassword = keystoreProperties['keyPassword']
    if (keyPassword == null || keyPassword.isEmpty()) {
        keyPassword = System.getenv("keyPassword")
    }
    return keyPassword
}

println "storePassword->${System.getenv("storePassword")}"
println "keyAlias->${System.getenv("keyAlias")}"
println "keyPassword->${System.getenv("keyPassword")}"

println "getStoreFile->${getStoreFile()}"
println "getStorePassword->${getStorePassword()}"
println "getKeyAlias->${getKeyAlias()}"
println "getKeyPassword->${getKeyPassword()}"

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.nxg.app"
        minSdk 21
        targetSdk 31
        versionCode 1
        versionName "1.0.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    signingConfigs {

        release {
            storeFile file(getStoreFile())
            storePassword getStorePassword()
            keyAlias getKeyAlias()
            keyPassword getKeyPassword()
        }
    }

    buildTypes {
        debug {
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            manifestPlaceholders = [RELEASE_TIME: getAppReleaseTime()]
            multiDexEnabled = true
        }

        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            shrinkResources true //是否清理无用资源,依赖于minifyEnabled
            zipAlignEnabled true //是否启用zipAlign压缩
            signingConfig signingConfigs.release
            manifestPlaceholders = [RELEASE_TIME: getAppReleaseTime()]
            multiDexEnabled = true
            versionNameSuffix = ''
            // 自定义apk名称
            applicationVariants.all { variant ->
                variant.outputs.all { output ->
                    def fileName = "lucky_cube_app_release_${variant.versionName}_${appReleaseTime}.apk"
                    def outFile = output.outputFile
                    if (outFile != null && outFile.name.endsWith('.apk')) {
                        outputFileName = fileName
                    }
                }
            }
        }

    }
}

You can see that app.gradleit is used System.getenv("ENV_NAME")to obtain the value of the environment variable named ENV_NAME in , and of course there is another way of writing it System.env.ENV_NAME.

Here we take storePassword as an example. When compiling, the value of storePassword defined in keystore.properties will be obtained first. If it is empty, the value defined in the environment variable will be obtained. Therefore, corresponding to the online compilation environment, we usually do not submit the value defined in keystore.properties The value is only used locally, and even keystore.properties are not submitted to the code warehouse.

def getStorePassword = {
    def storePassword = keystoreProperties['storePassword']
    if (storePassword == null || storePassword.isEmpty()) {
        storePassword = System.getenv("storePassword")
    }
    return storePassword
}

In this way, in signingConfigs, you can directly call the corresponding method to obtain the value in the environment variable.

signingConfigs {
    release {
        storeFile file(getStoreFile())
        storePassword getStorePassword()
        keyAlias getKeyAlias()
        keyPassword getKeyPassword()
    }
}

Is that the end? Of course not, don't forget that the environment variable and Github Actions Serects are not yet associated, so the environment variable read at this time has no value.

How are environment variables related to Github Actions Serects?

Students who are familiar with using the Linux system will immediately think of exportcommands. Through exportcommands, we can set temporary environment variables. The values ​​of the variables correspond to the values ​​set by Github Actions Serects .

The key android.yml code is as follows:

name: Android CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: '11'
        distribution: 'temurin'
        cache: gradle

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: |
        export storePassword=${
   
   { secrets.STOREPASSWORD }}
        export keyAlias=${
   
   { secrets.KEYALIAS }}
        export keyPassword=${
   
   { secrets.KEYPASSWORD }}
        ./gradlew app:assembleRelease


    - name: Upload apk to pgyer.com
      run: find ${
   
   { github.workspace }}/app/build/outputs/apk/release/ -name "*.apk*" -type f -exec curl -F "file=@{}" -F "uKey=${
   
   { secrets.PGYER_UKEY }}" -F "_api_key=${
   
   { secrets.PGYER_API_KEY }}" https://upload.pgyer.com/apiv1/app/upload  \;

    - name: Upload apk to artifact
      uses: actions/upload-artifact@master
      if: always()
      with:
        name: lottery_app
        path: ${
   
   { github.workspace }}/app/build/outputs/apk/release/*.apk

It can be seen that in Github Actions, ${ { secrets.STOREPASSWORD }}the set secret is obtained in this form, and then only need to cooperate with exportthe command to read the temporary environment variables set in the system in the current running compilation environment. Why is it temporary? Because the compilation At the end, this temporary environment variable can no longer be read, and it can be compiled and used at any time, and the system will not be affected when it is used up.

System.getenv("ENV_NAME")We use the assembleRelease task to compile the apk, so setting the environment variable must ensure that the Github Actions Serects set in the environment variable can be read when compiling the apk before executing the assembleRelease task .

So far, we have desensitized the certificate password through Github Actions Serects and environment variables, which ensures the security of the online compilation environment to a certain extent. New skills Get!

5. YAML, YML online editor (validator)

YAML, YML online editor (formatting verification)-BeJSON.com

As an extra bonus, because of the writing of android.yml, we often accidentally cause script compilation problems. At this time, we can verify it through this online website, which is very convenient for troubleshooting. Know that your time is very valuable! right.

Written at the end, first of all, thank you very much for your patience in reading the entire article. It is not easy to insist on writing original and practical articles. If this article happens to be helpful to you, you are welcome to like and comment on the article. Your encouragement is the author's insistence Unrelenting drive. If there are any mistakes in the article, please correct me, thank you again.

Guess you like

Origin blog.csdn.net/xiangang12202/article/details/122594984