🐕

`pre-commit`を`Github Actions`で実行する

2022/04/30に公開

はじめに

この記事は、cookiecutter-djangoで使用されているpre-commitGithub Actionsの動作を理解するための記事です。
pre-commitを用いて、コードの整形や型チェックを行えます。それをGithubActionpre-commitを実行することで環境構築の必要をなくします。

サンプルリポジトリ
https://github.com/ikura1/test-pre-commit

pre-commitの内容を読む

GithubActionsで実行させる前にpre-commitで何が実行されるようになっているのか読んでいきます。

exclude: "^docs/|/migrations/"
default_stages: [commit]

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.2.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml

  - repo: https://github.com/psf/black
    rev: 22.3.0
    hooks:
      - id: black

  - repo: https://github.com/PyCQA/isort
    rev: 5.10.1
    hooks:
      - id: isort

  - repo: https://github.com/PyCQA/flake8
    rev: 4.0.1
    hooks:
      - id: flake8
        args: ["--config=setup.cfg"]
        additional_dependencies: [flake8-isort]

# sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date
ci:
  autoupdate_schedule: weekly
  skip: []
  submodules: false

実行されるコマンド

repos内のコマンドが順に実行されていくので下記の様になっています。

  • pre-commit-hooks
    • trailing-whitespace
      • 行末のスペース削除
      • 空行の削除
    • end-of-file-fixer
      • ファイルの末尾を空行に変更
    • check-yaml
      • yaml ファイルの構文チェック
  • black
    • コードの整形
  • isort
    • import ライブラリの整形
  • flake8
    • コードのスタイルチェック

その他

  • exclude
    • 除外されるファイルパターンの定義
    • ここでは、ドキュメントフォルダーと DB のマイグレーションフォルダーが指定されている
  • default_stages
    • 実行する段階の定義
    • hooks の実行はcommit時に制限されている
  • ci
    • pre-commitci向け設定
    • autoupdate_schedule
      • hook のバージョンを更新するためのスケジュール
      • weeklyが指定されており、毎週更新される
    • skip
      • スキップする hookID のリストを指定
      • スキップはしないように設定されている
    • submodules
      • サブモジュールの再帰的チェックアウトの設定
      • false、デフォルトもfalse

環境構築

まずはローカル環境でpre-commitが実行できるように環境を構築します。

# Code quality
# ------------------------------------------------------------------------------
flake8==4.0.1  # https://github.com/PyCQA/flake8
flake8-isort==4.1.1  # https://github.com/gforcada/flake8-isort
coverage==6.3.2  # https://github.com/nedbat/coveragepy
black==22.3.0  # https://github.com/psf/black
pylint-django==2.5.3  # https://github.com/PyCQA/pylint-django
pre-commit==2.18.1  # https://github.com/pre-commit/pre-commit

上記はローカル環境用のrequirements.txtでのコード品質部分になります。

下記の 2 つはpre-commitで使用しないのでインストールしませんでした。

  • coverage
  • pylint-django

pre-commitの実行テスト

整形が必要な下記のようなコードを作成し、pre-commitを動作させます。
pre-commit installしたあとの確認になります。GithubActionsでの動作のときにはpre-commit installはしていない状況で動作確認を行います。

hoge.py
import random
from fizzbuzz import fizzbuzz as fb
import click
import os


@click.command()
@click.option('--num','-n',default=lambda: random.randint(1, 100),type=int,help='Number of loop')
def fizzbuzz(num: int):
    fb(num)


if __name__=='__main__':
  fizzbuzz()

pre-commit動作ログ

$ >git commit -m "feat: isortなどが動作するコードに変更"
[WARNING] Unstaged files detected.
[INFO] Stashing unstaged files to C:\Users\ikura\.cache\pre-commit\patch1651315960-10368.
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...........................................(no files to check)Skipped
black....................................................................Failed
- hook id: black
- files were modified by this hook

reformatted hoge.py

All done! \u2728 \U0001f370 \u2728
1 file reformatted, 1 file left unchanged.

isort....................................................................Failed
- hook id: isort
- files were modified by this hook

Fixing C:\Users\ikura\repos\test-pre-commit\hoge.py

flake8...................................................................Passed
[INFO] Restored changes from C:\Users\ikura\.cache\pre-commit\patch1651315960-10368.

$ >

pre-commit実行後のコード

hoge.py
import random

import click

from fizzbuzz import fizzbuzz as fb


@click.command()
@click.option(
    "--num",
    "-n",
    default=lambda: random.randint(1, 100),
    type=int,
    help="Number of loop",
)
def fizzbuzz(num: int):
    fb(num)


if __name__ == "__main__":
    fizzbuzz()

Github Action部分を読む

name: CI

# Enable Buildkit and let compose use it to speed up image building
env:
  DOCKER_BUILDKIT: 1
  COMPOSE_DOCKER_CLI_BUILD: 1

on:
  pull_request:
    branches: ["master", "main"]
    paths-ignore: ["docs/**"]

  push:
    branches: ["master", "main"]
    paths-ignore: ["docs/**"]

concurrency:
  group: ${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  linter:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code Repository
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v3
        with:
          python-version: "3.9"
          cache: pip
          cache-dependency-path: |
            requirements/base.txt
            requirements/local.txt

      - name: Run pre-commit
        uses: pre-commit/action@v2.0.3

  # With no caching at all the entire ci process takes 4m 30s to complete!
  pytest:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Code Repository
        uses: actions/checkout@v3

      - name: Build the Stack
        run: docker-compose -f local.yml build

      - name: Run DB Migrations
        run: docker-compose -f local.yml run --rm django python manage.py migrate

      - name: Run Django Tests
        run: docker-compose -f local.yml run django pytest

      - name: Tear down the Stack
        run: docker-compose -f local.yml down

jobsの後半pytest部分は無視する。読む分には前半部分で十分なのと簡易の動作確認としては、テスト部分の動作検証しにくい。

実行回りの設定

on:
  pull_request:
    branches: ["master", "main"]
    paths-ignore: ["docs/**"]

  push:
    branches: ["master", "main"]
    paths-ignore: ["docs/**"]

concurrency:
  group: ${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

実行するタイミング(on)
プルリクエストとプッシュ時にmastermainブランチで実行する。またdocsは除外

並行性(concurrency)
変更実行を許さず、同時の場合はプルリクのみ実行

実行部分

linter:
  runs-on: ubuntu-latest
  steps:
    - name: Checkout Code Repository
      uses: actions/checkout@v3

    - name: Set up Python
      uses: actions/setup-python@v3
      with:
        python-version: "3.9"
        cache: pip
        cache-dependency-path: |
          requirements/base.txt
          requirements/local.txt

    - name: Run pre-commit
      uses: pre-commit/action@v2.0.3
  • actions/checkout@3
    • コードのチェックアウト
  • actions/setup-python@v3
    • Python の Setup
    • 動作確認では、requirements.txt に変更
  • pre-commit/action@v2.0.3
    • pre-commit の実行

動作確認

pre-commit uninstallpre-commitを削除し、上で作った悲しみのコードをプッシュして動作を確認してみます。
コードをプッシュ・プルリクをすると、Github Actionsで失敗し、修正が求められるようになりました。:o:

error

所感

Github Actions良いなーというお気持ち
pre-commitは最終的にはユーザーがローカルに環境を構築していなければ駄目なのですが、Github Actionsで実行することにより人が環境に関与せずチェックされるのが良いですね。

備考

  • mypy
    • 型検査はコマンド実行
    • pre-commitには含まれていない
  • coverage
    • テストガバレッジはコマンド実行

参考

https://github.com/cookiecutter/cookiecutter-django
https://pre-commit.com/
https://pre-commit.ci/
https://github.com/pre-commit/pre-commit-hooks
https://github.com/psf/black
https://github.com/PyCQA/isort
https://github.com/PyCQA/flake8
https://docs.github.com/ja/actions/using-jobs/using-concurrency

GitHubで編集を提案

Discussion