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
- 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- - Matrix Testing: Test across multiple Java versions and OSes
strategy: matrix: java: [ '11', '17', '21' ] os: [ubuntu-latest, windows-latest, macos-latest]
- Environment-Specific Configurations:
deploy: environment: name: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} url: ${{ steps.deploy.outputs.url }} - Security Scanning:
- name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'sarif' output: 'trivy-results.sarif'
- Conditional Execution:
- name: Deploy if: contains(github.event.head_commit.message, '[deploy]') run: ./deploy.sh
Troubleshooting Common Issues
- Memory Issues: Add JVM options
- name: Build with increased memory run: mvn -B compile -DargLine="-Xmx4g"
- Flaky Tests: Retry failed tests
- name: Run tests with retry run: | mvn -B test || mvn -B test
- 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.