使用 CircleCI 2.0 Workflows 挑戰三倍速

減少 Monorepo CI 所需花費時間

Michael Hsu
8 min readJul 28, 2017
CI Workflows Parallel Job Execution

CircleCI 2.0 雖然出來一年多了,不久前七月初才正式 Launch,聽說加速不少,並且基於 Docker 以及 Workflow 設定模式,與公司內部的 DroneCI 設定起來蠻相似的,所以來試試看。

專案背景簡介

這是 MediaTek IoT Cloud 開源專案之一,文章討論的設定專注於前端的部分,基本上皆為 JavaScript 實作。此專案屬於 Monorepo 性質,也就是一個 Repository 包含數個 Project (Website) 或是 Library (NPM)。

在年初的時候因為還是 Private 狀態,當時考慮了 Circle 1.0 一個 Private 免費額度來作為主要 CI,開源後換到主流的 Travis,畢竟整合的服務較為完善,策略上兩個 CI 同時分別跑一部分的 Job,然後再設定 GitHub Protect 功能,必須都要綠勾勾才能 Merge PR。

以下作為數據參考:

  • Project 頁面類型數量:4
  • Library 類型數量:12
  • Test File 數量:262、Coverage: 87%
// jest
Test Suites: 225 total
Tests: 628 total
Snapshots: 479 total

CircleCI 2.0 VS Travis Beta

本篇文章一部分在於 Travis 與 Circle 2.0 的比較。雖然 Travis 也可以用 Docker,但是其整合的還不完整,基本上都要用 docker run 來執行 Script;Travis 也同樣提供 Stage Jobs 的概念,但仍然在 Beta 階段,我實際玩起來並沒有加速到反而變慢,所以最後選擇用 CircleCI 2.0。

CI 做了哪些事

a. Dependency Install

安裝 NPM 相依套件,全篇都以 Yarn 來作為 NPM Client 進行討論。

b. Bootstrap

使用 Lerna 進行 Monorepo 管理,其中最關鍵的步驟在於 Hoist Bootstrap,把所有子目錄的 node_modules 都提升到根目錄去,若彼此之間若有相依性會透過 Symbolic Link 關聯起來。最後把所有子目錄使用 Babel 轉為 CommonJS Module。

c. Lint

包含了 JavaScript Coding Style 的 Eslint、CSS Coding Style 的 Stylelint 以及少部分 Static Type Checker 的 Flow。

d. Unit Test

絕大部分都使用 Jest 做 Snapshot Testing。

e. Building Test

除了功能性 Unit Test 外,另一個重點是要測試頁面建置過程(Webpack)有沒有問題。

CircleCI 2.0 Workflows with GitHub

如上圖,將 Job 切分為上個段落的 a~e 個步驟,以往依序執行將會是這些片段時間的總和,大致會是 7 分鐘。

事實上,除了 a 與 b 是需要預先跑完才能往下執行外,其他步驟皆可獨立執行,因此可以採取 CircleCI 2.0 的 Workflows,預設免費可以同時跑 4 個 Job,所以切成四個恰恰好。如此一來,整體所需時間就會是 a + b + d 的總和,差不多三分半。

理論上,單純透過 Parallel Job Execution 就能加速兩倍左右。

CI 還做了哪些事

環境設定

最重要的就是開發時的版號能夠固定,這樣一旦發生錯誤時才能將範圍縮小。如果是 Travis 或 CircleCI 1.0 的設定方式是透過 YAML 設定 Node.JS 版號,但是像是 Yarn 的版號就比較麻煩,寫起來大概像是:

設定 Node.JS 與 Yarn 版號 (.travis.yml)

以往會需要自己寫些 Script 來判斷 Yarn 的環境,進行鎖版的動作。那如果是 CircleCI 2.0 則是使用 Docker Image,設定起來簡單很多:

# .circleci/config.yml
docker:
- image: node:8.2.1

可以直接使用官方維護的 Node Docker Image 即可,已經預設有把 Yarn 包在裡面。

Caching

Cache 絕對是 CI 的一大重點,在 Travis 時,可以透過簡單的目錄指定來設定哪些檔案要 Cache 起來:

# .travis.yml
cache:
yarn: true # Global .cache/yarn
directories:
- ~/.yarn # 上個段落安裝 yarn 鎖版用
- node_modules

雖然 CircleCI 2.0 變得稍微複雜一點,但是功能就更為強大了,例如這邊可以計算 Lockfile 的 Checksum 來指令要哪一包 Cache:

dependency-cache-{{ checksum "yarn.lock" }}

因為是 Docker Based,記得要把 Yarn Cache 目錄設在專案內:

yarn config set cache-folder .yarn-cache

這樣稍後才能把專案內的資料夾做 Cache:

# .circleci/config.yml
- save_cache:
key: yarn-cache-{{ checksum "yarn.lock" }}
paths:
- .yarn-cache

Workflows

在 CircleCI 2.0 的設計中會先把 Job 獨立設定好,而後才會在底下設定 Job 的執行順序與相依性,例如這個專案中會先執行完 a + b (Bootstrap),才會執行後續的 c、d、e:

# .circleci/config.yml
jobs:
- bootstrap
- lint:
requires:
- bootstrap
- test:
requires:
- bootstrap
- test-page:
requires:
- bootstrap

不嚴謹的實驗結果

Circle CI 1.0(左)與 2.0(右)每個 Job 所需時間減少 — CircleBuild Insights

Without cache:

Travis: 12 min
Circle2: 4 min

With cache:

Travis: 9.5 min
Circle2: 3.5 min 🚀

後記

雖然 Travis 與 CircleCI 各有優缺點,像要設定多個 Node.JS 環境來跑還是 Travis 的 Matrix 方便。他們都對於 OSS 非常的有善,基本上免費就很夠用,不如多設定幾個 CI 多一份保障。

下一步,可能會從根本的問題來處理,例如 Monorepo 在最外層設定一個 Test Runner 來跑 Unit Test 就好,減少啟動時間;另外 Lerna + Yarn 的方案之後可以考慮換成 Yarn 新版內建的 Workspace 功能,相信會有更好的整合。未來如果想看這部分的嘗試,可以關注我的 Medium。

本篇文章都沒有提及 CD 的部分,事實上我更推薦使用 Netlify 來做為前端專案的部署,在我之前的經驗中跑得比 Travis 或 CircleCI 還來的快。

Further Readings

  1. React Stack 開發體驗與優化策略
  2. Build A Web App in MediaTek

References

  1. CircleCI 2.0 DocumentsCircleci/Frontend Config
  2. 減少 node_modules 大小來加速部署 Node.js 專案

*完整的 Migrate PR 放在 MCS-Lite/mcs-lite,如果你喜歡這系列文章,關於 Michael 在 OSS 專案開發心得,別忘了可以點個 ❤️ 讓我知道喔!

--

--