现在在正规成体系的公司项目中,我们都会搭建和配置 CI/CD 环境来完成工程的构建,自动化测试,artifact 发布等任务。在 Github 上的开源项目也不例外,给代码 PR 或者 push 配置 CI/CD 可以自动验证期望 merge 的代码是否能通过构建及测试,也可以配置自动构建发布的工作流在远端执行这些任务并向对该项目感兴趣的人公开。

我最近在给 MMKV-Kotlin 及 SQLlin 两个 Kotlin Multiplatform 开源项目配置 actions 的时候遇到了一些坑,因此在本文中记录一下 Kotlin Multiplatform 工程通常会遇到的一些问题。至于如何配置基本 Github Actions 的教程网上已经有很多人写过了,因此本文不再详细介绍具体关键字如何使用。

基本配置

我们在项目工程目录下创建 .github/workflow 目录,然后将需要的 action(yml 文件)放在该目录下即可。通常情况下我认为一个 Kotlin Multiplatform项目至少需要两个 workflow;第一个是 build.yml, 在有代码 merge 或 push 的情况下触发,用于检查欲合并的代码是否能通过构建及单元测试;另一个是 publish.yml,在希望构建及发布新版的时候项目管理者手动触发,可自动完成构建、测试以及发布到 Maven Central。因此,build.yml 的 ‘on’ 语句如下:

on:  
  push:    
    branches:     
      - '*'  
  pull_request:    
    branches:          
      - '*'

publish.yml 的 ‘on’ 语句:

on:  
  workflow_dispatch:

环境初始化

一个 Kotlin Multiplatform 工程通常会打出以下平台的产物:JVM、Android、JS(Node, Browser)、Native(Android NDK, iOS, macOS, tvOS, watchOS, Linux, Windows, WASM)。Github 提供的服务器环境有 macOS、Windows、Ubuntu 三种,非原生平台产物(JVM, Android, JS)可以在任意系统的机器上构建,但是 Native 平台的情况就更为复杂。例如,iOS、macOS 等 Apple 平台的 target 构建依赖 Xcode,而 Xcode 又只能运行在 macOS 上,因此如果你的 target 中包含 Apple 平台,则必须在 macOS 上构建。此外 Windows 平台产物依赖 MinGW,因此只能在 Windows 上构建;Linux 的 linuxMips32 及 linuxMipsel32 targets 只能在 Linux 上构建;64 位的 Android NDK 产物只能在 Linux 或 macOS 上构建。我将 Kotlin/Native 的产物以及构建的 host 的关系的关系总结为下表:

Target\Host

macOS

Windows

Ubuntu

Android NDK


only 32-bit


iOS




macOS




watchOS




tvOS




Linux

except linuxMips32 and linuxMipsel32

except linuxMips32 and linuxMipsel32


Windows




WASM




MMKV-Kotlin 这个项目支持的平台分别有:Android、iOS、macOS。因此我们只需要配置一台 macOS 的环境即可完成整个流程的构建。

但是 SQLlin 的情况更为复杂。它支持的平台有:Android、iOS、macOS、tvOS、watchOS、Linux (仅 x64)、Windows。仅配置单一环境就无法完全覆盖期望支持的平台,因此我需要同时配置两个 job 并行执行。第一个 job 运行于 macOS,构建除了 Windows 外的所有 artifacts;第二个 job 运行于 Windows,构建 Windows artifacts:核心 yml 代码如下:

jobs:
  build-on-macos:    
    runs-on: macos-latest    
    timeout-minutes: 60        
      
    steps:      
    #......      
      - name: Build sqllin-driver        
        run: ./gradlew :sqllin-driver:assemble
      
      - name: Run sqllin-driver Test        
        run: ./test_driver_macos.sh
        
      #......          
      
  build-on-windows:    
    runs-on: windows-latest    
    timeout-minutes: 60        
      
    steps:    
    #......      
      - name: Build sqllin-driver        
        run: ./gradlew :sqllin-driver:mingwX64MainKlibrary && ./gradlew :sqllin-driver:mingwX86MainKlibrary
      
      - name: Run sqllin-driver Test
        run: ./test_driver_windows.sh             
      #......

自动化测试

我们可以在 workflow 中配置并自动执行测试,通常来说 Kotlin Multiplatform 项目的单元测试可以分为:JVM、Android、JS、Native 几个大类的独立测试。我们将测试的主要代码写在 common test source set 中,然后在各平台相关的 source set 中编写一些平台相关的代码,然后即可将同一套代码在不同的平台进行自动化测试。

不过,相应平台的单元测试只能运行在对应的 host 上。例如 macOS 的 host 上只能直接运行 macosX64 的 单元测试。对于 Android,有些 Android 的单元测试必须运行插桩测试(instrumented test),即必须要有 Android 环境,但目前我还未找到在 Github Actions 中开启 Android 模拟器的方法,因此目前也无法运行。

我们了解了 Kotlin Multiplatform 及 Github Actions 的一些限制,现在可以制定我们的测试策略。即在 build-on-macos 结束后运行一次 macosX64 test,在另一个并行的 job——build-on-windows 结束 Windows artifacts 的 build 后执行一次 mingwX64 test(代码见_环境初始化_小节的示例)。

当然这样的测试策略并不能覆盖所有的目标系统以及所有的场景,因此如果 merge 的代码中有修改未覆盖测试的平台 source set 中的代码,又或者在 publish 之前,项目的管理者最好在本地运行相应的单元测试。

Secrets

Kotlin Multiplatform 项目的 publish 流程有两个关键步骤——artifacts 签名以及发布到 Maven Central。这两个步骤都有一些不能公开的信息,例如签名的GPG Key 私钥,以及发布到 Maven Central 所需的 OSSRH 账号密码。在本地我们可以把这些信息维护到全局的 gradle.properties 文件,而在 Github Actions 中我们可以把这些信息配置到 Github Secrets。

我们找到项目的 Github 主页,依次点击 Settings -> Secrets -> Actions -> New reposirory secret 即可创建一条 Secret,Name 表示它的 key,Secret 表示 value,将明文填写进去即可。比如说 OSSRH 账号和密码我给它们的 key 分别起名为 NEXUS_USERNAMENEXUS_PASSWORD。在将其维护到 Github Secrets 之后我们可以在 build.gradle.kts 脚本中这样获取它们:

val NEXUS_USERNAME: String by project
val NEXUS_PASSWORD: String by project

配置 GPG Key 相对来说更为复杂,去网上查找教程通常会教我们下载安装 GPG Suite,然后生成自己的密钥,再将密钥发布到服务器,最后在 gradle.properties 中配置密钥 id,密码,以及 gpg 文件的路径。在 build.gradle.kts 中导入 signing 插件再编写如下脚本即可完成签名:

signing {    
    sign(publishing.publications)
}

这对本地签名是有效的,但是在 Github Actions 中,我们没法把 gpg 文件上传到 host,因此必须使用密钥内存签名。signing 还推荐我们使用 GPG 子密钥来做签名,这样更安全,我们在生成子密钥后修改 build.gradle.kts:

signing {    
    val SIGNING_KEY_ID: String by project    
    val SIGNING_KEY: String by project    
    val SIGNING_PASSWORD: String by project    
    useInMemoryPgpKeys(SIGNING_KEY_ID, SIGNING_KEY, SIGNING_PASSWORD)    
    sign(publishing.publications)
}

如上所示,区别在于我们需要把 GPG 私钥维护在 SIGNING_KEY 这个字段中,在 Github Actions 中也就是把它放到 Secrets 里。不过这里有一个坑,网上通常会教我们用这条命令来导出私钥:

gpg --export-secret-keys --armor <key id>

这样导出的私钥会有很多换行符,直接把它填入 Secrets 会报“GPG key read error”错误。这里卡了我很长时间,最后感谢霍佬找到一篇文档,文档中教我们使用以下命令导出私钥:

gpg2 --export-secret-keys --armor <key id> <path to secring.gpg> | grep -v '--' | grep -v '^=.' | tr -d '\n'

多加了几个参数,换行符会被消除,亲测可行。

总结

CI/CD 真香,没配置的赶紧配置。具体的代码可以参考我们的两个开源项目:

  • MMKV-Kotlin
  • SQLlin

大家新年快乐,新年立个 compose-jb 的 flag~

作者:Kotlin上海用户组