搞 iOS 以来,一直在想找一个云主机进行 iOS 项目的 CI 探索,方向想偏了,没找到几家提供 macOS 系统的云主机,遂作罢。
最近在读 《iOS App Distribution & Best Practices》,里面提到现在有大致三类 CI 服务商:
- Full-service CI,即全方位服务的 CI 提供商,可以理解为通过交互页面就可以进行部署。虽然简单,但可能不太容易拓展。典型如:
- Bitrise: https://www.bitrise.io/
- Microsoft’s App Center: https://appcenter.ms/
- Managed CI,即在云上替你托管硬件相关的工作,你只需要提供构建脚本,在 iOS 项目中,常见的就是构建方式就是
fastlane
和 xcodebuild
。比较流行的如:
- CircleCI: https://circleci.com/
- GitHub Actions: https://Github.com/features/actions
- Manual CI,即手动管理,感觉跟自己在云主机上处理一个概念。便宜,但得自己管理服务器。最著名的就是 Travis CI: https://travis-ci.org/ 了。
怀着激动的心,每个官方都去逛了一遍,确实都提供了 macOS 的构建环境,nice,der~。而且都有都有免费的配额(收费方式都是卖点卡)。
最后还是选择 GitHub Actions 作为远程 CI 的第一个实验品。截图如下
GitHub Actions 使用
中文的概念介绍和使用方式可以去看阮一峰的 GitHub Actions 入门教程 。
或者直接去看官方文档 GitHub Actions 。
简单来说,就是在仓库目录 .github/workflows/
放置脚本,当有 push 或 PR 的时候,就会触发脚本运行。
免费额度查看方式,「右上角LOGO」-「Settings」-「Billings & plans」
触发条件
1 2 3 4 5 6 7 8
| name: iOS starter workflow
on: push: branches: [ main ] pull_request: branches: [ main ]
|
Step 整理
构建环境
1 2 3 4 5 6 7 8 9 10 11 12 13
| jobs: build: name: Build and Test default scheme using any available iPhone simulator runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v2 - name: stepxxx
|
Pod install
iOS 项目免不了用 cocoaPods 安装依赖。
1 2 3
| - name: Install Dependencies run: | pod install --repo-update
|
踩坑记录
在 Podfile
头部标记 source 'https://github.com/CocoaPods/Specs.git'
不是很明智。
cocoaPods 官方在 CocoaPods 1.7.2 — Master Repo CDN is Finalized! 表示,随着 pod 数目的增多,利用 github 作为数据库备份越来越慢了,已经更换成 CDN 的形式了。
1 2 3
|
source 'https://cdn.cocoapods.org/'
|
默认 Scheme
1 2 3 4 5 6
| - name: Set Default Scheme run: | scheme_list=$(xcodebuild -list -json | tr -d "\n") default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") echo $default | cat >default echo Using default scheme: $default
|
Build
用模拟器进行编译。优先跑 .xcworkspace,无则跑 .xcodeproj。
我测试的时候,在 GitHub Actions 上 xcrun
得到的模拟器名称不正确,所以直接固定了使用 iPhone 11 作为模拟器。
1 2 3 4 5 6 7 8 9 10 11 12
| - name: Build env: scheme: ${{ 'default' }} platform: ${{ 'iOS Simulator' }} run: | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) # device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}'` device='iPhone 11' if [ $scheme = default ]; then scheme=$(cat default); fi if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device"
|
Test
1 2 3 4 5 6 7 8 9 10 11 12
| - name: Test env: scheme: ${{ 'default' }} platform: ${{ 'iOS Simulator' }} run: | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) # device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}'` device='iPhone 11' if [ $scheme = default ]; then scheme=$(cat default); fi if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device"
|
通知以及跨Job传递状态
使用 ravsamhq/notify-slack-action@v1 将 Job 状态通知到 Slack。
因为该 action 运行在 Linux 系统,所以需要另起一个 Job。
如何将上一个 Job 的状态通知到下一个 Job?一种思路是将 Job_A的状态通过文本保存到本地,再在Job_B中读取该状态。
可参考:How get the status of previous jobs
保存状态
1 2 3 4 5 6 7 8 9 10
| - name: Create file status_build_test.txt and write the job status into it if: always() run: | echo ${{ job.status }} > status_build_test.txt - name: Upload file status_build_test.txt as an artifact if: always() uses: actions/upload-artifact@v1 with: name: pass_status_build_test path: status_build_test.txt
|
读取状态
1 2 3 4 5 6 7 8
| - name: Download artifact pass_status_build_test uses: actions/download-artifact@v1 with: name: pass_status_build_test - name: Set the statuses of Job build output parameters id: set_outputs run: | echo "::set-output name=status_build_test::$(<pass_status_build_test/status_build_test.txt)"
|
附录-完整版
附上我当前使用的完整脚本,持续完善中。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| name: iOS starter workflow
on: push: branches: [ main ] pull_request: branches: [ main ]
jobs: build: name: Build and Test default scheme using any available iPhone simulator runs-on: macos-latest
steps: - name: Checkout uses: actions/checkout@v2 - name: Install Dependencies run: | pod install --repo-update - name: Set Default Scheme run: | scheme_list=$(xcodebuild -list -json | tr -d "\n") default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") echo $default | cat >default echo Using default scheme: $default - name: Build env: scheme: ${{ 'default' }} platform: ${{ 'iOS Simulator' }} run: | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) # device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}'` # 直接指定 iPhone 11 device='iPhone 11' if [ $scheme = default ]; then scheme=$(cat default); fi if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" - name: Test env: scheme: ${{ 'default' }} platform: ${{ 'iOS Simulator' }} run: | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) # device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}'` # 直接指定 iPhone 11 device='iPhone 11' if [ $scheme = default ]; then scheme=$(cat default); fi if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" - name: Create file status_build_test.txt and write the job status into it if: always() run: | echo ${{ job.status }} > status_build_test.txt - name: Upload file status_build_test.txt as an artifact if: always() uses: actions/upload-artifact@v1 with: name: pass_status_build_test path: status_build_test.txt notify: needs: [ build ] if: always() name: Notify Slack runs-on: ubuntu-latest steps: - name: Download artifact pass_status_build_test uses: actions/download-artifact@v1 with: name: pass_status_build_test - name: Set the statuses of Job build output parameters id: set_outputs run: | echo "::set-output name=status_build_test::$(<pass_status_build_test/status_build_test.txt)" - name: Notify Slack uses: ravsamhq/notify-slack-action@v1 if: always() with: status: ${{ steps.set_outputs.outputs.status_build_test }} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
|