はじめに
個人でRuby on RailsのWEBアプリ開発に着手しています。
初めてのCI環境構築で色々調べながら取り組んだので、次回はスッとできるよう、記録を残しておきます。
- はじめに
- やったこと
- ブランチ運用の設計
- CIの設計
- CIの設定ファイル(完成版)
- ワークフローの各部分について
- CodeQLの設定
- 指定したチェックをパスしないとPRをマージできないようにする
- リリース用PRの自動作成をする
- 完了!
やったこと
ざっくり以下のようなステップを踏みました。
- ブランチ運用の設計
- 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はマージできない
- リリース用のブランチ・PRを作成(main←リリースブランチ)
- mainブランチにリリースブランチをマージした時
- リリースバージョンのtagを打つ
- tagが打たれたバージョンのmainを本番にデプロイする
ツール
Publicリポジトリであれば上限なく無料で利用でき、公式ドキュメントも豊富だったため、GitHub Actionsを利用することにしました。
詳しくは上記の公式ドキュメントを読む必要がありますが、GitHub Actionsの基本的な概念は以下のとおりです。
- ワークフロー
- イベント
- ワークフローの実行をトリガーするアクティビティ。リモートブランチへのpush、PRの作成など。
- ジョブ
- ワークフロー内の一連のステップ。
- 実行するシェルスクリプトまたはアクション。
- アクション
- ランナー
- ワークフローを実行するサーバー。
CIの設定ファイル(完成版)
CIのワークフローを定義していきます。こちらの記事を参考にしました。
最終的なコードは下記になります。(クリックすると開きます)
========
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が作成あるいは更新された時としています。
なおイベントの詳細については、下記のドキュメントに一覧が記載されているので参考になりました。
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ですが、できるだけ多くのキャッシュヒットを得るためには設定しておくとよいようです。
実際に試してみると、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の場合はキャッシュできないようです。
下記のように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 を有効にし、マージ時に必須にしたいジョブを検索・選択します。
この設定により、CIがパスしていない状態だと、下記のようにPRのマージができないよう警告表示になります。
リリース用PRの自動作成をする
mainブランチ上のタグでリリースバージョンを管理したいため、tagpr
というアクションを導入しました。
導入方法については、こちらの記事をご参照ください。
(長くなったので別記事に切り出しました。。)
完了!
これで、やりたいこととして最初に設定したことは実現できました。
再掲:
- featureブランチからmainへのPRを作成した時
- CIが動き、パスしなければPRはマージできない
- featureブランチにpushしてコミットを追加した時
- CIが動き、パスしなければPRはマージできない
- featureブランチをmainへマージした時
- リリース用のPRを作成(リリースブランチからmain)
- リリースブランチに対してCIが動き、パスしなければPRはマージできない
- リリース用のPRをmainへマージした時
- リリースバージョンのtagを打つ
- tagが打たれたmainを本番にデプロイする
GitHub Actionsって何?という状態からだったのでけっこう苦労しましたが、次回からはスッとできそうです。