ぺのめも

Web開発を勉強中。学んだことや思ったことなど

個人開発プロジェクトの開始からCI構築までにやったこと( GitHub Actions )

はじめに

個人でRuby on RailsのWEBアプリ開発に着手しています。
初めてのCI環境構築で色々調べながら取り組んだので、次回はスッとできるよう、記録を残しておきます。

やったこと

ざっくり以下のようなステップを踏みました。

  • ブランチ運用の設計
  • CIの設計
  • CIの実装
    • テストを動かす
    • lintを動かす
  • CodeQLの設定
  • 必要なチェックをパスしないとPRをマージできないようにする
  • tagprを導入する

ブランチ運用の設計

そもそもCIの要件を整理するにあたり、ブランチの運用方法を決めました。
今回は、

  • 開発は基本的に一人で実施するので、できるだけシンプルにしたい
  • ローカル環境で確認後に即本番リリースはちょっと不安なので、ステージング環境で確認などしてからリリースできるようにしておきたい

という背景により、GitHub flowをベースに、下記のような運用にすることにします。

  • mainブランチと、各featureブランチのみで運用
    • featureブランチはmainへのマージ後に削除
  • mainブランチにfeatureブランチがマージされたら、ステージング環境へデプロイ
  • mainブランチにタグを打ったら、本番環境へデプロイ
    • どのコミットまでをリリースに含めるかを決めたら、タグを打つまでは追加でマージしないようにする
---
title: Git branch 運用イメージ
---
gitGraph
   commit
   branch feature1
   branch feature2
   checkout feature1
   commit
   commit
   checkout main
   merge feature1
   checkout feature2
   commit
   checkout main
   merge feature2
   branch feature3
   checkout feature3
   commit
   checkout main
   merge feature3
   commit tag: "v1.0.0"

CIの設計

上記の前提のうえで、CIをどのようにしたいかを(CDも含め)検討しました。

CIのチェックに含めるもの

  • lintの実行
  • テストの実行
  • CodeQLの実行(※GitHubが提供している、コードのセキュリティチェック)

やりたいこと

  • featureブランチからmainへのPRを作成した時
    • CIが動き、パスしなければPRはマージできない
  • featureブランチにコミットをpushした時
    • CIが動き、パスしなければPRはマージできない
  • mainブランチにfeatureブランチをマージした時
    • リリース用のブランチ・PRを作成(main←リリースブランチ)
      • リリースブランチは、機能の追加はせずにリリース用の変更(CHANGELOGの追記など)だけを行うブランチ
    • リリースブランチに対してCIが動き、パスしなければPRはマージできない
  • mainブランチにリリースブランチをマージした時
    • リリースバージョンのtagを打つ
    • tagが打たれたバージョンのmainを本番にデプロイする

ツール

Publicリポジトリであれば上限なく無料で利用でき、公式ドキュメントも豊富だったため、GitHub Actionsを利用することにしました。

docs.github.com

詳しくは上記の公式ドキュメントを読む必要がありますが、GitHub Actionsの基本的な概念は以下のとおりです。

  • ワークフロー
    • 1 つ以上のジョブを実行する自動化プロセス。リポジトリ内の .github/workflows 配下にYAML ファイルを作成して定義する。
  • イベント
    • ワークフローの実行をトリガーするアクティビティ。リモートブランチへのpush、PRの作成など。
  • ジョブ
  • アクション
    • GitHub Actions用に定義された、再利用可能なジョブ。GitHub Marketplaceにアクションが色々公開されている。独自で作ることもできる。
  • ランナー
    • ワークフローを実行するサーバー。

CIの設定ファイル(完成版)

CIのワークフローを定義していきます。こちらの記事を参考にしました。

zenn.dev

最終的なコードは下記になります。(クリックすると開きます)

========

github/workflows/ci.yml

name: ci

on:
  pull_request:
    branches: [ main ]

jobs:
  ci:
    runs-on: ubuntu-latest
    container:
      image: ruby:3.1.3
      env:
        RAILS_ENV: test
    steps:
      - name: Check out source code
         uses: actions/checkout@v3
      - name: Cache node modules
         uses: actions/cache@v3
         env:
           cache-name: cache-node-modules
         with:
           path: ~/.npm
           key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-build-${{ env.cache-name }}-
             ${{ runner.os }}-build-
             ${{ runner.os }}-

      - name: Cache bundle gems
         uses: actions/cache@v3
         with:
           path: vendor/bundle
           key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
           restore-keys: |
             ${{ runner.os }}-gem-
             ${{ runner.os }}-

      - name: Install Node.js
         uses: actions/setup-node@v3
         with:
           node-version: '19.4'

      - name: Npm install
         run: npm install

      - name: Install Bundler
         run: gem install bundler --no-document -v $(grep "BUNDLED WITH" -1 Gemfile.lock | tail -n 1)

      - name: Bundle install
         run: |
           bundle config set --local path 'vendor/bundle'
           bundle install --jobs 4 --retry 3

      - name: Run rubocop
         run: bundle exec rubocop

      - name: Run lint
         run: npm run lint

      - name: Run tests
         run: bundle exec rspec

========

ワークフローの各部分について

上記ファイルの各部分の内容について、順に見ていきます。

実行のトリガー

実行のトリガーとなるイベントは、mainブランチへのPull requestが作成あるいは更新された時としています。
なおイベントの詳細については、下記のドキュメントに一覧が記載されているので参考になりました。

docs.github.com

name: ci

on:
  pull_request:
    branches: [ main ]

依存ライブラリのキャッシュ

node-modulesとRuby gemをキャッシュしておくことで、ワークフローの実行を速くすることができます。

      - name: Cache node modules
         uses: actions/cache@v3
         env:
           cache-name: cache-node-modules
         with:
           path: ~/.npm
           key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-build-${{ env.cache-name }}-
             ${{ runner.os }}-build-
             ${{ runner.os }}-

      - name: Cache bundle gems
         uses: actions/cache@v3
         with:
           path: vendor/bundle
           key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
           restore-keys: |
             ${{ runner.os }}-gem-
             ${{ runner.os }}-
  • key
    • ハッシュ保存時に作成されるキー。検索時にも使われる。
  • path
    • キャッシュを保存・復元するときのパス。
  • restore-keys
    • key一致するキャッシュがない場合に探しにいくキーです。optionalですが、できるだけ多くのキャッシュヒットを得るためには設定しておくとよいようです。

docs.github.com

実際に試してみると、2回目の実行時は確かに速くなっていました。
←1回目 2回目→

Nodeのインストール・npm installの実行

Nodeのインストールには、actions/setup-nodeを使っています。

      - name: Install Node.js
         uses: actions/setup-node@v3
         with:
           node-version: '19.4'

      - name: Npm install
         run: npm install

このアクションを使うことで、Node.jsがキャッシュされると思ったのですが、現時点ではバージョン19.xの場合はキャッシュできないようです。

peno022.hatenablog.com

下記のようにNodeのインストールコマンドを直接実行した場合と比べると、10秒→5秒とactions/setup-nodeを使ったほうが速くなったので、19.4が対応されるのを待ちつつ、このまま使うことにしました。

      # 直接実行した版
      - name: Install Node.js
         run: |
           curl -sL https://deb.nodesource.com/setup_19.x | bash -
           apt-get install -y nodejs

Bundlerのインストール・bundle installの実行

      - name: Install Bundler
         run: gem install bundler --no-document -v $(grep "BUNDLED WITH" -1 Gemfile.lock | tail -n 1)

      - name: Bundle install
         run: |
           bundle config set --local path 'vendor/bundle'
           bundle install --jobs 4 --retry 3

gem installのオプション補足:

  • --no-document
    • gemに関するドキュメントの生成をスキップ。ライブラリ本体のダウンロードにとどめたほうが実行が速くなるため。
  • -v
    • bundlerのバージョンを指定。
    • grep "BUNDLED WITH" -1 Gemfile.lock | tail -n 1で、Gemfile.lockで指定しているBundlerバージョンを取得。

Command Reference - RubyGems Guides

bundle installのオプション補足:

  • --jobs
    • ダウンロード・インストールを並行して行うジョブ数を指定できる。

lintの実行

今回のプロジェクトでは、Rubocopとeslint、prettierを利用しています。

      - name: Run rubocop
         run: bundle exec rubocop

      - name: Run lint
         run: npm run lint

npm run lintの実行内容については、プロジェクトのpackage.jsonで指定しています。

// package.json

 "scripts": {
    // 中略
    "lint": "run-p lint:*",
    "lint:eslint": "eslint 'app/javascript/**/*.js' --max-warnings=0",
    "lint:prettier": "prettier app/javascript/**/*.js --check"
  },

テストの実行

今回のプロジェクトでは、RSpecを利用しています。
現段階では、ユニットテストの実行のみを確認しています。
システムテストの実行にはブラウザのインストールが必要など、実行するテストによって追加の設定が必要になります。

      - name: Run tests
         run: bundle exec rspec

CodeQLの設定

CodeQLのワークフローは、リポジトリ設定の「CodeQL analysis」をAdvancedで有効化することで、GitHubが作成してくれます。
今回はワークフローの内容は変更していません。

リポジトリ設定については、こちらの記事に詳細を記載しています。
peno022.hatenablog.com

ワークフローのymlファイルは下記になります。(クリックすると開きます)
========

.github/workflows/codeql.yml

# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ "main" ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ "main" ]
  schedule:
    - cron: '24 13 * * 6'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'javascript', 'ruby' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
        # Use only 'java' to analyze code written in Java, Kotlin or both
        # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

    steps:
    - name: Checkout repository
      uses: actions/checkout@v3

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v2
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.

        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
        # queries: security-extended,security-and-quality


    # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v2

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun

    #   If the Autobuild fails above, remove it and uncomment the following three lines.
    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.

    # - run: |
    #   echo "Run, Build Application using script"
    #   ./location_of_script_within_repo/buildscript.sh

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v2
      with:
        category: "/language:${{matrix.language}}"

========

指定したチェックをパスしないとPRをマージできないようにする

Repository Settings の Code and automation > Branches の画面を開きます。
今回はmainブランチへのマージを制限したいので、Branch protection rulesのmainの設定を編集します。

Require status checks to pass before merging を有効にし、マージ時に必須にしたいジョブを検索・選択します。

Require status checks to pass before merging を有効にする
「Require status checks to pass before merging」を有効にする

この設定により、CIがパスしていない状態だと、下記のようにPRのマージができないよう警告表示になります。

リリース用PRの自動作成をする

mainブランチ上のタグでリリースバージョンを管理したいため、tagprというアクションを導入しました。
導入方法については、こちらの記事をご参照ください。
(長くなったので別記事に切り出しました。。)

peno022.hatenablog.com

完了!

これで、やりたいこととして最初に設定したことは実現できました。

再掲:

  • featureブランチからmainへのPRを作成した時
    • CIが動き、パスしなければPRはマージできない
  • featureブランチにpushしてコミットを追加した時
    • CIが動き、パスしなければPRはマージできない
  • featureブランチをmainへマージした時
    • リリース用のPRを作成(リリースブランチからmain)
    • リリースブランチに対してCIが動き、パスしなければPRはマージできない
  • リリース用のPRをmainへマージした時
    • リリースバージョンのtagを打つ
    • tagが打たれたmainを本番にデプロイする

GitHub Actionsって何?という状態からだったのでけっこう苦労しましたが、次回からはスッとできそうです。