CI para projetos Java com Maven usando GitHub Actions

1. Visão geral

Continuous Integration (CI) ajuda você a detectar erros logo quando faz push do código, em vez de esperar até o merge e tudo quebrar. Este guia mostra como montar um pipeline CI para seu projeto Java com Maven no GitHub Actions: build, execução de testes, cache de dependências, matriz de build com múltiplas versões do JDK e publicação de relatórios. Ao final, você poderá copiar o arquivo de workflow para seu projeto real e executá-lo em poucos minutos.

2. Preparando o projeto de exemplo

Suponha que você tenha um projeto Spring Boot simples com estrutura Maven padrão. Um arquivo pom.xml mínimo seria assim:

<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>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <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>

A dependência junit-jupiter e assertj-core são usadas para escrever testes. O plugin maven-surefire-plugin é responsável por executar os testes quando você executa mvn test.

Uma classe de lógica de negócio pequena para testar:

package com.nonladev.order;

public class OrderCalculator {

    public double calculateTotal(double unitPrice, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity deve ser maior que 0");
        }
        return unitPrice * quantity;
    }
}

E o teste correspondente:

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 deve ser maior que 0");
    }
}

shouldCalculateTotalCorrectly verifica se o cálculo do total está correto. shouldThrowWhenQuantityIsInvalid verifica se uma exceção é lançada quando quantity é inválida. Estes são os testes que o CI executará cada vez que você fizer push do código.

3. Workflow CI básico

GitHub Actions lê arquivos YAML no diretório .github/workflows/. Crie o arquivo .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

Explicação de cada parte:

  • on: o workflow é executado quando há um push ou pull_request direcionado para a branch main.
  • actions/checkout@v4: clona o código-fonte para o runner.
  • actions/setup-java@v4: instala o JDK 21 da distribuição Temurin — você sempre precisa dessa etapa antes de executar mvn.
  • mvn -B clean verify: a flag -B (batch mode) desativa a saída interativa, adequada para ambiente CI. clean verify reconstrói do zero e executa os testes (a fase verify inclui test).

Faça push do commit, vá até a aba Actions no GitHub, e você verá o job rodando com um log similar a:

[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

Se o teste falhar, o job terá status vermelho e você verá o stack trace direto no log — sem precisar de SSH em nenhuma máquina.

4. Cache de dependências Maven para builds mais rápidos

Cada vez que o job é executado em um novo runner (efêmero), Maven precisa baixar todas as dependências novamente de .m2/repository. Para um projeto Spring Boot, isso pode levar 1–2 minutos cada vez. setup-java oferece suporte integrado a cache:

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

A propriedade cache: 'maven' automaticamente cacheia o diretório ~/.m2/repository com base no hash do arquivo pom.xml. No próximo build, se o pom.xml não mudar, o Actions restaura o cache imediatamente, pulando o download de cada jar.

Você pode verificar a eficácia observando o tempo da etapa “Build and run tests” antes e depois de adicionar o cache — geralmente reduz 30–60%.

Nota: Se seu projeto usa Maven Wrapper (./mvnw), é melhor cachear de acordo com mvnw em vez de confiar na versão Maven pré-instalada no runner, pois cada vez que GitHub atualiza a imagem, a versão padrão do Maven pode mudar.

5. Matriz de build com múltiplas versões do JDK

Se você mantém uma biblioteca que precisa ser compatível com múltiplas versões do Java, use strategy.matrix para executar vários jobs em paralelo:

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 lista as versões que você deseja testar. O GitHub Actions cria automaticamente 2 jobs executando em paralelo: um com JDK 17 e outro com JDK 21. A variável ${{ matrix.java-version }} é substituída em cada job correspondente.

Os resultados na aba Actions mostram 2 jobs separados, por exemplo build (17) e build (21), ajudando você a saber imediatamente em qual versão Java o projeto falha.

6. Publicar relatório de testes e verificar cobertura

Por padrão, o log dos testes é exibido apenas como texto no Actions. Para ter um relatório mais fácil de ler, use a action 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(): essa etapa é executada mesmo quando o teste falha (mas não é executada se o job for cancelado), para que você sempre veja o relatório. path aponta para o arquivo XML que o Surefire gera após executar os testes. O relatório aparece como uma verificação separada no PR, listando cada caso de teste como pass/fail.

Se você quer medir a cobertura, adicione o plugin JaCoCo ao pom.xml:

<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>

Executar mvn verify irá gerar automaticamente um relatório HTML em target/site/jacoco/index.html. Você pode fazer upload dele como um artefato:

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

Este artefato pode ser baixado da página de resumo da execução do workflow e visualizado diretamente no navegador.

7. Badge de status do build

Depois que o workflow está rodando estável, adicione um badge ao README.md para que as pessoas saibam o status do CI sem precisar entrar no Actions:

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

Substitua <org>/<repo> pelo seu nome real do GitHub. O badge se atualiza automaticamente de verde/vermelho conforme o resultado da execução mais recente na branch padrão.

8. Erros comuns

  • Esquecer de definir java-version correspondendo ao maven.compiler.source: o build local passa, mas o CI falha com o erro release version X not supported. Sempre sincronize a versão do JDK entre pom.xml e workflow.
  • Usar mvn test em vez de mvn verify: se seu projeto tem testes de integração que rodam na fase verify (por exemplo, plugin Failsafe), mvn test vai pular eles, criando uma falsa sensação de segurança.
  • Cache com chave errada ao mudar dependências: se você escrever manualmente a action de cache (actions/cache) e esquecer de incluir pom.xml na chave hash, o cache antigo pode fazer o build usar dependências desatualizadas. Use cache: 'maven' do setup-java para evitar isso.
  • Executar CI em um runner self-hosted sem Docker/rede estável: Maven precisa baixar dependências do repositório central; se o runner estiver bloqueado de rede, o build vai travar na etapa de download. Verifique o proxy ou use um mirror Maven interno.

Nota: Se seu projeto é muito pequeno, tem apenas alguns testes simples e não faz deploy frequentemente, um workflow CI completo com matriz + relatório de cobertura pode ser exagerado. Comece com o workflow básico na seção 3 e depois expanda gradualmente à medida que o projeto cresce.

9. Conclusão

Com apenas algumas dezenas de linhas YAML, você tem um pipeline CI que automaticamente faz build, executa testes, cacheia dependências e publica relatórios para seu projeto Maven. Comece com um workflow simples, depois adicione matriz JDK, cobertura JaCoCo e badge README conforme necessário. O mais importante não é ter um workflow complexo, mas sim ter um CI confiável e estável que roda cada vez que você faz push do código.

github-actionsjavamavenci-cdtesting