# 利用 GitHub Actions 进行 iOS 项目的 CI/CD

By [Untitled](https://paragraph.com/@0x74cdebd6028e5d8407c2ad6f13bedd9892e3fb2a) · 2021-12-09

---

搞 iOS 以来，一直在想找一个云主机进行 iOS 项目的 CI 探索，方向想偏了，没找到几家提供 macOS 系统的云主机，遂作罢。

最近在读 《[iOS App Distribution & Best Practices](https://www.raywenderlich.com/books/ios-app-distribution-best-practices/)》，里面提到现在有大致三类 CI 服务商：

1.  Full-service CI，即全方位服务的 CI 提供商，可以理解为通过交互页面就可以进行部署。虽然简单，但可能不太容易拓展。典型如：
    
    1.  Bitrise: [https://www.bitrise.io/](https://www.bitrise.io/)
        
    2.  Microsoft’s App Center: [https://appcenter.ms/](https://appcenter.ms/)
        
2.  Managed CI，即在云上替你托管硬件相关的工作，你只需要提供构建脚本，在 iOS 项目中，常见的就是构建方式就是 `fastlane` 和 `xcodebuild`。比较流行的如：
    
    1.  CircleCI: [https://circleci.com/](https://circleci.com/)
        
    2.  GitHub Actions: [https://Github.com/features/actions](https://github.com/features/actions)
        
3.  Manual CI，即手动管理，感觉跟自己在云主机上处理一个概念。便宜，但得自己管理服务器。最著名的就是 Travis CI: [https://travis-ci.org/](https://travis-ci.org/) 了。
    

怀着激动的心，每个官方都去逛了一遍，确实都提供了 macOS 的构建环境，nice，der~。而且都有都有免费的配额（收费方式都是卖点卡）。

最后还是选择 GitHub Actions 作为远程 CI 的第一个实验品。截图如下

![job](https://storage.googleapis.com/papyrus_images/55f30429da39d1b5fc403c32ca14955f3aa8d3f3a0712232704dfd8a238dbb49.png)

job

GitHub Actions 使用
=================

中文的概念介绍和使用方式可以去看阮一峰的 [GitHub Actions 入门教程](https://www.ruanyifeng.com/blog/2019/09/getting-started-with-github-actions.html) 。

或者直接去看官方文档 [GitHub Actions](https://docs.github.com/en/actions) 。

简单来说，就是在仓库目录 `.github/workflows/` 放置脚本，当有 push 或 PR 的时候，就会触发脚本运行。

**免费额度查看方式**，「右上角LOGO」-「Settings」-「Billings & plans」

触发条件
====

    name: iOS starter workflow
    
    # 触发条件，在 main 分支上有 push 和 PR 行为时触发脚本
    on:
      push:
        branches: [ main ]
      pull_request:
        branches: [ main ]
    

Step 整理
=======

构建环境
----

    jobs:
      build:
        name: Build and Test default scheme using any available iPhone simulator
        # 环境
        runs-on: macos-latest
          
          steps:
              # Checkout 当前分支进行处理
              - name: Checkout
            uses: actions/checkout@v2
              - name: stepxxx
                  # operation
     
    

Pod install
-----------

iOS 项目免不了用 cocoaPods 安装依赖。

          - 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!](https://blog.cocoapods.org/CocoaPods-1.7.2/) 表示，随着 pod 数目的增多，利用 github 作为数据库备份越来越慢了，已经更换成 CDN 的形式了。

    # source 'https://github.com/CocoaPods/Specs.git'
    # 替换成 CDN，或者干脆删除。因为默认 CDN 源了。
    source 'https://cdn.cocoapods.org/'
    

默认 Scheme
---------

          - 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 作为模拟器。

          - 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
----

          - 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传递状态
============

1.  使用 ravsamhq/notify-slack-action@v1 将 Job 状态通知到 Slack。
    
2.  因为该 action 运行在 Linux 系统，所以需要另起一个 Job。
    
3.  如何将上一个 Job 的状态通知到下一个 Job？一种思路是将 Job\_A的状态通过文本保存到本地，再在Job\_B中读取该状态。
    
    可参考：[How get the status of previous jobs](https://github.community/t/how-get-the-status-of-previous-jobs/17555)
    

保存状态
----

          - 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
    

读取状态
----

          - 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: 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-slack-action 跑在 Linux 系统所以另起一个 Job
      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 }}

---

*Originally published on [Untitled](https://paragraph.com/@0x74cdebd6028e5d8407c2ad6f13bedd9892e3fb2a/github-actions-ios-ci-cd)*
