背景

关于 CI/CD ,在2023年的今天,基本所有技术团队或多或少都会使用,其很大程度上减轻了我们的冗余重复工作,从而简化我们的工作流程。

不过对于大多数客户端工程师而言,其实 CI 这个词还是比较陌生。当然并不是说,CI/CD 有什么高大上或者门槛很高,因为毕竟不是所有人会去维护开源库或者搞基建,或者说少有场景去接触到。但对于一个 工程师 而言,这严格意义上其实属于 基本技能点 ,或者说在现在这个时代,这种小技能应该没有太多边界之分。

相应的,如果使用 Github Action,这个难度就更低了,其相比传统的 Jenkins ,容易上手了更多,简化了 环境配置 等等。并且,在这个过程中,我们也将逐步接触到一些 cmdpython 等其他工具使用方式或者语法,从而探索出更多可玩性。

写在开始

注意: 本篇不会讲 Github Action 基础语法,这些官网有更详细的文档,没有意义去做二次搬运🙅🏻♂️。

本篇更多是围绕要解决的实际问题进行分析,并在中间对关键语法进行解释,从而便于更快的使用 Github CI

个人建议对于这种工具类的技能,不需要学的很详细,只需要了解基本原则:自己要解决什么问题,即可。

故此,学完本篇,你将学会 Github CI 的基础使用,以及一些常见的实用操作,如:

  • 自动化打包以及上传;
  • 自动化版本号与code;
  • 自动化发布release;
  • 逐步解开传统思维陷阱,体会 CI 在日常开发中的妙用;

好了,让我们开始吧 ! 🏃🏻

什么是 CI/CD?

CI/CD 是指持续集成(Continuous Integration)和持续部署/交付(Continuous Deployment/Delivery)的缩写。

  • 持续集成(CI)是一种软件开发实践,指的是将代码集成到主干分支中并进行构建和测试的过程,以便尽早发现和解决问题。CI 工具可以自动执行这个过程,例如 1、Travis CI、CircleCI 等。每次提交代码时,CI 工具会自动构建和运行测试,并给出构建和测试结果的反馈。
  • 持续部署/交付(CD)是指自动化地将代码部署到生产环境或发布到应用商店的过程。持续部署/交付可以让开发团队更加快速和可靠地将新功能交付给用户。CD 工具可以自动化执行部署和发布过程,例如 AnsibleKubernetesDocker 等。在持续部署/交付的过程中,需要进行自动化测试、版本控制、持续监控等操作,以确保代码质量和应用稳定性。

CI/CD 的优点包括加速软件开发、提高代码质量、降低风险、提高工作效率 等。从而可以让开发团队更加专注于代码编写,而不必花费大量时间进行 手动构建测试部署 等重复性工作。

什么是 Github CI?

因为本篇,主要是讲 Github CI 的使用,故还是要简单说一下 Github CI 的简介及功能。

GitHub CI(GitHub Actions)是 GitHub 提供的一项自动化工具,用于 构建测试部署 GitHub 上托管的代码仓库。GitHub CI 提供了一种定义自动化工作流程的方式,可以根据代码仓库的变化自动触发工作流程。一组工作流程可以包括多个步骤,例如编译代码、运行测试、构建镜像、部署应用等。其优点包括与 GitHub 平台紧密集成、易于配置、支持多种语言和环境、提供丰富的集成能力等。它可以帮助开发团队自动化构建和测试过程,提高代码质量和开发效率。

具体运行示例中如下图所示:

写给Android工程师的 Github CI 快速指北_CI/CD

CI 可以做什么?

几乎可以简化任何我们能在本地做的所有 人工 操作,甚至自动编码。

为了更好的便于理解,我们切换到 Android工程师 视角,使用一个示例来说明。

比如我们现在有个 下厨房 Android工程,如果在没有 CI 时,我们最基础的流程通常如下:

  • 开发: 本地开发、调试、push;
  • 测试:本地打包、发给测试同学;
  • 打包: 改版本号、打tag、本地打包、发给运营同学;

上面的流程看上去似乎没有什么问题,对于本地开发而言,浓缩下来也就上面三步,这也是小团队常见流程。

但仔细观察的话,其中很多步骤都是冗余,比如每次 本地打包改版本号打tag发给指定人 ,这些步骤都显得很机械(或者比较呆),特别是如果是在 bug fix 阶段,更是繁琐。

那如果借助 CI,我们应该如何优化上述步骤呢?

  • 当我们每次提一个 PR 或者 push 时,就自动去打测试包,并执行一些我们自定义的一些 check,如 代码检查包大小检查自动化测试 等等,并将最后打出的 apk 上传到 fir 或者其他地方。并借助 webhook ,从而实现 飞书钉钉 等方式通知相关同学;
  • 发布新的 release版本 前,改版本号时,也可以支持自动化版本号。比如可以利用 git tag 作为版本号,commit 记录作为code,并与 CI 联动,实现动态指定;
  • 而当我们每次发布 release 版本时,通常情况下,我们都会打一个 tag ,然后 push 。所以我们也可以利用 CI ,发现有新的 tag 时,则触发工作流执行,从而去自动发布一个 release 版本,并且执行一遍打包,将相关产物上传到我们指定的位置;并根据项目的规则总结出相应的 release 更改信息,并更新描述,最后再将版本信息通知到相关运营同学;
  • 在应用包上传的过程中,人工必不可少会出现传错包的情况,此时也可以借助 CI 实现打包完成后自动上传应用商店,比如 华为、小米、Gogole 目前就支持 api 上传

如果上述步骤,你们团队都已经实现了,那就证明对于 Android工程 而言,你们的基础 CI/CD 设施已经做的很不错了,不妨为自己点点赞。

换个角度而言,CI 几乎可以完成大多数重复项工作,从而为我们节约时间。而使用 Github Action 实现上述步骤,如虎添翼,更为方便。

快速入门教程

对于 GitHub Action 而言,官方规定了工作流文件必须存储在代码仓库的 .github/workflows 目录中,文件名必须以 .yml.yaml 结尾,从而便于 Github 识别这是一个工作流程。

创建新的工作流

要创建一个工作流,有两种方式:

  • 在线创建:Github-Reposity-actions 里去创建,创建过程中可以随时添加别的工作流;
  • 本地创建: 在项目目录里创建 .github/workflows 文件夹,并在其中创建你的工作流文件,Github 会自动按照规则识别;

1. 在线创建

我们直接去相应的 Github 仓库底下,点击 Actions ,此时有两种选择:

  • 在现有的工作流模版上进行创建;
  • 新创建自己的工作流文件;

比如下面的示例中,我们搜索 Android ,并选择 Android CI 模版进行创建,如下所示:

在上面的图2里,这是官方给我们的 基础Android CI 模版,具体的逻辑我们下面再解释。图中箭头所指的是一些比较热门的 Action ,可以选择其中一个,快速复制(引用)到我们自己的工作流中,即相当于添加一个新的子步骤。

2. 本地创建

我们以刚才上面截图中的 Android CI 为示例,直接复制到本地新建的 android.yml 中,如下所示:

创建工作流

工作流运行效果

写给Android工程师的 Github CI 快速指北_Android_02

写给Android工程师的 Github CI 快速指北_CI/CD_03

将相应的工作流 push 之后,如图所示,我们会发现,我们新 push 的工作流已经被触发了多次(原因下面解释),而列表最顶部的,也是最新的,即正在运行的工作流。

点击进去看一下,如下图所示:

工作流运行结果

工作流具体执行步骤

写给Android工程师的 Github CI 快速指北_GitHub_04

写给Android工程师的 Github CI 快速指北_CI/CD_05

左边的图表示这是本次的运行结果,以及一些工件的上传或者日志输出位置;而右图则代表这个工作流具体执行子步骤列表,我们也可以点击去查看每一个步骤做的结果。

示例工作流分析

如下所示,这是我们上面步骤创建的 Android CI 工作流,其目的是用于每次 push 代码后,执行一次 build ,具体代码如下:

name: Android CI

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

jobs:
  build:

    runs-on: ubuntu-latest

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

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
      - name: Build with Gradle
        run: ./gradlew build

相应的,其内部的含义如下所示:

  • name:
    表示当前工作流的名字是什么。
  • on:
    表示当前工作流在什么情况下被触发。
    比如这个示例中,我们触发的时机有两个,即 push || pr 时,并且限制了必须是在 main分支 。再解释的通俗点就是:
    当我们在main分支push了代码,或者提了一个新的PR,要合并到main分支时,此时就会触发这个工作流。

ps: 这也是为什么我们上面的截图里,创建一个新的工作流后,为什么会出现运行多次(首次创建时main分支触发+push行为触发)。

  • jobs:
    指的是当前任务列表。比如上述示例中我们只有一个任务,名为 build,当然也可以使用 name: 进行重命名。
  • runs-on:
    指定当前任务运行的环境。比如上述示例中任务是在 ubuntu-latest 系统下运行。
  • steps:
    指定当前任务的步骤列表。比如当前示例中,我们有多个任务,分别如下:
  • -拉代码;
  • -设置 jdkGradle 环境;
  • -为 Gradle 设置运行权限;
  • -执行 Gradle 命令: build;

uses:

用于导入开源的 Action 或者自定义的 Action ,从而在自己的工作流中进行使用;

run:

shell 环境中执行一段命令,常用于执行 cmd 命令;

需要注意: yml 文件,严格控制段落间的缩进,所以如果 IDE 提示异常,或者排列不齐时,经常会出现工作流运行时报错。

常用的环境变量

在使用 Github Action 时,我们常常会遇到需要使用一些环境变量的情况,比如最常用的 Github.token 等等,对于每一个工作流,默认提供了以下环境变量用于使用:

  • GITHUB_ACTION当前运行的操作的名称,或id步骤的名称;
  • GITHUB_RUN_ID当前运行的工作流ID,这个 id 是固定的;
  • 更多环境变量见文档

当然,我们也可以自定义一些环境变量,从而将其保存到 Github Action 里的 secrets 中,从而实现安全的存储与使用,而非硬编码的方式。

如下所示,我们将fir.im 的api token保存到secrets里,并取名为FIR_TOKEN:

写给Android工程师的 Github CI 快速指北_GitHub_06

在具体使用时,如下所示:

- name: echo token
  run: |
    echo "-token: ${{ secrets.FIR_TOKEN }}"

这里我们尝试去打印一下 token ,但是结果肯定是 [**] 。因为 Github Action 默认会对其进行隐藏,从而防止其被意外泄漏。

小练习

自动化打包

在开发中,我们日常接触最多的无非就是 [fix bug] & [create new bug] 😂,而如果每次如果都要手动打包,再转发给测试同学,无疑是一件及其浪费时间和无聊的事情。

本小节示例代码见:Android CI

这里我们以打包并上传fir 为例,如下所示:

写给Android工程师的 Github CI 快速指北_Android_07

上述流程如下:

  • 拉代码;
  • 安装 && 配置gradle环境;
  • 打debug包;
  • 安装 fir-cli & 上传apk;

上面内部使用的 FIR_TOKEN 正是我们上面在介绍环境变量部分时,自己定义的。

当然做的更详细点,这里还可以加上打包成功后webhook到飞书或者钉钉等,以飞书为例,可以使用我司另一个小伙伴写的这个 xiachufang/action-feishu,碍于篇幅,这里就不做解释了。

自动化版本号

本小节示例代码见:settings.gradle、release.yml

在日常发版本的过程中,我们都有打 tag 的经历,比如每周在发布新的版本之前,打 新版本tag ,同时打新的线上包。这个时候,如果每次都要去再改一次硬编码里的 versionName 以及 versionCode ,无疑有点烦人,而且人工操作,依然存在改错的问题😶。

此时,常见的方式是利用 tag 作为 versionNamegit commit 数作为 versionCode 。这种方式固然好用,但是还是不够严谨,对于常见的团队而言,一般有更统一的名称,如 版本名@版本号,示例:2.1.1@807,当然这都是后话了。

要实现上面的基础需求,需要我们对 GradleGit 有一丢丢使用经验。比如,怎么获取 最新tag 呢?怎么获取 commit 数呢?

解决方式如下所示:

写给Android工程师的 Github CI 快速指北_上传_08

如上所示,我们直接在 settings.gradle 中新增了以下代码。目的是当 Gradle 加载完当前项目信息之后,此时就利用 cmd 去获取一下当前的 最新tagcommit数,并将其设置给 ext,从而便于我们在其他地方引用。

外部使用方式:rootProject.versionCode

此时 build 我们的项目, build 日志部分就会打印下面语句:

versionName:xxx,versionCode:x

ps: 如果你的 versionName 是空,请注意是否打过 tag 🙃。 [git tag xxx]


上面的方法看着似乎没什么问题,但是如果你实际用几次就会发现,如果你 两个tag之间并没有任何变化的话,此时 gitVersionTag() 取出的 tag永远不是最新的那一条(不知道该怎么解决)😅。

这个时候,我们就可以利用 Github Action,获取最新 release.tag,然后将其以 gradle传参 的方式传递到我们本次编译中,从而实现自动化版本号。

如下所示:

写给Android工程师的 Github CI 快速指北_CI/CD_09

我们重新调整下上述的写法,每次优先获取外部传入的参数 versionName 以及 versionCode,同时对其check。如果没传递或者为null,则本地重新利用Git去获取,否则就使用指定的参数。

release.yml

写给Android工程师的 Github CI 快速指北_CI/CD_10

在具体的工作流脚本这里,我们的触发时机选择为每次发布新的 release 时,此时就去获取本次 release 对应的 tag_name,并在打包时,通过 Gradle 命令行传参的方式,将其传递给我们本次的打包流程。

上面的 env:用于设置一个或多个环境变量

比如在这个示例里,我们定义了一个名为 VERSION_NAME 的变量,其的值取自 本次release所对应的tag_name ,而 {{ xx }} 这种取值方式,则是 Github Action 中的一个规范。而在shell里,我们可以不加 {{ }} ,直接 $xx

自动化发布release

本小节示例代码见:create_release.yml

每个版本发布 release 时,我们一般都要去写一遍描述,但如果每个版本都去写一遍,无疑非常呆。

所以,那能不能把两个版本之间的 PR 自动收集下来,然后写到 release 的描述里呢?🤔

回答肯定是可以的,而且 Github 也提供了默认的方式,如下所示:

创建新的release

最终结果页

写给Android工程师的 Github CI 快速指北_CI/CD_11

写给Android工程师的 Github CI 快速指北_Android_12

看着效果还不错,省事不少。👏


那能不能我每次 push tag 时,就自动触发 release创建 呢?

🤖: 你最好别懒死。😂

当然是可以的,Github Action 工作流提供了很多触发时机,所以我们只需要设置触发时机为 push tag 时,然后再去新建 release 即可。

示例代码如下:

写给Android工程师的 Github CI 快速指北_上传_13

这里使用开源的 action,ncipollo/release-action,从而更简单的实现上述需求,当然也可以选择使用 Github Api

效果如下所示:

Github Action

Github release

写给Android工程师的 Github CI 快速指北_CI/CD_14

写给Android工程师的 Github CI 快速指北_上传_15

一些经验分享

关于 Github Action,因为其本身上手难度很低,所以当我们想解决某个问题时,只需要考虑清下面的几个问题:

  • 当前问题 到底 是什么?
  • 有没有开源的 Action ?
  • 如果问题比较复杂,那能不能拆解为多个步骤呢?
  • Github API 能不能解决,能不能搭配其他方式呢?如 shellpythonjargradle;

当然如果你想再探索一点,此时可以考虑以下:

  • 工作流复用,工作流依赖执行,工作流结果传递,工作流并发等等。

常用的一些资料:

  • Github Action开源库搜索;
  • Github Action文档;

总结

本篇,我们从 CI/CD 是什么开始,叨叨絮絮,一直到解决常见开发中的一些问题。纵观这些问题或者场景,虽然并不是特别繁琐,但也构成了 CI 的基本使用单元。希望通过这些场景,能让大家对于 Github CI 有快速的了解及上手体验。

当然我本人也不是一个熟练的 CI工程师 ,更多是个半吊子,所以文章里肯定也有模糊不清的地方,此时就建议大家多搜多试验,或者评论区问我。但对于这些工具方面,我个人的原则一直是,会用即可。当然更好的是,当问题不能直线解决时,我们能不能拆分步骤去逐个解决。

我们生在一个幸运的时代,很多事情,都能很简单的去解决,比如有问题问 GPT,不懂就翻翻源码,而对于一些繁琐的重复项工作,此时不妨交给CI/CD 或者其他 自动化工具。

对于开发者的我们而言,我们只需要明白一个原则:当下要解决什么问题,即可

把时间浪费在更有意思的事情上,真的 泰库辣 :)

见字如面,我们下篇文章再见 👋

参考

  • GitHub Actions文档

关于我

我是 Petterp ,一个 Android工程师。如果本文,你觉得写的还不错,不妨点个赞或者收藏,你的支持,是我持续创作的最大鼓励!

欢迎关注我的 公众号(Petterp) ,期待与你一同前进 :)