在做的项目最近终于接近了第一个里程碑 ,挤了些时间把一直没做的 CI/CD 利用现有的资源和 GitLab 实现了,记录下大概的过程。

CI/CD 是什么,为什么需要 CI/CD

什么是 CI/CD ?持续集成与持续交付
上面这篇文章已经总结得不错了,我这边主要也是利用 CI/CD 确保每次 push 的代码都是完整可编译的,从而减少错误的发生,同时自动部署,免去手动发布 APP 测试版本和登陆测服人工部署网站更新的麻烦,提高工作效率。

相比于原始的编写自动化脚本(shell/python等),yaml 格式的 CI/CD 配置方式简化了流程,提供了很多便捷的功能。但是相对应的,需要查阅相关配置文档,而且需要更强的 shell 命令编写能力

Flutter 项目(目前仅限 Android 端)

安装 GitLab Runner

install_runner
如上图,进入项目的 GitLab 主页,导航至[设置]-[CI/CD]-展开[Runner]选项卡,可以看到简易教程.

这里我他妈被误导了😤,看到那个大大蓝底的“在 Kubernetes 上安装 Runner”按钮,以为是 Runner 必须安装在 K8s上呢,结果浪费了时间去搭建 K8s 还没搭成功……其实这里表述的真实含义是在 K8s 集群上或者某台服务器上安装 Runner 都是可以的,二选一。。。😑

手动安装 GitLab Runner 的教程在: https://docs.gitlab.com/runner/install/

由于 Flutter 项目环境相对比较难配,为了以后可以方便地在多台设备上配置 Runner, 所以我选择基于 docker 环境用以项目编译。恰好内网中有一台之前创建好专门用于运行 docker 的 debian 服务器,所以这次就把项目需要的 Runner 直接安装在这台服务器上了。

在服务器上执行安装命令(参考:Install GitLab Runner using the official GitLab repositories):

1
2
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
export GITLAB_RUNNER_DISABLE_SKEL=true; sudo -E apt-get install gitlab-runner

注册 Runner

安装好后运行注册命令:

1
sudo gitlab-ci-multi-runner register

配置如下:

register

  1. 设置 GitLab 服务器的地址,用于 Runner 定时查询 GitLab server 是否有新的作业;
  2. 设置 Token(注册令牌);
  3. 设置 Runner 的描述,用语在 GitLab 管理页面区分不同的 Runner 实例;
  4. 设置 tag,和项目通过 .gitlab-ci.yml 文件定义流水线时指定的 tag 匹配;
  5. 选择执行器,这里输入 docker
  6. 由于上一步选择了 docker,这一步需要指定默认的 Docker image,这里提供的是 flutter 官方的镜像。

建议此时预先 pull 好所需的镜像,这样第一次作业执行时就不用花时间 pull 镜像了:

1
docker pull cirrusci/flutter:latest

这样一来就注册完成了,可以在下面的页面看到新注册的 Runner:
runners

编写 CI 配置文件

enable_ci_cd

回到项目主页,点击 配置 CI/CD 按钮,开始编写 CI 流水线配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
stages:
# 作业执行时会依次执行下面列出的 stage,为了方便,这里只设置了一个stage
- release

release_project:
# 如果省略,则会使用 Runner 注册时配置的镜像,否则会使用这里指定的镜像
image: cirrusci/flutter:latest
stage: release
script:
# 为了在国内网络下加速 flutter 项目依赖的下载速度,先配置 pub 仓库的中国镜像
- export PUB_HOSTED_URL=https://pub.flutter-io.cn
- export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
# 由于Android系统App升级时要求下载的新版本apk文件内的版本号大于已安装的版本号
# 所以这里将作业ID写入flutter的版本号配置,用于实现Android客户端正常的版本更新
- sed -i "s/\(version.\+\)[0-9]+\([0-9]\)/\1${CI_JOB_ID}+${CI_JOB_ID}/" pubspec.yaml
# 打release包所需的签名文件和key.properties用如下方式在流水线中动态添加,参考问题如下:
# https://stackoverflow.com/questions/51725339/how-to-manage-signing-keystore-in-gitlab-ci-for-android
- echo ${FLUTTER_BUILD_APK_KEY} | base64 -d > key.jks
- echo storePassword=${FLUTTER_BUILD_APK_KEY_PASSWD} > android/key.properties
- echo keyPassword=${FLUTTER_BUILD_APK_KEY_PASSWD} >> android/key.properties
- echo keyAlias=key >> android/key.properties
- echo storeFile=${CI_PROJECT_DIR}/key.jks >> android/key.properties
- flutter pub get
- flutter clean
# - flutter doctor --android-licenses
# 由于项目中使用了intl_utils用于生成多语言资源,所以需要执行下面两句生成i8n代码
- flutter pub global activate intl_utils
- flutter --no-color pub global run intl_utils:generate
# 替换项目默认的仓库为阿里云的maven仓库用于加速android项目的构建速度
- sed -i "s/google()/maven { url 'https:\/\/maven.aliyun.com\/repository\/google' }/g" android/build.gradle
- sed -i "s/jcenter()/maven { url 'https:\/\/maven.aliyun.com\/repository\/jcenter' }/g" android/build.gradle
- sed -i "s/google()/maven { url 'https:\/\/maven.aliyun.com\/repository\/google' }/g" ${FLUTTER_HOME}/packages/flutter_tools/gradle/flutter.gradle
- sed -i "s/jcenter()/maven { url 'https:\/\/maven.aliyun.com\/repository\/jcenter' }/g" ${FLUTTER_HOME}/packages/flutter_tools/gradle/flutter.gradle
- sed -i "s/https:\/\/storage.googleapis.com/https:\/\/storage.flutter-io.cn\/download.flutter.io/g" ${FLUTTER_HOME}/packages/flutter_tools/gradle/flutter.gradle
# 执行flutter编译apk的指令,指定仅编译arm指令的apk,并且禁用代码压缩以避免某些原生插件的异常行为
- flutter -v build apk --no-shrink --target-platform=android-arm
# 将编译生成的最终产物先移动到根目录下,否则通过下载得到的作业产物将有非常深的目录层级
- mv build/app/outputs/apk/release/app-release.apk app-release.apk
- echo "build success, uploading..."
# 利用curl和python脚本将编译产物、git提交信息、apk的md5值等信息发送到app后台服务器,实现自动发布新测试版
- curl -X 'POST' -F "file=@app-release.apk" http://xxx.xxx.xxx.xx/debugger_api/upload/`md5sum app-release.apk | awk '{print $1}'`.apk
- python3 deploy.py `md5sum app-release.apk | awk '{print $1}'` ${CI_JOB_ID} "${CI_COMMIT_MESSAGE}"
- echo "uploaded, start release..."
artifacts:
paths:
# 这里指定的是作业结束后保留的作业产物,由于上面已经复制到了根目录,所以可以直接给出文件名
- app-release.apk

tags:
- flutter

效果

每次向 master 分支提交代码时,都会自动触发 CI 流水线:

jobs

可以在作业详情页面查看流水线的日志、下载作业产物等:

job_result

作业执行完后自动将apk上传服务器发版,app侧可以检测到版本更新:

app_update

加速构建速度

刚配置好的流水线运行时间长达接近一个小时,原因在于每次执行 job 时都会基于 docker base image 开始重新构建环境,需要重新下载 gradle、flutter插件、android build tools、android 依赖等大量资源,即使配置了镜像加速,重新下载这些东西还是要花费大量时间。

解决办法就是配置这些路径为 docker 的 volume 以持久化数据,防止重复下载。
修改 Runner 服务器的 /etc/gitlab-runner/config.toml

volume