この記事は Sansan Advent Calendar 2023 の13日目の記事です。および【R&D DevOps通信】の連載記事のひとつです。
こんにちは、研究開発部 Architectグループの藤岡です。 今回は部で運用しているCI/CDに関する取り組みについてお話しします。共通化のノウハウや、どういった種類のCI/CDを導入してコード品質を担保しているかといった話をしたいと思います。 そのまま使える実装例もあるので、是非参考にしてみてください。
目次
CI/CD共通化
以下のブログでも説明した通り、研究開発部では基本 GitHub Actions でCI/CDを組んでいます。 また、 reusable workflows という他のリポジトリの workflow を呼び出せる機能を使って、部内のCI/CDを共通化しています。
共通化することにより以下のような利点が得られます。
- 各リポジトリにCI/CDを導入するコストが低くなる
- 部内のworkflowの重複を避けられる
- バグ修正やバージョンの更新などが一括でできる
- 共通のナレッジを蓄積できる
reusable workflows の設定方法は簡単で、以下の設定をするだけで、Organization内の他のリポジトリから参照できるようになります。
リポジトリの GitHub Actions の設定を管理する - GitHub Docs
reviewdog による Pull Request へのコメント
reviewdog とは、各種静的解析の結果を GitHub(GitLab, Bitbucket にも対応している) の Pull Request にコメント形式で指摘してくれるツールです。 今までは Sider というコードレビューツールを使用していましたが、サービス終了に伴い reviewdog へ移行したという背景があります。
reviewdog は Pull Request 上で指摘してくれるので別画面に遷移する必要はなく、全て GitHub 上で完結できるため非常に使い勝手がよいです。
以下は black の解析結果を指摘する例です。
black はdiff形式で指摘してくれるため、 Sign off and commit suggestion
からそのままコミットできます。
導入しているCI/CD
次に部内で導入している共通化されたCI/CDについて紹介します。 CIは基本 reviewdog で指摘できるworkflowを導入しています。
PythonのCI
- flake8: 論理エラーやスタイルチェックを行うlinter
- mypy: 型チェックを行うlinter
- black: コードを整形するformatter
- isort: import文を自動で整理するformatter
- pytest: テストコードを実行
その他のCI
- Docker build: Dockerイメージが正常にビルドできるかをチェック
- shellcheck: シェルスクリプトの静的解析
- hadolint: Dockerfileの静的解析
- misspell: スペルミスのチェック
- detect-secrets: 機密性の高い文字列の検出
CD
- DockerイメージをビルドしてAWSのECRへpushする
実装例
実際にどのようなyamlファイルの記述で上記のCIを実現しているか、一例を示したいと思います。 Poetry を使っているPythonプロジェクトにおいて、black の解析結果を reviewdog により指摘する例です。
reusable workflows
.github/workflows/python-lint-with-poetry.yml
name: "Python Lint with Poetry" on: workflow_call: inputs: python_version: required: false type: string default: "3.10" description: "Pythonのバージョン" poetry_version: required: false type: string default: "1.7.0" description: "Poetryのバージョン" working_dir: required: false type: string default: . description: "pyproject.tomlがあるディレクトリ" dev_dependencies_group_name: required: false type: string default: "dev" description: "dev-dependencies のグループ名" permissions: contents: read checks: write pull-requests: write jobs: black: runs-on: ubuntu-latest timeout-minutes: 10 defaults: run: working-directory: ${{ inputs.working_dir }} steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Linter uses: ./.github/composite/setup-linter with: python_version: ${{ inputs.python_version }} poetry_version: ${{ inputs.poetry_version }} working_dir: ${{ inputs.working_dir }} dev_dependencies_group_name: ${{ inputs.dev_dependencies_group_name }} - name: black env: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | poetry run black --diff --quiet --check . \ | reviewdog -f="diff" \ -name="black" \ -f.diff.strip=0 \ -reporter="github-pr-review" \ -filter-mode="file" \ -fail-on-error="true" \ -level="warning"
reusable workflows として実装したい場合、workflow_call
を指定する必要があります。また、Pythonバージョンなどプロジェクトごとに異なる設定を inputs
として受け取るようにしています。
このジョブは2つのステップからなっており、1つ目のステップでは uses
で他のworkflowを指定しています。
uses: ./.github/composite/setup-linter
ここでは composite action というステップ単位で再利用可能な機能を使っています。 reusable workflows との主な違いはどの単位で利用できるかという点です。reusable workflows がworkflow単位なのに対し、composite action はステップ単位で再利用できる点が異なります(他のリポジトリから利用できる点は同じ)。 そのため、共通化した処理の前段か後段に何らかの処理を入れたい場合は composite action、そうでない場合は reusable workflows という使い分けになるかと思います。 actionの中身は後述します。
2つ目のステップでは、 black をdiff形式で実行し、その結果を reviewdog に渡しています。いくつか reviewdog のオプションを設定していますが、以下のオプションは特に重要です。
- reporter: どのような形式で結果を表示するか
- github-pr-review を指定することで Pull Request にコメントしてくれる
- filter-mode: どの範囲を検出するか
- added: 変更された差分のみ
- diff_context: Pull Request上の差分として表示される範囲
- file: 変更されたファイル全て
- nofilter: 全ファイル
- fail-on-error: 1つでもエラーがあった場合、失敗ステータスにするかどうか
- これを true にしておくと、以下のように Pull Reqeust 上からどのジョブが失敗しているか一目で確認できるので便利です。
composite action
.github/composite/setup-linter/action.yml
name: "Set up Linter" description: "Set up Linter" inputs: python_version: required: true description: "Pythonのバージョン" poetry_version: required: true description: "Poetryのバージョン" working_dir: required: true description: "pyproject.tomlがあるディレクトリ" dev_dependencies_group_name: required: true description: "dev-dependencies のグループ名" runs: using: "composite" steps: - name: Checkout uses: actions/checkout@v4 - name: Install Poetry shell: bash run: pipx install poetry==${{ inputs.poetry_version }} - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ inputs.python_version }} cache: "poetry" - name: Install dependencies shell: bash run: | poetry env use ${{ inputs.python_version }} poetry install --with ${{ inputs.dev_dependencies_group_name }} working-directory: ${{ inputs.working_dir }} - name: Set up Reviewdog uses: reviewdog/action-setup@v1 with: reviewdog_version: latest
ここでやっているのは、Python, Poetry, reviewdog のセットアップと poetry install
により依存関係をインストールしています。
また、actions/setup-python で cache: "poetry"
とすることで、poetry install
の結果をキャッシュしておくことができます。
これらの処理を composite action にしている理由は、black だけでなく flake8, mypy, isort のジョブでも同様のセットアップが必要なため、共通化することで重複を排除できるからです。 ちなみに、1つのジョブにまとめずそれぞれジョブを分けている理由は、どのCIが成功/失敗したのか一目で判断するためです。
reusable workflows を利用する例
name: "Python Lint" on: pull_request: types: [synchronize, opened] jobs: python-lint: uses: <reusable workflows を管理しているリポジトリ>/.github/workflows/python-lint-with-poetry.yml@v1 with: working_dir: app
上記の reusable workflows を、同一 Organization の別リポジトリから利用したい場合、このような workflow を書けばよいです。 この設定では、Pull Request をオープンした時とコミット&プッシュした時にCIが実行されます。 利用する側は非常にシンプルな記述になるのがわかりますね。
release-drafter によるリリース作業の簡易化
最後に、共通化したworkflowのリリースを release-drafter により簡易化する方法を紹介したいと思います。
release-drafter とは Draft のリリースノートを自動で生成するツールです。 このツールを使い、以下のようなリリースフローを組むことができます。
- mainブランチに対して Pull Request を作成する。
- Pull Request がマージされると自動的に Draft が生成される。
- Draft を確認し、問題なければ
Publish release
する。 - メジャーバージョンのタグが更新されリリース完了。
4 のタグ更新により、利用する側で @v1
のようにメジャーバージョンを指定している場合、何も変更しなくてもリリース内容が反映されます。
これらを実現するために、2つのworkflowが必要なのでそれぞれ解説します。
自動で Draft を生成する
Pull Request の mainブランチへのマージをトリガーとして、自動的に Draft が生成される workflow は以下で実現できます。
name: Release Drafter on: push: branches: - main jobs: update_release_draft: runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
また、release-drafter の設定ファイル .github/release-drafter.yml
も必要となります。
name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' template: | # What's Changed $CHANGES **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION categories: - title: 'Breaking' label: 'breaking' - title: 'New' label: 'feature' - title: 'Bug Fixes' label: 'bug' - title: 'Maintenance' label: 'maintenance' - title: 'Documentation' label: 'documentation' - title: 'Other changes' - title: 'Dependency Updates' label: 'dependencies' collapse-after: 5 version-resolver: major: labels: - 'breaking' minor: labels: - 'feature' patch: labels: - 'bug' - 'maintenance' - 'documentation' - 'dependencies' exclude-labels: - 'skip-changelog'
template:
にリリースノートのテンプレートを書き、 categories:
を設定すると、ラベルのついた Pull Request を分類して表示できます。
例えば、 feature
ラベルと bug
ラベルがついた Pull Request をマージし、 release-drafter により Draft を生成すると以下のようになります。
カテゴリごとに分けられているので非常に見やすいですね。
version-resolver:
ではラベルによりタグの Semantic Versioning を制御でき、今回の設定では以下のような挙動になります。
breaking
ラベルをつけると major version が上がるfeature
ラベルをつけると minor version が上がる- それ以外のラベルをつけると patch version が上がる
どういった場合にどのバージョンを上げるか暗黙的だった部分が、この設定ファイルで明示できるので便利です。
Draft リリース時にメジャータグ更新
Draft をリリースするとタグが作成されるのですが、それをトリガーにして以下の workflow を走らせることで、メジャータグを上書き更新できリリース完了となります。
name: Release on: push: tags: - v*.*.* jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: version id: version run: | tag=${GITHUB_REF/refs\/tags\//} version=${tag#v} major=${version%%.*} echo "tag=${tag}" >> $GITHUB_OUTPUT echo "version=${version}" >> $GITHUB_OUTPUT echo "major=${major}" >> $GITHUB_OUTPUT - name: force update major tag run: | git tag v${{ steps.version.outputs.major }} ${{ steps.version.outputs.tag }} -f git push origin refs/tags/v${{ steps.version.outputs.major }} -f
まとめ
今回紹介した内容をまとめると以下のようになります。
- reusable workflows で Organization 共通の workflow を作ることができる
- composite action でステップ単位の処理を共通化できる
- reviewdog で簡単に Pull Request へコメントするCIを作ることができる
- release-drafter でリリースノートのテンプレ作成とバージョン管理が容易になる
参考になるものがあれば是非取り入れてみてください。