背景
关于 CI/CD
,在2023年的今天,基本所有技术团队或多或少都会使用,其很大程度上减轻了我们的冗余重复工作,从而简化我们的工作流程。
不过对于大多数客户端工程师而言,其实 CI
这个词还是比较陌生。当然并不是说,CI/CD
有什么高大上或者门槛很高,因为毕竟不是所有人会去维护开源库或者搞基建,或者说少有场景去接触到。但对于一个 工程师 而言,这严格意义上其实属于 基本技能点 ,或者说在现在这个时代,这种小技能应该没有太多边界之分。
相应的,如果使用 Github Action
,这个难度就更低了,其相比传统的 Jenkins
,容易上手了更多,简化了 环境 、配置 等等。并且,在这个过程中,我们也将逐步接触到一些 cmd
、python
等其他工具使用方式或者语法,从而探索出更多可玩性。
写在开始
注意: 本篇不会讲 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
工具可以自动化执行部署和发布过程,例如Ansible
、Kubernetes
、Docker
等。在持续部署/交付的过程中,需要进行自动化测试、版本控制、持续监控等操作,以确保代码质量和应用稳定性。
CI/CD
的优点包括加速软件开发、提高代码质量、降低风险、提高工作效率 等。从而可以让开发团队更加专注于代码编写,而不必花费大量时间进行 手动构建 、测试 和 部署 等重复性工作。
什么是 Github CI?
因为本篇,主要是讲 Github CI
的使用,故还是要简单说一下 Github CI
的简介及功能。
GitHub CI
(GitHub Actions)是GitHub
提供的一项自动化工具,用于 构建 、 测试 和 部署GitHub
上托管的代码仓库。GitHub CI
提供了一种定义自动化工作流程的方式,可以根据代码仓库的变化自动触发工作流程。一组工作流程可以包括多个步骤,例如编译代码、运行测试、构建镜像、部署应用等。其优点包括与GitHub
平台紧密集成、易于配置、支持多种语言和环境、提供丰富的集成能力等。它可以帮助开发团队自动化构建和测试过程,提高代码质量和开发效率。
具体运行示例中如下图所示:
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
中,如下所示:
创建工作流 | 工作流运行效果 |
---|---|
将相应的工作流 push
之后,如图所示,我们会发现,我们新 push
的工作流已经被触发了多次(原因下面解释),而列表最顶部的,也是最新的,即正在运行的工作流。
点击进去看一下,如下图所示:
工作流运行结果 | 工作流具体执行步骤 |
---|---|
左边的图表示这是本次的运行结果,以及一些工件的上传或者日志输出位置;而右图则代表这个工作流具体执行子步骤列表,我们也可以点击去查看每一个步骤做的结果。
示例工作流分析
如下所示,这是我们上面步骤创建的 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:
指定当前任务的步骤列表。比如当前示例中,我们有多个任务,分别如下:
- -拉代码;
- -设置
jdk
与Gradle
环境; - -为
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:
在具体使用时,如下所示:
- name: echo token
run: |
echo "-token: ${{ secrets.FIR_TOKEN }}"
这里我们尝试去打印一下 token
,但是结果肯定是 [**] 。因为 Github Action
默认会对其进行隐藏,从而防止其被意外泄漏。
小练习
自动化打包
在开发中,我们日常接触最多的无非就是 [fix bug]
& [create new bug]
😂,而如果每次如果都要手动打包,再转发给测试同学,无疑是一件及其浪费时间和无聊的事情。
本小节示例代码见:Android CI
这里我们以打包并上传fir 为例,如下所示:
上述流程如下:
- 拉代码;
- 安装 && 配置gradle环境;
- 打debug包;
- 安装
fir-cli
& 上传apk;
上面内部使用的
FIR_TOKEN
正是我们上面在介绍环境变量部分时,自己定义的。
当然做的更详细点,这里还可以加上打包成功后webhook到飞书或者钉钉等,以飞书为例,可以使用我司另一个小伙伴写的这个 xiachufang/action-feishu,碍于篇幅,这里就不做解释了。
自动化版本号
本小节示例代码见:settings.gradle、release.yml
在日常发版本的过程中,我们都有打 tag
的经历,比如每周在发布新的版本之前,打 新版本tag ,同时打新的线上包。这个时候,如果每次都要去再改一次硬编码里的 versionName
以及 versionCode
,无疑有点烦人,而且人工操作,依然存在改错的问题😶。
此时,常见的方式是利用 tag
作为 versionName
,git commit
数作为 versionCode
。这种方式固然好用,但是还是不够严谨,对于常见的团队而言,一般有更统一的名称,如 版本名@版本号,示例:2.1.1@807,当然这都是后话了。
要实现上面的基础需求,需要我们对 Gradle
与 Git
有一丢丢使用经验。比如,怎么获取 最新tag 呢?怎么获取 commit
数呢?
解决方式如下所示:
如上所示,我们直接在 settings.gradle
中新增了以下代码。目的是当 Gradle
加载完当前项目信息之后,此时就利用 cmd
去获取一下当前的 最新tag 与 commit数,并将其设置给 ext
,从而便于我们在其他地方引用。
外部使用方式:
rootProject.versionCode
此时 build
我们的项目, build
日志部分就会打印下面语句:
versionName:xxx,versionCode:x
ps: 如果你的
versionName
是空,请注意是否打过tag
🙃。 [git tag xxx]
上面的方法看着似乎没什么问题,但是如果你实际用几次就会发现,如果你 两个tag之间并没有任何变化的话,此时 gitVersionTag() 取出的 tag
永远不是最新的那一条(不知道该怎么解决)😅。
这个时候,我们就可以利用 Github Action
,获取最新 release.tag
,然后将其以 gradle传参
的方式传递到我们本次编译中,从而实现自动化版本号。
如下所示:
我们重新调整下上述的写法,每次优先获取外部传入的参数 versionName
以及 versionCode
,同时对其check。如果没传递或者为null,则本地重新利用Git去获取,否则就使用指定的参数。
release.yml
在具体的工作流脚本这里,我们的触发时机选择为每次发布新的 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 | 最终结果页 |
---|---|
看着效果还不错,省事不少。👏
那能不能我每次 push tag
时,就自动触发 release创建
呢?
🤖: 你最好别懒死。😂
当然是可以的,Github Action
工作流提供了很多触发时机,所以我们只需要设置触发时机为 push tag
时,然后再去新建 release
即可。
示例代码如下:
这里使用开源的 action,ncipollo/release-action,从而更简单的实现上述需求,当然也可以选择使用 Github Api
。
效果如下所示:
Github Action | Github release |
---|---|
一些经验分享
关于 Github Action
,因为其本身上手难度很低,所以当我们想解决某个问题时,只需要考虑清下面的几个问题:
- 当前问题 到底 是什么?
- 有没有开源的
Action
? - 如果问题比较复杂,那能不能拆解为多个步骤呢?
Github API
能不能解决,能不能搭配其他方式呢?如shell
、python
、jar
、gradle
;
当然如果你想再探索一点,此时可以考虑以下:
- 工作流复用,工作流依赖执行,工作流结果传递,工作流并发等等。
常用的一些资料:
- Github Action开源库搜索;
- Github Action文档;
总结
本篇,我们从 CI/CD
是什么开始,叨叨絮絮,一直到解决常见开发中的一些问题。纵观这些问题或者场景,虽然并不是特别繁琐,但也构成了 CI
的基本使用单元。希望通过这些场景,能让大家对于 Github CI
有快速的了解及上手体验。
当然我本人也不是一个熟练的 CI工程师 ,更多是个半吊子,所以文章里肯定也有模糊不清的地方,此时就建议大家多搜多试验,或者评论区问我。但对于这些工具方面,我个人的原则一直是,会用即可。当然更好的是,当问题不能直线解决时,我们能不能拆分步骤去逐个解决。
我们生在一个幸运的时代,很多事情,都能很简单的去解决,比如有问题问 GPT
,不懂就翻翻源码,而对于一些繁琐的重复项工作,此时不妨交给CI/CD
或者其他 自动化工具。
对于开发者的我们而言,我们只需要明白一个原则:当下要解决什么问题,即可。
把时间浪费在更有意思的事情上,真的 泰库辣 😃
见字如面,我们下篇文章再见 👋
参考
- GitHub Actions文档
关于我
我是 Petterp ,一个 Android工程师。如果本文,你觉得写的还不错,不妨点个赞或者收藏,你的支持,是我持续创作的最大鼓励!
欢迎关注我的 公众号(Petterp) ,期待与你一同前进 😃