使用 GitLab CI/CD 来实现持续集成和持续交付
我们使用 GitLab 作为代码仓库,使用 GitLab CI/CD 作为持续集成,持续部署工具。
我们要实现如下自动化流程
- 代码提交或合并到仓库后,启动测试、构建过程
- 每日定时发布新包或可手动发布
- 可以选择构建不同环境(qa, production)的 ipa 或 apk
- iOS 自动将测试包发布到自建的文件服务器,并可扫码安装
- iOS 自动将生产包发布到 App Store Connect
- Android 自动将 apk 发布到自建的文件服务器,并可扫码安装
自建文件服务器
为了方便交付,我们需要部署一个文件服务器。我们上传 apk 或 ipa 到该服务器,测试组同学通过扫码安装即可。
我们在 GitHub 找到了一个开源的文件服务器,Go Http File server。
我们把该文件服务器部署到外网,并且支持 https,因为部署 ipa 需要 https 协议。
安装和注册 GitLab Runner
GitLab Runner 是用来跑构建和部署任务的,我们需要在 CI 服务器或本机安装。
- 通过 Homebrew 安装 GitLab Runner
brew install gitlab-runner
- 注册 Runner
在 GitLab 上打开项目,找到左侧菜单 Settings -> CI / CD -> Runners -> Expand
在这里,有我们注册需要用到的信息,也可以注册 Shared Runners 或 Group Runners ,这需要 GitLab 的管理员或项目组的 owner 为你提供 token
Shared Runners 对 GitLab 上的所有项目生效
Group Runners 对项目组的所有项目生效
Specific Runner 只对该项目生效
输入如下命令,开始注册
gitlab-runner register
tags 填写 ios,android,react-native
executor 填写 shell
刷新界面,可以看到刚刚注册的 Runner
但是处于未激活状态,执行以下命令,激活该 Runner
gitlab-runner install
gitlab-runner start
再次刷新页面,可以看到 Runner 已经被激活
GitLab Runner 内部使用的 shell 是 bash,如果主机的默认 shell 是 zsh,需要在 ~/.bash_profile 文件中添加
source ~/.zshrc
GitLab Runner 的配置文件位于 ~/.gitlab-runner/config.toml
注入环境变量
我们的脚本依赖了许多环境变量,我们可以通过以下方式配置那些不会经常发生变化的环境变量。
前往 Settings -> CI / CD -> Variables
分别注入 APP_STORE_CONNECT_API_KEY_PATH
FASTLANE_TEAM_ID
FILE_SERVER
MATCH_GIT_URL
SLACK_WEB_HOOT_URL
等环境变量
还记得 fastlane 目录下那个 .env 文件吗?在跑 CI 的机器上是不需要它的,因为可以通过 CI 注入环境变量。
别忘了把 fastlane API Key JSON file 拷贝到 APP_STORE_CONNECT_API_KEY_PATH
所指向的路径。
编写 CI 脚本
在 react-native 项目根目录下创建名为 ci 的文件夹
创建 ci/utils.js 文件,这个文件负责提供一些工具函数,具体内容请查看 ci/utils.js 文件。
其中 SLACK_WEB_HOOK_URL
请替换成你司的 slack web hook url。
创建 ci/config.js 文件,这里定义了常量, 某些常量的值通过读取环境变量获得。 通常,你需要替换 FILE_SERVER
APP_NAME
APP_MODULE
APPLICATION_ID
这几个常量。具体内容请查看 ci/config.js 文件
编写打包脚本,关键代码如下
// android 打包脚本,具体请查看 ci/build/android.js 文件
const workdir = process.env.ANDROID_DIR || path.join(REACT_ROOT, 'android')
sh(`./gradlew assemble${ENVIRONMENT_CAPITALIZE}Release`, { cwd: workdir })
// ios 打包脚本,具体请查看 ci/build/ios.js 文件
const workdir = process.env.IOS_DIR || path.join(REACT_ROOT, 'ios')
if (process.env.SHOULD_RUBY_GEM_UPDATE === 'true') {
sh(`gem install bundler && bundle install`, { cwd: workdir })
}
sh('bundle exec fastlane build', { cwd: workdir })
包打好后,自然要上传到服务器,以下是上传文件的关键代码
// android 上传 apk 脚本,具体请查看 ci/upload/android.js
const apk = path.join(ARTIFACTS_DIR, `${APP_MODULE}-${ENVIRONMENT}-${abi}-${BUILD_TYPE}.apk`)
let filename = `${APP_NAME}-${ENVIRONMENT}-${abi}-${BUILD_TYPE}-${VERSION_NAME}-${VERSION_CODE}.apk`
if (process.env.CI_BUILD_REF_SLUG) {
filename = `${APP_NAME}-${process.env.CI_BUILD_REF_SLUG}-${ENVIRONMENT}-${abi}-${BUILD_TYPE}-${VERSION_NAME}-${VERSION_CODE}.apk`
}
uploadFile(apk, filename, `${FILE_SERVER}/${dest}`)
// ios 上传 ipa 脚本,具体请查看 ci/upload/ios.js
uploadFile(`${file}.ipa`, `${filename}.ipa`, `${FILE_SERVER}/${dest}`)
uploadFile(`${file}.plist`, `${filename}.plist`, `${FILE_SERVER}/${dest}`)
uploadFile(`${file}.html`, `${filename}.html`, `${FILE_SERVER}/${dest}`)
iOS 除了需要上传 ipa 包,还需要上传一个 plist 文件和一个 html 文件。
html 文件提供一个二维码,通过 iphone 的相机扫码后打开,点击网页上的按钮即可安装 ipa 包进行测试。
html 通过模版生成,具体内容请查看 ci/upload/template/ipa.html 文件,关键代码如下
<div class="container">
<h1>使用相机扫描二维码</h1>
<h3>{AppVersion}</h3>
<div id="qrcode"></div>
<div class="download">
<a title="iPhone" href="itms-services://?action=download-manifest&url={PListUrl}">点击下载安装</a>
</div>
<div class="server">
<a title="iPhone" href="{ServerUrl}">前往文件服务器</a>
</div>
</div>
这里使用了苹果提供的 itms-services
协议,{PListUrl} 会被替换成 plist 文件所在 url。
plist 文件通过模版生成,具体内容请查看 ci/upload/template/ipa.plist 文件,关键代码如下
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>{IpaUrl}</string>
</dict>
</array>
其中 {IpaUrl} 会被替换成 ipa 文件所在 url。
编写 .gitlab-ci.yml
在根目录创建 .gitlab-ci.yml 文件,该文件由 YAML 语言 编写,更多的配置可查看官方文档。
# .gitlab-ci.yml
before_script:
- export
stages:
- build
- deploy
variables:
LC_ALL: 'en_US.UTF-8'
LANG: 'en_US.UTF-8'
APP_MODULE: app
build:ios:
stage: build
artifacts:
paths:
- ios/build/
script:
- yarn install
- node ./ci/build ios
tags:
- ios
except:
refs:
- tags
variables:
- $ANDROID_ONLY
deploy:ios:upload:
stage: deploy
dependencies:
- build:ios
script:
- node ./ci/upload ios
only:
- schedules
tags:
- ios
except:
variables:
- $ANDROID_ONLY
build:android:
stage: build
script:
- yarn install
- node ./ci/build android
artifacts:
paths:
- android/${APP_MODULE}/build/artifacts/
tags:
- android
except:
refs:
- tags
variables:
- $IOS_ONLY
deploy:android:upload:
stage: deploy
dependencies:
- build:android
script:
- node ./ci/upload.js android
only:
- schedules
tags:
- android
except:
variables:
- $IOS_ONLY
创建 GitLab 仓库
添加如下配置到 .gitignore 中
Pods/
builds/
我们在公司自建的 GitLab 服务器上创建一个新的项目, 根据指引,执行如下命令,将代码推到仓库。
cd existing_folder
git init
git remote add origin [email protected]:react-native/myapp.git
git add .
git commit -m "Initial commit"
git push -u origin master
自动触发每日构建、部署
根据我们在 .gitlab-ci.yml 文件的配置,当有新的代码 push 或 merge 到仓库,将会自动触发构建流程。
我们也可以配置每日自动构建、部署。
在 GitLab 上打开项目,找到左侧菜单 CI / CD -> Schedules
点击绿色的 "New schedule" 按钮
在这里,我们注入了 ENVIRONMENT
等环境变量,表示要打生产环境的包。是的,我们在这里配置那些会经常发生变化的环境变量。
一个定时构建任务就创建好了,如果有需要,也可以点击 Play 按钮立即触发构建、部署任务
如果只想手动触发而不希望定时触发,在创建 Schedule 时把 Acitive 的勾去掉就好。
下面,就让我们来 Play 一下吧:
我们还可以利用 Settings -> CI / CD -> Pipeline triggers 来给测试同学提供一个触发构建和部署的页面,如果他们有需要的话。
本地调试
所有脚本均可以在本机进行调试
cd 到项目根目录,分别输入以下命令试试
调试 ios 打包脚本
node ./ci/build ios
调试在 gitlab-ci.yml 中配置的任务,此时会注入一些 CI 环境变量
gitlab-runner exec shell build:ios