actions/labeler のラベル自動割当によって実行する GitHub Actions ジョブ/ステップを制御する
ワークフローごとに条件分岐をしている方々、いかがお過ごしでしょうか
今回は、GitHub Actions に関する記事です。
今回は表題の通り GitHub Actions で付与されたラベルに応じて実行するジョブを制御し、最終的には actions/labeler を用いてラベルの付与自体も自動化するアプローチを紹介します。
ジョブ/ステップと書いていますが、本記事ではジョブの制御を中心に紹介し、ステップは簡単に紹介します。
以下の方には刺さる内容かな、と思っています。
- モノリポ開発を行っている方
- CI 実行時に同時にワークフローがいくつも走る方
思いつきで書き始めたのですが、かなり筋がいいような気がしたので、メモ書きとして残しておくことにしました。
二番煎じかもしれませんが、ご容赦くださいませ。
モチベーション
ワタシの個人開発では、複数言語・複数プロジェクトが1個のリポジトリ内で管理されているモノリポジトリを運用しています。この際に、CI による自動レビューを走らせているのですが、「CI の実行時間を節約するために差分を見て走らせる CI を限定する(ex: Go コードしか書き換えていない時は Go の CI しか走らせない)」ために、各言語・プロジェクトごとに CI を分割することを検討しました。
つまり、以下のようなワークフローファイルが作成されていきます。
ci_xxx_go.yaml
ci_xxx_openapi.yaml
ci_yyy_typescript.yaml
ci_terraform.yaml
何かあるたびに新しいワークフローファイルが作成されていくのですが、この際に、以下の問題が発生しました。
- 各ワークフローファイルに実行条件が分散してしまい、何がどこにあるのかがわからなくなる
- 必須で成功してほしいワークフローが困難になる
- ワークフロー間での依存関係を表現できない
それぞれの問題の詳細
各ワークフローファイルに条件が分散してしまい、何がどこにあるのかがわからなくなる
各ワークフローにはファイル差分で実行するかどうかを判断するために、以下のような形でワークフローの実行条件を記述するかと思います。
on:
pull_request:
paths:
- .github/workflows/ci_go.yaml
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
これが各ファイルに分散しており、どの条件で CI が行われるのかを俯瞰しにくくなりました。
また、「ほぼ」同じ内容を actions/labeler の設定ファイルにも記述することを踏まえると、条件管理が二重管理となってしまうのが面倒でした。
この「ほぼ」と言うのが肝になってきます。
ラベルで得たい情報と CI を分けたい基準は異なると思うので、これが一致しない場合はこのアプローチは推奨されません。「それでも他の理由でこのアプローチを使用したい」と言う場合にはラベルにプレフィックスを設定するなどの対策を取る必要があります。
必須で成功してほしいワークフローの設定が困難になる
CI が各ワークフローファイルに分散することで、必須で成功してほしいワークフローの設定が困難になります。
GitHub Actions では Ruleset で merge を許可するかどうかの判断するための設定項目に Require status checks to pass (ステータスチェックを通過しなければいけない)というものがありますが、GitHub Actions はこれを ジョブ名 単位で管理しています。
つまり、以下のように処理されます。
-
ci.yaml / ci_goジョブとci_go.yaml / ci_terraformジョブは異なる扱いにできる -
ci_go.yaml / ciジョブとci_terraform.yaml / ciジョブは同じ扱い
これによって、異なるワークフロー間で必須条件を共有したい時(CI なのでできれば全部通過してほしい)は job を同一名で設定する必要が出てきました。
ワークフロー間での依存関係を表現できない
各ワークフローファイルは独立しているので依存関係を持つことはありません。ただし、CI の順序として先に動かした方が都合が良いものが存在します。
例えば以下のような例です。
-
ci_xxx_go.yamlコードの静的解析およびテストを実行する -
ci_xxx_openapi.yamlOpenAPI スキーマからクライアント・サーバー向けにコードを生成する
この場合、OpenAPI スキーマから生成される Go コードの変更が行われる可能性が考えられます。すると、コード生成を行っていない・フォーマットが不適切などの原因で ci_xxx_openapi.yaml で失敗するとき、その間動いていた ci_xxx_go.yaml は無駄になってしまいます。そのため、できることなら ci_xxx_openapi.yaml, ci_xxx_go.yaml の順序で実行してほしいです。
これらの主な問題は CI で実行されるワークフローが分散してしまうことに問題がありますが、逆に一つのワークフローで管理しようとすると当初の目的である「CI の実行時間を節約するために差分を見て走らせる CI を限定する(ex: Go コードしか書き換えていない時は Go の CI しか走らせない)」が達成できません。
同一ワークフロー・同一ジョブ内で変更差分で実行制御を行おうと思った際に tj-actions/changed-files などを用いる方法もありますが、これらは変更されたファイルが何かを教えるだけで、複雑な条件を書こうと思うとそれなりに工夫が必要になってきます。これは面倒です。
一方で、今回のアプローチであるラベルによるジョブ制御は actions/labeler を用いることでラベル付与を CI に組み込んで自動化することで CI ワークフローを一本化しつつ、実行するジョブやステップを制御する代替方法と見なすことができます。
基本方針
-
actions/labelerの設定ファイルを作成 - Pull Request イベントを条件に発火するワークフロー中で
actions/labelerを呼び出すジョブを作成 - ラベルの有無を実行条件にしたジョブ/ステップを作成
- (同一ワークフローファイルを用いた場合)すべてのジョブが成功したかどうかを確認する job を作成する
実際に書いてみる
actions/labeler の設定ファイルを作成
.github/labeler.yaml を作成します。細かい書き方は actions/labeler を見ていただければと思います。
# ...
go:
- changed-files:
- any-glob-to-any-file:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
- go/**
- .github/workflows/ci_go.yaml
- .golangci.yml
- .mockery.yml
- taskfile/go.taskfile.yml
kubernetes:
- changed-files:
- any-glob-to-any-file:
- kubernetes/**/*.yaml
- kubernetes/**/*.yml
- argocd/**/*.yaml
- argocd/**/*.yml
- argocd/**
- kubernetes/**
- .github/workflows/ci_kubernetes.yaml
- taskfile/kubernetes.taskfile.yml
python:
- changed-files:
- any-glob-to-any-file:
- "**/*.py"
- python/**
- .github/workflows/ci_python.yaml
- .python-version
- pyproject.toml
- uv.lock
- taskfile/python.taskfile.yml
sql:
- changed-files:
- any-glob-to-any-file:
- "**/*.sql"
- sql/**
- .github/workflows/ci_sql.yaml
- sqlc.yml
- taskfile/sql.taskfile.yml
# ...
Pull Request イベントを条件に発火するワークフロー中で actions/labeler を呼び出すジョブを作成
PR イベントを条件に発火するワークフローで actions/labeler を呼び出し、ラベルの自動付与を行います。
それでは .github/workflows/ci.yaml に CI workflow を作成していきます。
on:
pull_request:
types:
- opened
- synchronize
- reopened
- labeled # actions/labeler ラベル付与時も CI が実行される必要があるので追加する
- unlabeled # 同上
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci_setup:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v5
- uses: actions/labeler@v6
# ...
大事なポイントとしては、ワークフローの実行条件となる Pull Request イベントをデフォルトのものに加えて labeled, unlabeled を追加します。
これにより、 actions/labeler による付与直後に再度ワークフローが走ることになります。
concurrency.cancel-in-progress: true を記述することで、ラベル付与した瞬間に自分自身のワークフローが停止するため、見かけ上では actions/labeler 実行直後に停止して再度ワークフローを立ち上げるような挙動をします。
ラベルの有無を実行条件にしたジョブ/ステップを作成
ラベルの有無を実行条件にした各種 CI を配置していきます。
.github/workflows/ci.yaml に以前の続きから書きます。
# ...
ci_go:
needs: [ci_setup, ci_sql]
if: always() && contains(github.event.pull_request.labels.*.name, 'go')
# ...
ci_kubernetes:
needs: [ci_setup]
if: always() && contains(github.event.pull_request.labels.*.name, 'kubernetes')
# ...
ci_python:
needs: [ci_setup]
if: always() && contains(github.event.pull_request.labels.*.name, 'python')
# ...
ci_sql:
needs: [ci_setup]
if: always() && contains(github.event.pull_request.labels.*.name, 'sql')
# ...
# ...
ここの肝は大きく分けて 2 種類です。
一つ目は、すべてのジョブの needs に ci_setup を追加することです。これにより、ラベル付与よりも先に動くことを阻止します。
二つ目は if の条件のところに always() と contains(github.event.pull_request.labels.*.name, '...') の論理積をとることです。
always() を取るのは、needs で ci_setup 以外のジョブがあった時、そのジョブがスキップになるとそれに巻き込まれてスキップになってしまうからです。
今回の例では、 ci_go は needs に ci_sql を持っていますが、 ci_sql が差分なしでスキップになった時に ci_go もそれに巻き込まれてスキップされてしまうことを防ぐ目的です。
ステップの場合も if を設定することで成立します
jobs:
job_a:
needs: [ci_setup]
steps:
- name: step 1
if: contains(github.event.pull_request.labels.*.name, 'required/step_a')
# ...
(同一ワークフローファイルを用いた場合)すべてのジョブが成功したかどうかを確認する job を作成する
最後に、ステータスチェック用にすべてのジョブが成功したかどうかを報告するためのジョブを作成します
ci_report:
runs-on: ubuntu-latest
needs:
[
ci_go,
ci_kubernetes,
ci_python,
ci_sql,
# ...
]
if: always()
steps:
- name: Check all CI results
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
それぞれを繋げるとこんな感じ
.github/labeler.yaml
# ...
go:
- changed-files:
- any-glob-to-any-file:
- "**/*.go"
- "**/go.mod"
- "**/go.sum"
- go/**
- .github/workflows/ci_go.yaml
- .golangci.yml
- .mockery.yml
- taskfile/go.taskfile.yml
kubernetes:
- changed-files:
- any-glob-to-any-file:
- kubernetes/**/*.yaml
- kubernetes/**/*.yml
- argocd/**/*.yaml
- argocd/**/*.yml
- argocd/**
- kubernetes/**
- .github/workflows/ci_kubernetes.yaml
- taskfile/kubernetes.taskfile.yml
python:
- changed-files:
- any-glob-to-any-file:
- "**/*.py"
- python/**
- .github/workflows/ci_python.yaml
- .python-version
- pyproject.toml
- uv.lock
- taskfile/python.taskfile.yml
sql:
- changed-files:
- any-glob-to-any-file:
- "**/*.sql"
- sql/**
- .github/workflows/ci_sql.yaml
- sqlc.yml
- taskfile/sql.taskfile.yml
# ...
.github/workflows/ci.yaml
on:
pull_request:
types:
- opened
- synchronize
- reopened
- labeled # actions/labeler ラベル付与時も CI が実行される必要があるので追加する
- unlabeled # 同上
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci_setup:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v5
- uses: actions/labeler@v6
# ...
ci_go:
needs: [ci_setup, ci_sql]
if: always() && contains(github.event.pull_request.labels.*.name, 'go')
# ...
ci_kubernetes:
needs: [ci_setup]
if: always() && contains(github.event.pull_request.labels.*.name, 'kubernetes')
# ...
ci_python:
needs: [ci_setup]
if: always() && contains(github.event.pull_request.labels.*.name, 'python')
# ...
ci_sql:
needs: [ci_setup]
if: always() && contains(github.event.pull_request.labels.*.name, 'sql')
# ...
# ...
ci_report:
runs-on: ubuntu-latest
needs:
[
ci_setup,
ci_go,
ci_kubernetes,
ci_python,
ci_sql,
# ...
]
if: always()
steps:
- name: Check all CI results
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
終わりに
これにより、ラベルに応じて必要なジョブだけを柔軟に実行する CI を構築できます。何かの参考になれば幸いです。
今回は時期的な都合もあり GitHub Actions Advent Calendar 2025 の記事として書かせていただきました。ぜひ他の記事もご一読ください。
Discussion