JavaプロジェクトのCIをMavenとGitHub Actionsで構築する

1. 概要

Continuous Integration (CI) は、コードをマージする時点でトラブルが発生するのを待つのではなく、プッシュ直後にバグを検出するのに役立ちます。このガイドでは、GitHub Actions で Java プロジェクト向けの CI パイプラインを構築する方法を説明します:ビルド、テスト実行、依存性のキャッシング、複数 JDK バージョンのビルドマトリックス、レポート公開。読み終わったら、ワークフローファイルを実際のプロジェクトにコピーして、数分で実行できます。

2. サンプルプロジェクトの準備

Maven の標準的な構造を持つシンプルな Spring Boot プロジェクトがあると仮定します。最小限の pom.xml ファイルは以下の通りです:

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.nonladev</groupId>
    <artifactId>order-service</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <!-- コンパイラの Java バージョンを指定 -->
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- JUnit 5 フレームワーク -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
        <!-- アサーション用ライブラリ -->
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.25.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- テスト実行用プラグイン -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

junit-jupiter と assertj-core 依存性はテストを書くために使用されます。maven-surefire-plugin は mvn test を実行した時にテストを実行する責任があります。

テスト対象となるシンプルなビジネスクラス:

package com.nonladev.order;

public class OrderCalculator {

    public double calculateTotal(double unitPrice, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantityは0より大きい必要があります");
        }
        return unitPrice * quantity;
    }
}

対応するテスト:

package com.nonladev.order;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class OrderCalculatorTest {

    private final OrderCalculator calculator = new OrderCalculator();

    @Test
    void shouldCalculateTotalCorrectly() {
        double total = calculator.calculateTotal(50_000, 3);

        assertThat(total).isEqualTo(150_000);
    }

    @Test
    void shouldThrowWhenQuantityIsInvalid() {
        assertThatThrownBy(() -> calculator.calculateTotal(50_000, 0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Quantityは0より大きい必要があります");
    }
}

shouldCalculateTotalCorrectly は合計が正しく計算されることを確認します。shouldThrowWhenQuantityIsInvalid は quantity が無効な場合に例外がスローされることを確認します。これは CI が毎回コードをプッシュするたびに実行するテストです。

3. 基本的な CI ワークフロー

GitHub Actions は .github/workflows/ ディレクトリ内の YAML ファイルを読みます。.github/workflows/ci.yml ファイルを作成します:

name: CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Build and run tests
        run: mvn -B clean verify

各部分の説明:

  • on:ワークフローは main ブランチへの push または pull_request 時に実行されます。
  • actions/checkout@v4:ソースコードを runner にクローンします。
  • actions/setup-java@v4:Temurin JDK 21 をインストールします。mvn を呼び出す前にこのステップが常に必要です。
  • mvn -B clean verify:フラグ -B(バッチモード)はインタラクティブ出力を無効にしており、CI 環境に適しています。clean verify は最初からビルドし、テストを実行します(verify フェーズは test を含みます)。

コミットをプッシュすると、GitHub の Actions タブで、次のようなログを含むジョブが実行されているのを見ることができます:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.nonladev.order.OrderCalculatorTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

テストが失敗した場合、ジョブは赤いステータスを持ち、ログにスタックトレースが表示されます。マシンに SSH でアクセスする必要はありません。

4. Maven の依存性キャッシングでビルドを高速化

ジョブが新しい runner で実行されるたびに(一時的な環境のため)、Maven は .m2/repository から依存性全体を再ダウンロードする必要があります。Spring Boot プロジェクトの場合、これには毎回 1~2 分かかることがあります。setup-java はキャッシングをサポートしています:

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: 'maven'

cache: 'maven' 属性は pom.xml ファイルのハッシュに基づいて ~/.m2/repository ディレクトリを自動的にキャッシュします。次回のビルドで pom.xml が変わらなければ、Actions はキャッシュをすぐに復元し、jar の再ダウンロードをスキップします。

キャッシュ追加前後の「ビルドとテスト実行」ステップの実行時間を確認することで効果を検証できます。通常、30~60% の削減が期待できます。

注意: プロジェクトが Maven Wrapper(./mvnw)を使用している場合、runner にインストールされているデフォルトの Maven バージョンに依存する代わりに、mvnw に基づいてキャッシュすべきです。GitHub がイメージを更新するたびに、デフォルトの Maven バージョンが変わる可能性があるためです。

5. 複数の JDK バージョンでのビルドマトリックス

複数の Java バージョンとの互換性が必要なライブラリを保守している場合、strategy.matrix を使用して複数のジョブを並行実行します:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        java-version: [ '17', '21' ]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK ${{ matrix.java-version }}
        uses: actions/setup-java@v4
        with:
          java-version: ${{ matrix.java-version }}
          distribution: 'temurin'
          cache: 'maven'

      - name: Build and run tests
        run: mvn -B clean verify

matrix.java-version はテスト対象のバージョンのリストです。GitHub Actions は自動的に 2 つのジョブを並行実行します:1 つは JDK 17 用、1 つは JDK 21 用です。変数 ${{ matrix.java-version }} は各ジョブで対応する値に置き換えられます。

Actions タブの結果には 2 つのジョブが別々に表示されます(例:build (17) と build (21))。これにより、プロジェクトがどの Java バージョンで失敗するかをすぐに確認できます。

6. テストレポートの公開とカバレッジの測定

デフォルトでは、テストログは Actions にテキスト形式でのみ表示されます。より読みやすいレポートを得るには、dorny/test-reporter アクションを使用します:

      - name: Build and run tests
        run: mvn -B clean verify

      - name: Publish test report
        uses: dorny/test-reporter@v1
        if: success() || failure()
        with:
          name: Maven Tests
          path: '**/surefire-reports/*.xml'
          reporter: java-junit

if: success() || failure():このステップはテストが失敗した場合でも実行されます(ジョブがキャンセルされていない限り)。常にレポートを表示できるようにします。path は Surefire がテスト実行後に生成する XML ファイルを指します。レポートは PR の個別チェックとして表示され、各テストケースの pass/fail をリストアップします。

カバレッジを測定したい場合は、pom.xml に JaCoCo プラグインを追加します:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

mvn verify を実行すると、自動的に target/site/jacoco/index.html に HTML レポートが生成されます。それをアーティファクトとしてアップロードできます:

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: jacoco-report
          path: target/site/jacoco/

このアーティファクトはワークフロー実行のサマリーページからダウンロードでき、ブラウザで直接表示できます。

7. ビルドステータスバッジ

ワークフローが安定して実行されたら、README.md にバッジを追加して、Actions にアクセスしなくても誰もが CI ステータスを確認できるようにします:

![CI](https://github.com/<org>/<repo>/actions/workflows/ci.yml/badge.svg)

<org>/<repo> を実際の GitHub リポジトリ名に置き換えます。バッジは最後の実行結果に応じて自動的に緑/赤で更新されます。

8. よくあるエラー

  • maven.compiler.source と一致する java-version を設定し忘れ:ローカルビルドは成功するが CI 失敗。pom.xml とワークフローの JDK バージョンを常に同期してください。
  • mvn verify の代わりに mvn test を使用:プロジェクトに integration test があり、verify フェーズで実行される場合(例えば Failsafe プラグイン)、mvn test はそれらをスキップし、誤った安心感を招きます。
  • 依存性を変更した時にキャッシュキーが間違っている:actions/cache を手動で記述していて、pom.xml をキーハッシュに含めていない場合、古いキャッシュが廃止された依存性を使わせるかもしれません。setup-java の cache: 'maven' を使用してこれを回避してください。
  • Docker/ネットワークが安定していない self-hosted runner で CI 実行:Maven は中央リポジトリから依存性をダウンロードする必要があります。runner がネットワークブロック中の場合、ビルドはダウンロード段階で停止します。プロキシを確認するか、内部 Maven ミラーを使用してください。

注意: プロジェクトが非常に小さく、単純なテストのみで頻繁にデプロイしない場合、マトリックス + カバレッジレポートを含む完全な CI ワークフローは過度かもしれません。セクション 3 の基本的なワークフローから始めて、プロジェクトが成長するにつれて段階的に拡張してください。

9. まとめ

数十行の YAML で、Java プロジェクトの CI パイプラインを自動的にビルド、テスト、依存性をキャッシング、レポートを公開できます。シンプルなワークフローから始めて、必要に応じてマトリックス JDK、JaCoCo カバレッジ、README バッジを追加します。最も重要なのは複雑なワークフローではなく、CI が安定して信頼性のあるものであることです。

github-actionsjavamavenci-cdtesting