用Github Actions 持續整合 Ruby on Rails
Tags: githubactions, chinese
Published: 2021-06-21 16:37:36

最近研究了一下用 Github Actions 做 Rails 的 CI ，分享一下經驗 : )

Github Actions 是 Github 的自動化工具。
Github Actions 只要在你的專案根目錄新增 `.github/workflows` ，再新增任意名稱的 Yaml 檔。
Git push 到 Github 後，即會根據你寫的 workflow 的內容自動執行了。

可以直接看下面分享的 YAML 檔，不過建議還是先看一下 Github Action 的文檔 [Introduction to GitHub Actions - GitHub Docs](https://docs.github.com/en/actions/learn-github-actions/introduction-to-github-actions) ，會比較有概念喔(常常更新的也滿快的)

先分享 `.github/workflows/ci.yml`，後面會再說明每一列是代表什麼意思
```yaml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.7.2
          bundler-cache: true
      - name: Install Node
        uses: actions/setup-node@v2
        with:
          node-version: '12.16.x'
      - name: Restore cached ./node_modules
        uses: actions/cache@v2
        with:
          path: ./node_modules
          key: ${{ runner.os }}-yarn-lock-${{ hashFiles('./yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-lock-
      - name: Yarn Install
        run: yarn install
  test:
    needs: build
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8
        ports: ['3306:3306']
        env:
          MYSQL_ROOT_PASSWORD: 'my-root-pw'
          MYSQL_DATABASE: test_db
          MYSQL_USER: username_you_like
          MYSQL_PASSWORD: password_you_like
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
      redis:
        image: redis
        ports: ['6379:6379']
        options: --entrypoint redis-server
    steps:
      - uses: actions/checkout@v1
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.7.2
          bundler-cache: true
      - name: Install Node
        uses: actions/setup-node@v2
        with:
          node-version: '12.16.x'
      - name: Restore cached ./node_modules
        uses: actions/cache@v2
        with:
          path: ./node_modules
          key: ${{ runner.os }}-yarn-lock-${{ hashFiles('./yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-lock-
      - name: Yarn Install
        run: yarn install
      - name: Prepare Database
        env:
          RAILS_ENV: test
          RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
        run: bundle exec rails db:prepare
      - name: Run tests
        env:
          REDIS_URL: redis://localhost:6379/1
          RAILS_ENV: test
          RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
        run: |
          bundle exec rspec --format RspecJunitFormatter --out ./reports/rspec.xml
      - name: Publish Test Report
        uses: mikepenz/action-junit-report@v2
        with:
          report_paths: './reports/rspec.xml'
```

說明版
```yaml
# 此 workflow 的名稱，可任意取，到時會出現在 Github Actions 中
name: CI
# on 是控制何時要執行這個 workflow
# * push: 有 commit  push 時
# * pull_request: PR 更新時
on: [push, pull_request]
# 一個 workflow 可以有很多jobs 組成，它們通常是同時(平行)執行的，
#不過可以設定先後順序，下面一點會提到。
#job 的名稱可任意取名，我這裡叫它 build
# `runs-on` 是 job 要在什麼 OS 上執行， github 上的 Github Actions 好像得用 ubuntu-latest，我也是照教學沿用了
jobs:
  build: 
    runs-on: ubuntu-latest 
    # `step` 是 Github Actions 的最小單位
    # steps 裡每個 step 可以執行一或多個指令或一個 Action
    # 要執行指令，則使用 `run`
    # `run: echo "hello world!"`
    # 要執行 Action，用 `uses`
    # `uses: actions的名字`
    # 每個 step 的 name 並不是必填，但填了就像註解一樣，方便理解
    steps:
      # Action 其實就預先寫好的腳本
      # GitHub Marketplace 上有一堆，可以想像是想把自動化的腳本當成在 App Store 上賣
      # [GitHub Marketplace · Actions to improve your workflow · GitHub](https://github.com/marketplace?type=actions)
      #`actions/checkout@v1` 就是一個常用的 Action，可以把你的 git repository 下載下來
      - uses: actions/checkout@v1

      # ruby/setup-ruby@v1 是用來安裝 Ruby 的 Action
      # 下載的 ruby-version: 2.7.2 是要裝 2.7.2 的 Ruby
      # bundler-cache: true 是要快取 bundler 下載的 gem，下面再說明快取
      # 基本上這些 Action 都可以在 Github Marketplace 上查到用法
      # [Setup Ruby, JRuby and TruffleRuby · Actions · GitHub Marketplace · GitHub](https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby)
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.7.2
          bundler-cache: true
      # 同上，安裝 Node
      - name: Install Node
        uses: actions/setup-node@v2
        with:
          node-version: '12.16.x'
      # `actions/cache@v2` 是專門來做快取的 Action
      # 這裡寫 path: ./node_modules 就是要把 ./node_modules 下的檔案全 cache 起來
      # 快取的結果會用 key 的設定去命名。
      # 這個 action 會先依照 restore-keys 去找可以回復的快取
      # 最後的 step 純粹就是跑 yarn install

      # 如果光看這個 順序應該有人會疑惑：
      # 快取的 action 是放在 yarn install 前，或更上方 ruby 的部分有 bundle install 前，
      # 在安裝依賴前，先「讀取」快取好理解，
      # 但重要的是「存」快取的時機怎麼沒看到？
      # 執行一次就知道，這類快取的 Action 都有掛個動作在 PostJob 的 callback，
      # 當這個 job 結束後會把該快取的 path 存起來

      # 另外，每個倉庫有 5GB 的快取空間，正常來說應該是用不完啦。
      # 所以 gems, npm 這種安裝的程式庫都把它們快取起來吧
      - name: Restore cached ./node_modules
        uses: actions/cache@v2
        with:
          path: ./node_modules
          key: ${{ runner.os }}-yarn-lock-${{ hashFiles('./yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-lock-
      - name: Yarn Install
        run: yarn install
  # 這是第二個 job ，我命名為 test，因為準備要跑 rspec 了
  # 上面說 job 其實是平行執行的，`needs` 可以控制先後順序，needs: build 的意思就是 build 跑完才會跑 test
  # 雖然以目前這個 workflow 來看，其實可以把steps 全寫在同一個 job ，沒什麼差別。
  # 但寫成這樣，可以方便未來加入不同類的 test，比如 js 的 test， capybara 的 test，可以在 build 後，所有的 test 同時執行。
  test:
    needs: build
    runs-on: ubuntu-latest
    # `services` 可以用 docker image 架起需要用的服務，設定好 port  mapping
    # 我這裡架了 mysql 和 redis
    # 那些 env 是去 DockerHub 上去找官方的 image 的說明才知道有什麼可以加的
    # 順帶一提，CI 時 `config/database.yml` 可以兩種處理方式
    # * 直接加入 git，內容再用 ENV 去替換
    # * 新增一個 database.yml.ci ，在 workflow 裡加一個 step 去 `run: cp config/database.yml.ci config/database.yml`
    services:
      mysql:
        image: mysql:8
        ports: ['3306:3306']
        env:
          MYSQL_ROOT_PASSWORD: 'my-root-pw'
          MYSQL_DATABASE: test_db
          MYSQL_USER: test_user
          MYSQL_PASSWORD: test_pw
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
      redis:
        image: redis
        ports: ['6379:6379']
        options: --entrypoint redis-server
    # 下面這段到 yarn install 跟 build 是做完全一模一樣的事，執行時會直接取 build 快取的結果，所以很快就會跑完。
    # 看起來很冗，不過如果未來可以用 YAML 的 Anchor 功能，這一塊可以直接寫成一個可複用的 block，
    # 目前就直接重複吧！
    steps:
      - uses: actions/checkout@v1
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.7.2
          bundler-cache: true
      - name: Install Node
        uses: actions/setup-node@v2
        with:
          node-version: '12.16.x'
      - name: Restore cached ./node_modules
        uses: actions/cache@v2
        with:
          path: ./node_modules
          key: ${{ runner.os }}-yarn-lock-${{ hashFiles('./yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-lock-
      - name: Yarn Install
        run: yarn install
      # 如果要開始跑 rails 的指令時，要帶一些`環境變數`時，例如我這樣會用 RAILS_ENV 跟 RAILS_MASTER_KEY
      # 是密碼類的字串可以存在Github倉庫 >Settings>Secrets 裡，workflow 裡可以直接 `${{ secrets.RAILS_MASTER_KEY }}` 去讀取
      - name: Prepare Database
        env:
          RAILS_ENV: test
          RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
        run: bundle exec rails db:prepare
      # 這邊是跑 rspec
      # 有裝 rspec_junit-formatter ，所以可以產生一個 XML 檔紀錄測試結果
      - name: Run tests
        env:
          REDIS_URL: redis://localhost:6379/1
          RAILS_ENV: test
          RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
        run: bundle exec rspec --format RspecJunitFormatter --out ./reports/rspec.xml
      - name: Publish Test Report
        uses: mikepenz/action-junit-report@v2
        with:
          report_paths: './reports/rspec.xml'
```
最後用 `mikepenz/action-junit-report@v2`
去讀取 XML 的測試結果並秀出在頁面上
![junit xml](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3axxpeoomj83ifzwlc3u.png)
 
---
 
## 心得
我覺得自動化最煩的就是一堆小細節要顧，
比如說也可以完全不快取硬讓它跑，但就會很慢。
像 CircleCI 的 `orb` 就是把每個語言、框架常用的自動化指令整理起來變成一組指令集。
但 Github Actions 直接更進一步讓社群製作指令集，並準備好市集，方便搜尋跟分享，野心勃勃要幹掉其它 CI。

整體上還滿容易用，連 Rails 都能輕易導入(回想一下把 Rails 裝進 Docker 的夜晚)，滿推薦試試看的。
而且Github(微軟)滿佛的，每個月給2000小時，個人練習或小型專案應該是用不完啦，可以去帳號 Billing & Plans 下查用量。
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/radcdxyrqdrql2qohlu4.png)
 


## 參考資料
* [GitHub Actions with Ruby on Rails: Setting up Continuous Integration (Example) | GoRails](https://gorails.com/episodes/github-actions-continuous-integration-ruby-on-rails?autoplay=1)
* [Introduction to GitHub Actions - GitHub Docs](https://docs.github.com/en/actions/learn-github-actions/introduction-to-github-actions) ，會比較有概念喔(常常更新的也滿快的)