こんにちは、テクマトリックスの米田です。

CI/CDツールを利用して自動化の流れを検討・構築していると「特定のファイルやディレクトリに変更があった時だけ処理を実行したい」「必要なときだけ特定のジョブを実行したい」といったニーズが出てきます。今回はGitHub Actionsを利用して、このようなケースに直面した時のワークフローの記述方法について調べ、試してみました。GitHub Actionsの公式ドキュメントにも記載のある基本的な構文ばかりですが、本記事でまとめます。

はじめに

GitHub Actionsは、GitHubで利用できるCI/CDツールとして広く利用されています。実行されるワークフローの効率化は、開発スピードの向上やコスト削減、リソースの有効活用といった多くのメリットをもたらします。特にクラウドベースのCI/CDツールを利用する場合、無駄なワークフローの実行は直接的なコストの増加や生産性の低下につながるため、出来る限り回避すべきです。

無駄なワークフロー実行による影響

1. 時間とリソースの浪費
不要なワークフローが実行されることで、開発者の待ち時間が増えます。たとえば、README.mdの修正やコメントの変更など、アプリケーション本体に影響しない変更でも、重たいビルドやテストが走る場合があります。この待ち時間がチーム全体に広がると、月単位・年単位の大きなロスになります。例えば、1回の無駄なワークフロー実行が5分で済むとしても、1日10回あれば50分、20営業日で約17時間のロスになります。

2. コストの増加
GitHub ActionsやCircleCIなどのクラウドベースのCI/CDツールは、無料枠を超えると使用時間や実行回数に応じて課金が発生します。
特に、モノレポ構成(1つのリポジトリに複数のプロジェクトが存在する構成)では、関係のない変更であってもすべてのワークフローが実行されることがあります。このような状況を放置すると、コストが大幅に増加してしまう可能性があります。

3. ワークフロー全体の混雑・遅延
ワークフローの実行数が増えすぎると、CIサービス側でジョブがキュー待ちの状態になることがあります。その結果、本当にワークフローを実行したいときに動かないという本末転倒な状態に陥ることもあります。
このような状況では、本来優先すべきリリース作業や緊急対応が後回しになり、プロジェクト全体の進行に影響を及ぼす可能性があります。

4. 無駄な通知の増加
多くのチームでは、CI/CDの実行結果をSlackやメールなどで通知する仕組みを導入していますが、通知が過剰に送られるとそれらがノイズになりかねません。関係のない変更による通知が増えることで、本当に重要な通知が埋もれてしまう危険性もあり、チームの情報共有にとって致命的な課題となる可能性があります。

無駄なワークフロー実行の回避方法

GitHub Actionsで無駄なワークフローの実行を回避するための基本的かつ効果的な方法を挙げます。

ファイルベースの実行制御(paths / paths-ignore

README.mdやドキュメントなどのファイル変更でワークフローが実行されないようにするには、pathsフィルターやpaths-ignoreフィルターを使います。ワークフローの実行を特定のファイルの変更だけに反応させることで、不要なワークフローの実行回数を削減できます。

pathsフィルターを使った以下の記述では、ソースコードが格納されているsrcフォルダ内のファイルに変更があった場合のみワークフローが実行されます。

on:
  push:
    paths:
      - 'src/**'

paths-ignoreフィルターを使った以下の記述では、README.mdやドキュメント類を格納しているdocsフォルダ内のファイルに変更があった場合に、ワークフローが実行されないように制御できます。

on:
  push:
    paths-ignore:
      - 'README.md'
      - 'docs/**'

ブランチ・タグによる制御(branches / tags

Gitを利用した開発では、その作業に合わせてブランチを作成して作業を行うのが一般的です(例:GitフローGitHubフロー)。GitHub Actionsを利用していくと、開発用・検証用・本番用など、異なるブランチごとに異なるワークフローが必要になる場合があります。このような場合、branchesフィルターにより対象のブランチを限定することで、不要なワークフローの実行を防止します。

branchesフィルターを利用した以下の記述では、main ブランチに変更が push されたときだけワークフローが実行されます。例えばmainブランチにマージされた時だけある処理を実行したい場合など、ブランチごとに異なる処理の振り分ける際に活用できます。

on:
  push:
    branches:
      - main

リリースのタイミングだけワークフローを実行したいときは、tagsフィルターを使用してタグ付きのpushに限定する方法が有効です。これにより、通常のpushやプルリクエスト(PR)ではワークフローは実行されず、タグがpushされたときだけワークフローが実行されます。

tagsフィルターを使った以下の記述では、v1.0.0v2.1.5 のような形式のタグが push されたときだけワークフローが実行されます。例えば、バージョンを明示したときだけリリース系の処理を実行したい場合に利用できます。

on:
  push:
    tags:
      - 'v*.*.*'

実行条件の追加(if:

GitHub Actionsでは if: を使った条件分岐によって、ワークフローに定義されたジョブごとに柔軟な実行条件を設定することができます。これにより、特定のイベントやブランチに応じた処理をジョブレベルで制御することができ、無駄なジョブの実行を回避し、リソースの節約や誤動作の防止に役立ちます。

例えば以下の設定では、deployジョブに条件式を設定することで、mainブランチへのpush(PRのマージ)時だけデプロイを行うように制御できます。

jobs:
  deploy:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: echo "本番デプロイ処理を実行します"

長時間ジョブの強制終了(timeout-minutes

意図せずジョブが長時間実行され、無料枠を浪費してしまうのは避けたいところです。このような事態を未然に防止する手段として、タイムアウト時間を設定することは有効です。
(GitHub Actionsのジョブのタイムアウトの時間はデフォルトでは6時間です。デフォルトのタイムアウトに関する公式ドキュメントの記述はこちら。)

timeout-minutes で実際の処理時間に応じた上限を設定しておくことで、一定時間でジョブを強制終了でき、無駄な実行やリソースの消費を防ぐことができます。

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 10  # 最大10分で強制終了

試してみる

上記で挙げた手法をいくつか踏襲して、GitHub Actionsのワークフローを実際に設定・実行してみます。
用意したリポジトリの構成とワークフローのymlファイルの内容を以下に示します。リポジトリにはJavaを用いたソースコードファイルを格納しており、JavaのビルドツールであるMavenを用いたビルドやテストを行います。
ワークフローでは以下の内容を確認します。

  1. README.mdだけの変更ではワークフローの実行をスキップする
  2. ソースコード本体(src/)の変更がdevelopブランチにpushされた場合はワークフローが実行され、testジョブのみ実行される
  3. developブランチをmainブランチへマージするPR作成後、build-and-testジョブのみ実行される
  4. main ブランチへのPRマージ時のみ、ワークフローはbuild-and-testジョブに加えてdeployジョブも実行する

./
├── README.md            
├── pom.xml                      # Mavenのビルド設定ファイル
├── src/          
|   └── main/java/com/mycompany/app/App.java   # ソースコード
│   └── test/java/com/mycompany/app/AppTest.java  # テストコード
└── .github/
    └── workflows/
        └── ci.yml                # ワークフローファイル

name: Demo workflow

on:
  # main・developブランチへpush時、src/**に変更がある場合のみ実行
  push:
    branches:
      - main
      - develop
    paths:
      - 'src/**'

  # mainブランチへのPR作成・更新時に実行
  pull_request:
    branches:
      - main

jobs:
  build-and-test:
    name: Run build and test
    runs-on: ubuntu-latest

    steps:
      # 1. リポジトリをチェックアウト
      - name: Checkout repository
        uses: actions/checkout@v4

      # 2. Javaのセットアップ
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      # 3. Mavenでビルドとテストの実行
      - name: Build and Test with Maven
        run: mvn -B clean install
  
  # mainブランチにpushされた場合のみ実行
  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: build-and-test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Deploy to server
        run: echo "mainブランチにマージされたのでデプロイジョブを実行しました"

README.mdだけの変更ではワークフローの実行をスキップする
まずは、developブランチ上でREADME.mdを変更してpushします。この変更ではソースコードに影響を与えないため、ワークフローが実行されないことを確認します。
実際にdevelopブランチ上でREADME.mdを編集してコミットをpushしたところ、GitHub Actions の「Actions」タブにはワークフローの実行履歴は表示されませんでした。これはci.ymlpushイベントにpaths: src/**の指定があるため、README.mdの変更ではワークフローがトリガーされないためです。

developブランチでREADME.mdを修正してもワークフローが実行されない様子

② ソースコード本体(src/)の変更がdevelopブランチにpushされた場合はワークフローが実行され、build-and-testジョブのみ実行される
次に、developブランチでsrcフォルダ内のApp.javaを編集し、同様にpushします。このファイルはソースコードの一部で、テストが必要な対象です。
リポジトリにpush後、GitHub Actions上でbuild-and-testジョブ(Run build and test)が自動的に実行されました。これはリポジトリにpushした変更内容がpaths: src/** の条件に合致しているためで、ソースコードの変更が行われたときにのみワークフローが実行されるよう設定しているためです。

developブランチでsrcフォルダ内のApp.javaを修正し、ワークフローが実行される様子

developブランチをmainブランチにマージするPRを作成後、build-and-testジョブのみ実行される
続いて、developブランチからmainブランチにマージするためのプルリクエスト(PR)を作成します。developブランチには上記①,②で行ったREADME.mdsrcフォルダ内のApp.javaの修正が含まれています。
PR 作成後、ワークフローが起動し、build-and-test ジョブが実行されました。これは pull_request イベントで、mainブランチにマージするための PR を作成した後にワークフローが実行される設定をしているためです。

developブランチをmainブランチにマージするPR作成後にワークフローが実行される様子

main ブランチへのPRマージ時のみ、ワークフローはbuild-and-testジョブに加えてdeployジョブも実行する
最後に③で作成したPRをmainブランチにマージします。この操作はmainブランチへのpushイベントとして扱われます。
マージ後、GitHub Actions上でtestジョブとdeployジョブの両方が実行されました。これはpushイベントがmainブランチを対象としており、さらにdeployジョブにはif: github.event_name == 'push' && github.ref == 'refs/heads/main'という条件が設定されているため、mainブランチに対するpush(PRのマージ)のタイミングでのみdeployジョブが動作しました。

作成したPRをmainブランチにマージした時にワークフローが実行される様子

このように、GitHub Actionsのbranchesフィルターやpathsフィルター、pull_requestといったトリガー設定を適切に使い分けることで、不要なワークフローの実行を抑制しつつ、品質を保つために必要なタイミングでワークフローを実行させることが可能になります。
また、Mavenのような、ライブラリを使用するワークフローでは、各ジョブでキャッシュを設定することで、ダウンロード済みのライブラリを再利用できます。キャッシュを活用することで、より効率的にワークフローを実行することが見込めます。(GitHub Actionsのキャッシュに関する公式ドキュメントはこちら

まとめ

GitHub Actionsによるワークフローの実行は、リポジトリ内に変更が加わると自動で実行されるので非常に便利です。しかし、その便利さゆえに、必要のないタイミングでワークフローが実行され、不要なコストや混乱が発生する場合があります。今回紹介した無駄なワークフローの実行を回避する手法は、いずれもGitHub Actionsの基本的な構文を利用したもので、すぐに取り入れられるものばかりです。無駄なワークフローの実行を回避することは意識すべき重要なポイントだと思うので、ぜひご参考ください。

弊社の開発基盤構築ソリューションチームでは、GitHub ActionsやJenkinsを利用したCI環境の構築を承っています。CI環境の導入や構築について気になることがあれば、ぜひお気軽にお問い合わせください。

参考リンク
GitHub Actionsのワークフロー構文
ワークフローをトリガーするイベント

By yoneta