はじめに
複数のPython projectを1つのリポジトリで管理する場合、CIの設定をどうするか悩むことがあると思います。
通常は、各プロジェクトごとにworkflowを用意し、paths
に対応プロジェクトを指定することで、特定プロジェクトに対してのみworkflowを実行することになります。
ただ、個々のプロジェクトで使用しているライブラリやpythonのバージョンは違うが、Lintやテストのworkflowは、ほぼ同じ内容になることが多いです。
そのため、この方法では、同じ内容のworkflowがいくつもできあがってしまいます。
on:
push:
paths:
- 'project-1/**' # project-1 に変更があった場合
この記事では、以下の要望を満たすworkflowを作成します。
- 複数のPython projectを1つのリポジトリで管理している。root直下に、複数のディレクトリが存在し、それぞれがプロジェクトを表す。
- 例
project-1/
project-2/
- 例
- lintやテストなど共通のworkflowを1つにまとめたい
- 各プロジェクトごとに使いたいライブラリやpythonのバージョンは異なる。そのため、各プロジェクト直下にある
pyproject.toml
に応じてライブラリやpythonのバージョンを変更してほしい - 複数のプロジェクトが同時に変更されることもある。
方法
以下のリポジトリの.github/workflows/python-lint-test.yaml
に上記要件を満たすような完成形のworkflowがあります。
GitHub - haru-256/blog-python-multiple-project-20250405
jobs:
target_projects:
name: Find target Projects
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
projects: ${{ steps.get-projects.outputs.projects }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed directories
id: changed-files
uses: tj-actions/changed-files@6cb76d07bee4c9772c6882c06c37837bf82a04d3 # v46
with:
matrix: true # 後続のjobでmatrixで使うために。ファイルがjson形式で出力される
dir_names: true # uniqueディレクトリ名を取得
dir_names_max_depth: 1 # ディレクトリ名の深さを指定
files_ignore: |
.github/**
- name: Get Python projects
uses: actions/github-script@v7
id: get-projects
with:
script: |
const fs = require('node:fs');
// If target_projects input is provided, use it
dispatch_inputs = "${{ github.event.inputs.target_projects }}";
if (dispatch_inputs !== "") {
// TODO: validate the input
core.setOutput('projects', JSON.stringify(dispatch_inputs));
} else { // If no input is provided, get directories from changed files
const inputs = JSON.parse(${{ toJSON(steps.changed-files.outputs.all_changed_and_modified_files) }});
// Filter out directories that contain pyproject.toml and Makefile
const paths = inputs.filter(path => {
if (
fs.statSync(path).isDirectory()
&& fs.existsSync(`${path}/pyproject.toml`)
&& fs.existsSync(`${path}/Makefile`)
) {
return true;
}
return false;
});
core.setOutput('projects', JSON.stringify(paths));
}
- name: Print changed directories
run: |
echo "Changed directories: ${{ steps.get-projects.outputs.projects }}"
echo "::debug::Changed directories: ${{ steps.get-projects.outputs.projects }}"
python-lint-and-test:
name: Python Lint and Test
needs: target_projects
if: ${{ needs.target_projects.outputs.projects != '' && toJson(fromJson(needs.target_projects.outputs.projects)) != '[]' }}
strategy:
matrix:
project: ${{ fromJson(needs.target_projects.outputs.projects) }}
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ matrix.project }}
cancel-in-progress: true
defaults:
run:
working-directory: ${{ matrix.project }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "${{ matrix.project }}/pyproject.toml"
cache-suffix: "${{ matrix.project }}"
- name: Install the project
run: make install
- name: Lint
run: make lint
- name: Test
run: make test
概要
Github Action: tj-actions/changed-filesを使用して、各event(push
, pull_request
) ごとに適切なcommit前後で、変更されたディレクトリを取得します。その後、大賞プロジェクトを推定します。
そして、上記のjobの後に、プロジェクトディレクトリをworking directoryとして、Lint や Testを実行します。これにより、working directory配下のpyproject.toml
に応じてライブラリやpythonバージョンを変更して、workflowを実行できmるようになります。
具体的な流れ
ここではworkflowの中でもキモとなる target_projects
というjobの中身を説明します。
target_projects
では、Lintとテストを実行する対象のプロジェクト = ディレクトリを取得します。
その後のstepで取得したディレクトリをworking directoryにするため、ディレクトリ名をprojects
に格納します。
このstepは、以下のような流れで処理を行います。
-
tj-actions/changed-files
で変更されたファイルのユニークディレクトリ名を取得する - 各ディレクトリ直下に
pyproject.toml
とMakefile
が存在するかを確認し、存在するディレクトリのみをcontextprojects
に格納する。
1. tj-actions/changed-files
で変更されたファイルのユニークディレクトリ名を取得
tj-actions/changed-files
を使用して、変更されたファイルのユニークディレクトリ名を取得します。
このとき、以下の変数を指定します。
-
matrix
: 後続のjobでmatrixで使うために。ファイルがjson形式で出力される。変更されたプロジェクトが複数ある場合、matrixで指定することで、後続のjobでそれぞれのプロジェクトに対してworkflowを実行できるようになります。 -
dir_names
: ファイル名ではなく、ユニークディレクトリ名を取得するために指定します。 -
dir_names_max_depth
: ディレクトリ名の深さを指定します。1
を指定することで、直下のディレクトリ名のみを取得します。 e.g.project-1
やproject-2
のように、直下のディレクトリ名を取得します。 -
files_ignore
: 除外するファイルを指定します。ここでは、.github
ディレクトリを除外しています。
with:
matrix: true # 後続のjobでmatrixで使うために。ファイルがjson形式で出力される
dir_names: true # uniqueディレクトリ名を取得
dir_names_max_depth: 1 # ディレクトリ名の深さを指定
files_ignore: |
.github/**
2. 各ディレクトリ直下にpyproject.toml
とMakefile
が存在するかを確認
pyproject.toml
とMakefile
が存在するディレクトリのみを取得します。
このとき、bash scriptを書いても良いのですが、actions/github-scriptを使用することで、jsが使用でき、より簡潔に書くことができます。
actions/github-script
は以下の記事が参考になります。
取得したディレクトリをworking directoryとして、Lint や Testを実行
target_projects
jobで取得したprojectsを、python-lint-and-test
jobで使用します。
このとき、複数projectが同時に変更されることもあるため、matrixを使用して、各projectに対してworkflowを実行します。
tj-actions/changed-files
でinput: matrix
をtrueにしておくと、出力がjson文字列形式で出力されるため、fromJson
を使用しつつ、matrixに指定することができます(公式の例)。
python-lint-and-test:
name: Python Lint and Test
needs: target_projects
if: ${{ needs.target_projects.outputs.projects != '' && toJson(fromJson(needs.target_projects.outputs.projects)) != '[]' }}
strategy:
matrix:
project: ${{ fromJson(needs.target_projects.outputs.projects) }}
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ matrix.project }}
cancel-in-progress: true
defaults:
run:
working-directory: ${{ matrix.project }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "${{ matrix.project }}/pyproject.toml"
cache-suffix: "${{ matrix.project }}"
- name: Install the project
run: make install
- name: Lint
run: make lint
- name: Test
run: make test
まとめ
この記事では、以下のことを紹介しました。
- 複数のPython projectを1つのリポジトリで管理する場合、CIの設定をどうするか悩むことがある
- 各プロジェクトごとに使いたいライブラリやpythonのバージョンは異なる。そのため、各プロジェクト直下にある
pyproject.toml
に応じてライブラリやpythonのバージョンを変更してほしい -
tj-actions/changed-files
で変更されたファイルを取得しつつ、それらをmatrix
指定することで複数projectでLintとテスト をそれぞれの環境で実施することができる