CI/CD Basics

Continuous Integration and Continuous Deployment for Java Projects

← Back to Index

What is CI/CD?

CI/CD stands for Continuous Integration and Continuous Deployment (or Delivery). It's a set of practices that automate the building, testing, and deployment of applications, enabling teams to deliver software faster and more reliably.

CI/CD Components
  • Continuous Integration (CI) - Automatically build and test code when changes are pushed
  • Continuous Delivery - Automatically prepare releases for deployment (manual trigger)
  • Continuous Deployment - Automatically deploy to production after passing all tests

The CI/CD Pipeline

// Typical CI/CD Pipeline Flow

Code Push → Build → Unit Tests → Integration Tests → Package → Deploy

// Detailed stages:
1. Source        // Developer pushes code to Git
2. Build         // Compile Java code (mvn compile)
3. Unit Test     // Run unit tests (mvn test)
4. Code Analysis // SonarQube, checkstyle
5. Package       // Create JAR/WAR (mvn package)
6. Integration   // Run integration tests
7. Deploy Stage  // Deploy to staging environment
8. Deploy Prod   // Deploy to production

Benefits of CI/CD

For Development Teams

For Business

Popular CI/CD Tools

Tool Type Best For
GitHub Actions Cloud-hosted GitHub repositories, easy setup
Jenkins Self-hosted Enterprise, highly customizable
GitLab CI Cloud/Self-hosted GitLab users, integrated DevOps
CircleCI Cloud-hosted Fast builds, Docker support
Azure DevOps Cloud-hosted Microsoft ecosystem, enterprise
Travis CI Cloud-hosted Open source projects

GitHub Actions for Java

GitHub Actions is integrated directly into GitHub repositories, making it easy to set up CI/CD pipelines.

Basic Java Build Workflow

# .github/workflows/build.yml
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 package --file pom.xml

    - name: Run tests
      run: mvn test

    - name: Upload artifact
      uses: actions/upload-artifact@v4
      with:
        name: app-jar
        path: target/*.jar

Multi-Version Testing

# Test on multiple Java versions
name: Multi-Version CI

on: [push, pull_request]

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

    steps:
    - 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: Build and Test
      run: mvn -B verify

Spring Boot Deployment

# Deploy Spring Boot to Docker Hub
name: Build and Push Docker

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]

jobs:
  build:
    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: Build with Maven
      run: mvn -B package -DskipTests

    - name: Login to Docker Hub
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_TOKEN }}

    - name: Build and Push Docker Image
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: |
          username/myapp:latest
          username/myapp:${{ github.sha }}

Jenkins Pipeline

Jenkins is the most widely used self-hosted CI/CD server, offering maximum flexibility and control.

Declarative Pipeline (Jenkinsfile)

// Jenkinsfile in project root
pipeline {
    agent any

    tools {
        maven 'Maven-3.9'
        jdk 'JDK-17'
    }

    environment {
        APP_NAME = 'my-java-app'
        DEPLOY_ENV = 'staging'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean compile'
            }
        }

        stage('Unit Tests') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }

        stage('Code Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Package') {
            steps {
                sh 'mvn package -DskipTests'
                archiveArtifacts artifacts: 'target/*.jar'
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                sh './deploy.sh staging'
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                input message: 'Deploy to production?'
                sh './deploy.sh production'
            }
        }
    }

    post {
        success {
            slackSend color: 'good', message: "Build successful: ${env.JOB_NAME}"
        }
        failure {
            slackSend color: 'danger', message: "Build failed: ${env.JOB_NAME}"
        }
    }
}

Parallel Stages

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                sh 'mvn clean compile'
            }
        }

        stage('Tests') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'mvn test -Dtest=*UnitTest'
                    }
                }
                stage('Integration Tests') {
                    steps {
                        sh 'mvn test -Dtest=*IntegrationTest'
                    }
                }
                stage('Code Coverage') {
                    steps {
                        sh 'mvn jacoco:report'
                    }
                }
            }
        }
    }
}

GitLab CI/CD

GitLab CI/CD is integrated into GitLab and configured via a .gitlab-ci.yml file.

# .gitlab-ci.yml
image: maven:3.9-eclipse-temurin-17

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

cache:
  paths:
    - .m2/repository/

stages:
  - build
  - test
  - analyze
  - package
  - deploy

build:
  stage: build
  script:
    - mvn compile

unit-tests:
  stage: test
  script:
    - mvn test
  artifacts:
    reports:
      junit: target/surefire-reports/TEST-*.xml

integration-tests:
  stage: test
  script:
    - mvn verify -DskipUnitTests
  artifacts:
    reports:
      junit: target/failsafe-reports/TEST-*.xml

sonarqube:
  stage: analyze
  script:
    - mvn sonar:sonar -Dsonar.host.url=$SONAR_URL
  only:
    - main
    - develop

package:
  stage: package
  script:
    - mvn package -DskipTests
  artifacts:
    paths:
      - target/*.jar

deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - develop

deploy-production:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
    url: https://example.com
  when: manual
  only:
    - main

Maven Configuration for CI/CD

CI-Friendly POM Configuration

<!-- pom.xml -->
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>${revision}</version>

    <properties>
        <revision>1.0.0-SNAPSHOT</revision>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <!-- Flatten POM for CI -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>flatten-maven-plugin</artifactId>
                <version>1.5.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>flatten</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- Surefire for unit tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.2</version>
            </plugin>

            <!-- Failsafe for integration tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>3.2.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- JaCoCo for code coverage -->
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.11</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>
        </plugins>
    </build>
</project>

Maven CI Commands

# Batch mode (no interactive prompts)
mvn -B clean install

# Set version from CI
mvn -B -Drevision=1.2.3 clean package

# Skip tests for quick builds
mvn -B package -DskipTests

# Run only unit tests
mvn test

# Run only integration tests
mvn verify -DskipUnitTests

# Generate site with reports
mvn site

Docker in CI/CD

Dockerfile for Spring Boot

# Multi-stage Dockerfile
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn -B package -DskipTests

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar

# Create non-root user
RUN addgroup -S spring && adduser -S spring -G spring
USER spring

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Docker Compose for Testing

# docker-compose.yml for integration tests
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=test
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/testdb
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=testdb
      - POSTGRES_USER=test
      - POSTGRES_PASSWORD=test
    ports:
      - "5432:5432"

CI/CD Best Practices

Pipeline Best Practices
  • Fail Fast - Run quick checks (lint, compile) before slow tests
  • Cache Dependencies - Speed up builds by caching Maven/Gradle dependencies
  • Parallelize - Run independent stages concurrently
  • Use Artifacts - Build once, deploy the same artifact everywhere
  • Keep Pipelines Short - Target under 10 minutes for feedback
  • Secure Secrets - Never hardcode credentials, use secret management

Security Scanning

# GitHub Actions with security scanning
name: Security Scan

on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Run OWASP Dependency Check
      uses: dependency-check/Dependency-Check_Action@main
      with:
        project: 'my-app'
        path: '.'
        format: 'HTML'

    - name: Upload Report
      uses: actions/upload-artifact@v4
      with:
        name: dependency-check-report
        path: reports/

Deployment Strategies

Blue-Green Deployment

// Two identical production environments
Blue (current)  <-- Production traffic
Green (new)     <-- Deploy and test new version

// After testing, switch traffic
Blue (old)
Green (current) <-- Production traffic now here

// Easy rollback: switch back to Blue

Canary Deployment

// Gradual rollout to subset of users
v1.0 (current) <-- 95% of traffic
v1.1 (canary)  <-- 5% of traffic

// Monitor metrics, gradually increase
v1.0 (current) <-- 80% of traffic
v1.1 (canary)  <-- 20% of traffic

// Full rollout when confident
v1.1           <-- 100% of traffic

Rolling Deployment

// Update instances one at a time
Instance 1: v1.0 -> v1.1 (updating)
Instance 2: v1.0
Instance 3: v1.0

Instance 1: v1.1 (done)
Instance 2: v1.0 -> v1.1 (updating)
Instance 3: v1.0

// Continue until all updated