CI Pipelines for a Java App
A 1-hour hands-on session: GitHub Actions and Jenkins, end to end, for a Maven-based Java application.
Session plan
| Time | Segment |
|---|---|
| 0:00 to 0:05 | Prerequisites check and repo setup |
| 0:05 to 0:15 | Build the sample Java app, tests, and Maven configuration |
| 0:15 to 0:25 | GitHub Actions workflow |
| 0:25 to 0:45 | Jenkins setup and Jenkinsfile |
| 0:45 to 0:55 | Trigger both pipelines, break the build, observe failures |
| 0:55 to 1:00 | Recap and next steps |
Prerequisites
- JDK 17 or later (Temurin recommended), Maven 3.9 or later, Git, and an IDE such as IntelliJ IDEA or VS Code
- A GitHub account and an empty public repository, for example
java-ci-demo - Docker Desktop (or Docker Engine on Linux) to run Jenkins locally
- Optional: a Jenkins installation on a VM if you don't want to use Docker
Verify the basics:
java -version
mvn -version
git --version
docker --version
Part 1: Sample Java app (0:05 to 0:15)
Create the project structure:
mkdir -p calc-ci-demo/src/main/java/com/example/calc
mkdir -p calc-ci-demo/src/test/java/com/example/calc
cd calc-ci-demo
git init -b main
package com.example.calc;
public final class Calculator {
public int add(int a, int b) {
return a + b;
}
public double divide(double a, double b) {
if (b == 0) {
throw new IllegalArgumentException("Cannot divide by zero");
}
return a / b;
}
}
package com.example.calc;
public final class App {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println("2 + 3 = " + calc.add(2, 3));
System.out.println("10 / 4 = " + calc.divide(10, 4));
}
}
package com.example.calc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
class CalculatorTest {
private final Calculator calc = new Calculator();
@Test
void add() {
assertEquals(5, calc.add(2, 3));
}
@Test
void divide() {
assertEquals(2.5, calc.divide(5, 2));
}
@Test
void divideByZeroThrows() {
assertThrows(IllegalArgumentException.class, () -> calc.divide(1, 0));
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>calc-ci-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<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>
</dependencies>
<build>
<plugins>
<!-- Lint step: -Werror turns every compiler warning into a build failure -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Werror</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.1</version>
<configuration>
<archive>
<manifest>
<mainClass>com.example.calc.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
target/
.idea/
*.iml
.vscode/
*.class
Run locally to confirm everything works before automating it:
mvn -B compile # lint: fails on any compiler warning
mvn -B verify # tests, coverage report, and jar
java -jar target/calc-ci-demo-1.0.0.jar
Test results land in target/surefire-reports/ and the coverage report in target/site/jacoco/index.html.
Commit and push:
git add . && git commit -m "Add sample app, tests, and Maven build"
git remote add origin https://github.com/<your-user>/java-ci-demo.git
git push -u origin main
Part 2: GitHub Actions (0:15 to 0:25)
Create the workflow file at .github/workflows/ci.yml:
name: Java CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
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:
distribution: temurin
java-version: ${{ matrix.java-version }}
cache: maven
- name: Lint (compile with warnings as errors)
run: mvn -B compile
- name: Test and coverage
run: mvn -B verify
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: surefire-reports-java${{ matrix.java-version }}
path: target/surefire-reports/
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: maven
- name: Package application
run: mvn -B -DskipTests package
- name: Upload jar
uses: actions/upload-artifact@v4
with:
name: calc-jar
path: target/*.jar
Commit and push, then check the Actions tab in GitHub. The matrix produces two test jobs, one for JDK 17 and one for JDK 21. The build job runs only after both pass.
Part 3: Jenkins (0:25 to 0:45)
3.1 Run Jenkins
The lts-jdk17 image already includes a JDK, so no Java install is needed:
docker run -d --name jenkins \
-p 8080:8080 -p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
jenkins/jenkins:lts-jdk17
Get the initial admin password and open the UI:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
Go to http://localhost:8080, paste the password, choose Install suggested plugins, and create an admin user.
3.2 Install Maven and Git in the Jenkins container
docker exec -u root jenkins bash -c "apt-get update && apt-get install -y maven git"
Verify both tools:
docker exec jenkins java -version
docker exec jenkins mvn -version
maven.compiler.release in the pom. If you change that value to 21, use a Jenkins image with JDK 21 (for example jenkins/jenkins:lts-jdk21).3.3 Add the Jenkinsfile to the repository
Create Jenkinsfile in the repo root:
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Lint') {
steps {
sh 'mvn -B compile'
}
}
stage('Test') {
steps {
sh 'mvn -B verify'
}
post {
always {
junit 'target/surefire-reports/*.xml'
archiveArtifacts artifacts: 'target/site/jacoco/**', allowEmptyArchive: true
}
}
}
stage('Package') {
steps {
sh 'mvn -B -DskipTests package'
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}
post {
always {
cleanWs()
}
}
}
Commit and push the Jenkinsfile:
git add Jenkinsfile && git commit -m "Add Jenkinsfile" && git push
3.4 Create the Jenkins pipeline job
- On the Jenkins dashboard, click New Item.
- Name it
java-ci-demo, select Pipeline, and click OK. - Under Build Triggers, check Poll SCM and enter
H/5 * * * *. A GitHub webhook is the better option for production, but it requires a publicly reachable Jenkins URL. - Under Pipeline, set Definition to Pipeline script from SCM.
- Set SCM to Git, enter your repository URL (
https://github.com/<your-user>/java-ci-demo.git), and set Branch Specifier to*/main. - Keep Script Path as
Jenkinsfileand click Save. - Click Build Now and open the console output to follow the run.
Part 4: Trigger, observe, and break things (0:45 to 0:55)
Introduce failures on purpose and watch both systems catch them.
Create a feature branch:
git checkout -b feature/break-build
Make two changes:
- In
CalculatorTest.java, changeassertEquals(5, calc.add(2, 3));toassertEquals(6, calc.add(2, 3));. This causes a test failure. - In
App.java, add a raw type insidemain:
This causes a compiler warning, whichjava.util.List raw = new java.util.ArrayList();-Werrorturns into a build failure at the lint step.
Commit, push, and open a pull request:
git add . && git commit -m "Introduce failures" && git push -u origin feature/break-build
Observe:
- GitHub Actions: the PR shows failed checks. The job log stops at the
mvn -B compilestep with a[rawtypes]warning promoted to an error. Because lint runs first, the tests never run. - Jenkins: the job fails at the Lint stage. The Test stage is skipped, so no JUnit report is published. Discuss why fail-fast ordering saves time.
Remove the raw type, fix the assertion, push again, and confirm both pipelines go green. Then merge the PR into main.
To see the test failure on its own, remove only the raw type and keep the wrong assertion. Lint passes, the test fails, and the JUnit report in both systems shows the failing add test.
Part 5: Recap and next steps (0:55 to 1:00)
Key concepts covered:
- CI triggers: push and pull request events in GitHub Actions versus SCM polling or webhooks in Jenkins
- Pipeline as code:
ci.ymlandJenkinsfileboth live in the repository - Lint as a compile gate:
-Xlint:all -Werrormakes warnings fail the build without extra tools - Fail-fast stages: lint before test, test before package
- Test reporting: Surefire writes JUnit XML that both platforms read
- Artifacts: the executable jar and coverage report are stored by each system
Suggested next steps:
- Add Checkstyle or SpotBugs for deeper static analysis, bound to the
verifyphase. - Enforce a coverage minimum with the JaCoCo
checkgoal. - Build a container image with Jib or a Dockerfile and push it to Azure Container Registry or Docker Hub.
- Store secrets in GitHub Secrets and Jenkins Credentials, not in the repository.
- Add a deployment stage gated on the
mainbranch, with a manual approval step in Jenkins. - Use the Maven Docker image (
maven:3.9-eclipse-temurin-17) as a Jenkins agent so each build runs in a clean container.
Troubleshooting quick reference
| Symptom | Likely cause and fix |
|---|---|
[ERROR] warnings found and -Werror specified | The lint step caught a compiler warning. Read the warning in the log, fix the code, or remove the -Werror argument if the team agrees to allow warnings. |
invalid target release: 17 or class file has wrong version | The JDK on the agent doesn't match maven.compiler.release. Use a JDK 17 or later image, or change the release value. |
mvn: not found in Jenkins | Re-run the apt-get install step in section 3.2 |
| JUnit report not shown in Jenkins | Confirm the JUnit plugin is installed and the path is target/surefire-reports/*.xml |
| Jenkins can't clone the repo | Use a public repo, or add a GitHub credential under Manage Jenkins, then Credentials |
| Dependencies download slowly in CI | Confirm cache: maven is set in setup-java, and that the first run has completed to populate the cache |
| Workflow doesn't trigger | Check the branches: filter and confirm the file is in .github/workflows/ |
| JaCoCo report missing | Confirm the verify phase ran (not just test), since the report goal is bound to verify |