Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【GitHub Actions】reviewdog + reusable workflows によるCI/CD共通化

この記事は Sansan Advent Calendar 2023 の13日目の記事です。および【R&D DevOps通信】の連載記事のひとつです。

こんにちは、研究開発部 Architectグループの藤岡です。 今回は部で運用しているCI/CDに関する取り組みについてお話しします。共通化のノウハウや、どういった種類のCI/CDを導入してコード品質を担保しているかといった話をしたいと思います。 そのまま使える実装例もあるので、是非参考にしてみてください。

目次

CI/CD共通化

以下のブログでも説明した通り、研究開発部では基本 GitHub Actions でCI/CDを組んでいます。 また、 reusable workflows という他のリポジトリの workflow を呼び出せる機能を使って、部内のCI/CDを共通化しています。

buildersbox.corp-sansan.com

共通化することにより以下のような利点が得られます。

  • 各リポジトリにCI/CDを導入するコストが低くなる
  • 部内のworkflowの重複を避けられる
  • バグ修正やバージョンの更新などが一括でできる
  • 共通のナレッジを蓄積できる

reusable workflows の設定方法は簡単で、以下の設定をするだけで、Organization内の他のリポジトリから参照できるようになります。

リポジトリの GitHub Actions の設定を管理する - GitHub Docs

reviewdog による Pull Request へのコメント

reviewdog とは、各種静的解析の結果を GitHub(GitLab, Bitbucket にも対応している) の Pull Request にコメント形式で指摘してくれるツールです。 今までは Sider というコードレビューツールを使用していましたが、サービス終了に伴い reviewdog へ移行したという背景があります。

github.com

reviewdog は Pull Request 上で指摘してくれるので別画面に遷移する必要はなく、全て GitHub 上で完結できるため非常に使い勝手がよいです。 以下は black の解析結果を指摘する例です。 black はdiff形式で指摘してくれるため、 Sign off and commit suggestion からそのままコミットできます。

reviewdogの指摘例

導入している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 上からどのジョブが失敗しているか一目で確認できるので便利です。

fail-on-errorの説明

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-pythoncache: "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 により簡易化する方法を紹介したいと思います。

github.com

release-drafter とは Draft のリリースノートを自動で生成するツールです。 このツールを使い、以下のようなリリースフローを組むことができます。

  1. mainブランチに対して Pull Request を作成する。
  2. Pull Request がマージされると自動的に Draft が生成される。
  3. Draft を確認し、問題なければ Publish release する。
  4. メジャーバージョンのタグが更新されリリース完了。

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 を生成すると以下のようになります。

release-drafter の説明

カテゴリごとに分けられているので非常に見やすいですね。

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 でリリースノートのテンプレ作成とバージョン管理が容易になる

参考になるものがあれば是非取り入れてみてください。

© Sansan, Inc.