Automating Java Development: A Complete Guide to CI/CD with GitHub Actions

Continuous Integration and Continuous Deployment (CI/CD) have become essential practices for modern software development. For Java developers, GitHub Actions provides a powerful, integrated platform to automate build, test, and deployment processes directly within your repository. This guide explores how to create robust CI/CD pipelines for Java projects using GitHub Actions.


What is GitHub Actions?

GitHub Actions is a CI/CD platform that allows you to automate your build, test, and deployment pipeline directly from your GitHub repository. You can create workflows that build and test every pull request to your repository or deploy merged pull requests to production.

Key Concepts:

  • Workflows: Automated procedures defined in YAML files
  • Events: Specific activities that trigger workflows (pushes, PRs, releases)
  • Jobs: Sets of steps that execute on the same runner
  • Steps: Individual tasks that run commands or actions
  • Actions: Reusable units of code for common tasks
  • Runners: Servers that execute your workflows

Setting Up Your First Java CI Workflow

Create a file at .github/workflows/ci.yml in your repository:

name: Java CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build with Maven
run: mvn -B compile
- name: Run tests
run: mvn -B test
- name: Run integration tests
run: mvn -B verify
env:
TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

Advanced Maven CI/CD Pipeline

Here's a comprehensive workflow for Maven projects:

name: Java CI/CD Pipeline
on:
push:
branches: [ main, develop, feature/* ]
pull_request:
branches: [ main, develop ]
release:
types: [ published ]
env:
MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.count=3
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Code analysis with SonarCloud
uses: SonarSource/sonarcloud-github-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=my-java-project
-Dsonar.organization=my-org
unit-tests:
runs-on: ubuntu-latest
needs: quality-gate
strategy:
matrix:
java: [ '11', '17', '21' ]
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
cache: 'maven'
- name: Run unit tests
run: mvn -B test
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-java-${{ matrix.java }}-${{ matrix.os }}
path: target/surefire-reports/
retention-days: 7
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Wait for database
run: sleep 15
- name: Run integration tests
run: mvn -B verify -Pintegration-tests
env:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/testdb
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: postgres
SPRING_REDIS_HOST: localhost
SPRING_REDIS_PORT: 6379
security-scan:
runs-on: ubuntu-latest
needs: integration-tests
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Dependency check
run: mvn org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=7
- name: Run security scan
uses: shiftleft/scan-action@v2
with:
output: reports
build-artifact:
runs-on: ubuntu-latest
needs: security-scan
if: github.ref == 'refs/heads/main' || github.event_name == 'release'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build and package
run: mvn -B clean package -DskipTests
- name: Upload JAR artifact
uses: actions/upload-artifact@v4
with:
name: java-app
path: target/*.jar
retention-days: 7
deploy-staging:
runs-on: ubuntu-latest
needs: build-artifact
if: github.ref == 'refs/heads/main'
environment: staging
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: java-app
- name: Deploy to staging
run: |
echo "Deploying to staging environment"
# Add your deployment commands here
# Example: scp *.jar user@staging-server:/app/
env:
DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
if: github.event_name == 'release' && github.event.action == 'published'
environment: production
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: java-app
- name: Deploy to production
run: |
echo "Deploying version ${{ github.event.release.tag_name }} to production"
# Add your production deployment commands
env:
PRODUCTION_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}

Gradle-Specific Workflow

For Gradle projects, here's a specialized workflow:

name: Java Gradle 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 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Build with Gradle
run: ./gradlew build
- name: Run tests
run: ./gradlew test
- name: Run integration tests
run: ./gradlew integrationTest
- name: Generate code coverage
run: ./gradlew jacocoTestReport
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./build/reports/jacoco/test/jacocoTestReport.xml
publish:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
server-id: github
settings-path: ${{ github.workspace }}
- name: Publish to GitHub Packages
run: ./gradlew publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Spring Boot-Specific Workflow

For Spring Boot applications with Docker:

name: Spring Boot CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
IMAGE_NAME: my-spring-app
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Run tests
run: mvn -B test
- name: Start containers for integration tests
run: docker-compose -f docker-compose.test.yml up -d
- name: Run integration tests
run: mvn -B verify -Pintegration-tests
build-docker:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t $IMAGE_NAME:${{ github.sha }} .
docker tag $IMAGE_NAME:${{ github.sha }} $IMAGE_NAME:latest
- name: Run Docker image tests
run: |
docker run -d --name test-container -p 8080:8080 $IMAGE_NAME:${{ github.sha }}
sleep 30
curl -f http://localhost:8080/actuator/health || exit 1
docker stop test-container
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push Docker image
run: |
docker tag $IMAGE_NAME:${{ github.sha }} myorg/$IMAGE_NAME:${{ github.sha }}
docker tag $IMAGE_NAME:${{ github.sha }} myorg/$IMAGE_NAME:latest
docker push myorg/$IMAGE_NAME:${{ github.sha }}
docker push myorg/$IMAGE_NAME:latest
deploy-kubernetes:
runs-on: ubuntu-latest
needs: build-docker
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
namespace: default
manifests: |
k8s/deployment.yml
k8s/service.yml
images: |
myorg/$IMAGE_NAME:${{ github.sha }}
kubectl-version: 'latest'

Dockerfile for Spring Boot:

FROM eclipse-temurin:17-jre-alpine
VOLUME /tmp
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

Multi-Module Maven Project Workflow

For complex multi-module projects:

name: Multi-Module Maven CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
module: [ 'core', 'web', 'api', 'persistence' ]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build module ${{ matrix.module }}
run: mvn -B -pl :${{ matrix.module }} clean compile
- name: Test module ${{ matrix.module }}
run: mvn -B -pl :${{ matrix.module }} test
integration:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build entire project
run: mvn -B clean install -DskipTests
- name: Run integration tests
run: mvn -B verify -Pintegration-tests

Performance Testing in CI/CD

name: Performance Tests
on:
schedule:
- cron: '0 2 * * 1'  # Run every Monday at 2 AM
jobs:
performance-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build application
run: mvn -B clean package -DskipTests
- name: Start application
run: |
java -jar target/myapp.jar &
echo $! > app.pid
sleep 60
- name: Run JMeter tests
uses: some-actions/jmeter-action@v2
with:
test-file: performance/test-plan.jmx
output-file: performance-results.jtl
- name: Stop application
run: kill $(cat app.pid) || true
- name: Analyze performance results
run: |
python scripts/analyze_performance.py performance-results.jtl

Best Practices for Java GitHub Actions

  1. Use Caching: Significantly reduces build times
- name: Cache Maven dependencies
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
  1. Matrix Testing: Test across multiple Java versions and OSes
strategy:
matrix:
java: [ '11', '17', '21' ]
os: [ubuntu-latest, windows-latest, macos-latest]
  1. Environment-Specific Configurations:
deploy:
environment:
name: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
url: ${{ steps.deploy.outputs.url }}
  1. Security Scanning:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
  1. Conditional Execution:
- name: Deploy
if: contains(github.event.head_commit.message, '[deploy]')
run: ./deploy.sh

Troubleshooting Common Issues

  1. Memory Issues: Add JVM options
- name: Build with increased memory
run: mvn -B compile -DargLine="-Xmx4g"
  1. Flaky Tests: Retry failed tests
- name: Run tests with retry
run: |
mvn -B test || mvn -B test
  1. Long Build Times: Parallelize jobs
jobs:
unit-tests:
# ... unit test setup
integration-tests:
# ... integration test setup
code-analysis:
# ... code analysis setup

Conclusion

GitHub Actions provides a powerful, flexible platform for implementing CI/CD pipelines for Java projects. By leveraging:

  • Matrix builds for comprehensive testing across environments
  • Dependency caching for faster builds
  • Container services for integration testing
  • Security scanning for vulnerability detection
  • Multi-environment deployments for smooth releases

You can create robust pipelines that ensure code quality, catch issues early, and automate deployments. The examples provided here serve as templates that can be adapted to your specific Java project needs, whether you're working with Maven, Gradle, Spring Boot, or other Java frameworks.

Remember to start simple and gradually add more advanced features as your team becomes comfortable with the CI/CD process.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper