Migrating Maven Build Architectures from Jenkins to GitLab CI/CD

The transition of Java-based build processes from Jenkins to GitLab CI/CD represents a fundamental shift in how continuous integration and continuous delivery are conceptualized and executed. At the core of this transition is the migration of Maven-driven projects, which utilize the Project Object Model (POM) for dependency management and build lifecycle orchestration. For organizations utilizing GitLab.com, GitLab Self-Managed, or GitLab Dedicated, this migration is accessible across all subscription tiers, including Free, Premium, and Ultimate. By leveraging Java Spring project templates, developers can effectively transition legacy Jenkins build definitions into a modern, YAML-based declarative pipeline.

The architectural disparity between Jenkins and GitLab CI/CD is most evident in the execution environment. Jenkins often relies on a single, persistent agent—a virtual or physical machine where Maven and the Java Development Kit (JDK) are pre-installed and maintained manually. In contrast, GitLab CI/CD promotes an ephemeral, containerized approach, although it supports shell-based execution for those mimicking the persistent agent model. The goal of a successful migration is to replicate the three critical Maven lifecycle phases: testing, packaging, and installing, while improving the portability and scalability of the build environment.

Analysis of Legacy Jenkins Maven Configurations

Before executing a migration, it is necessary to understand the three primary methods used in Jenkins to manage Maven builds. Each method varies in its configuration delivery but ultimately executes the same sequence of Maven commands.

The first method is Freestyle with shell execution. In this scenario, the Jenkins configuration calls mvn commands directly from the shell on the agent. This is the most basic form of automation, where the sequence of commands is defined in the Jenkins UI.

The second method involves the Freestyle with Maven task plugin. This approach uses a specific plugin to declare and execute goals within the Maven build lifecycle. While it provides a more structured UI for Maven goals, it still requires the Jenkins agent to have Maven pre-installed and utilizes a script wrapper to execute the commands.

The third method is the use of a declarative pipeline via a Jenkinsfile. This represents the "Pipeline as Code" philosophy. A typical declarative pipeline for a Maven project is structured as follows:

groovy pipeline { agent any tools { maven 'maven-3.6.3' jdk 'jdk11' } stages { stage('Build') { steps { sh "mvn package -DskipTests" } } stage('Test') { steps { sh "mvn test" } } stage('Install') { steps { sh "mvn install -DskipTests" } } } }

In all three Jenkins scenarios, the pipeline executes three specific operations in a strict order:

  • mvn test: This command identifies and runs all tests found within the codebase.
  • mvn package -DskipTests: This compiles the code into an executable type as defined in the POM. The -DskipTests flag is critical here to avoid redundant test execution, as the tests were already validated in the previous stage.
  • mvn install -DskipTests: This installs the compiled executable into the agent's local .m2 repository, again skipping tests to optimize execution time.

Transitioning to GitLab CI/CD via Shell Executor

The most direct path for migrating from a persistent Jenkins agent to GitLab is by using a GitLab Runner configured with a Shell executor. This method most closely mimics the Jenkins environment because it relies on the host machine's installed software rather than a Docker container.

To achieve this, the environment must meet specific prerequisites. A GitLab Runner with a Shell executor must be active, and the host machine must have Maven 3.6.3 and the Java 11 JDK installed and configured in the system path.

The resulting .gitlab-ci.yml configuration file transforms the Jenkins stages into "jobs" grouped within "stages." The migration uses global keywords to define the pipeline's structure and environment variables.

The following configuration represents the shell-based migration:

```yaml
stages:
- build
- test
- install

variables:
MAVENOPTS: >-
-Dhttps.protocols=TLSv1.2
-Dmaven.repo.local=$CI
PROJECTDIR/.m2/repository
MAVEN
CLI_OPTS: >-
-DskipTests

build-JAR:
stage: build
script:
- mvn $MAVENCLIOPTS package

test-code:
stage: test
script:
- mvn test

install-JAR:
stage: install
script:
- mvn $MAVENCLIOPTS install
```

In this architecture, the stages keyword defines the execution order. The variables section is used to optimize Maven's behavior. MAVEN_OPTS is used to set the TLS protocol to version 1.2 via -Dhttps.protocols=TLSv1.2, ensuring secure HTTP requests. Furthermore, -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository redirects the local Maven repository to the project directory. This is a critical change; it allows the job to access and modify the repository within the scope of the GitLab project directory on the runner, rather than relying on a global system path. The MAVEN_CLI_OPTS variable contains -DskipTests, which is passed to the mvn commands to prevent the test phase from running during packaging and installation.

Advanced Containerized Implementation with Docker

While the shell executor is a valid migration path, GitLab CI/CD offers a more robust, scalable approach using Docker images. This method removes the requirement for Maven and JDK to be pre-installed on the runner's host machine, instead packaging them within a versioned container. This is particularly beneficial for users of GitLab.com who utilize public instance runners.

The advanced configuration introduces the default keyword and cache mechanisms to improve build efficiency.

```yaml
stages:
- build
- test
- install

default:
image: maven:3.6.3-openjdk-11

cache:
key: $CICOMMITREF_SLUG
paths:
- .m2/

variables:
MAVENOPTS: >-
-Dhttps.protocols=TLSv1.2
-Dmaven.repo.local=$CI
PROJECTDIR/.m2/repository
MAVEN
CLI_OPTS: >-
-DskipTests

build-JAR:
stage: build
script:
- mvn $MAVENCLIOPTS package

test-code:
stage: test
script:
- mvn test

install-JAR:
stage: install
script:
- mvn $MAVENCLIOPTS install
```

The default section defines the image as maven:3.6.3-openjdk-11. This ensures that every job in the pipeline executes within a consistent environment containing the exact version of Maven and Java required. This eliminates the "it works on my machine" problem often found in persistent Jenkins agents.

The cache configuration is a significant improvement over the Jenkins model. By setting the key to $CI_COMMIT_REF_SLUG (a shortened version of the Git commit reference), GitLab ensures that any job running for the same commit reference reuses the same cache. The paths attribute specifies the .m2/ directory. This prevents the pipeline from re-downloading all Maven dependencies from the central repository on every single run, drastically reducing build times and network overhead.

Deep Dive into GitLab CI/CD Componentry

To fully understand the migration, one must examine the specific functions of the keywords used in the .gitlab-ci.yml file.

The stages keyword defines the linear progression of the pipeline. In this Maven example, the stages are build, test, and install. Each stage acts as a container for one or more jobs. The jobs in the build stage must complete successfully before the test stage begins, and so on.

The variables section serves as the global environment configuration.
- MAVEN_OPTS handles the underlying JVM and Maven system properties. The use of $CI_PROJECT_DIR ensures that the local repository is stored within the build's working directory, which is essential for the GitLab cache to function.
- MAVEN_CLI_OPTS acts as a shortcut for common command-line arguments, specifically -DskipTests, which ensures that the build-JAR and install-JAR jobs do not waste time running tests that are already handled by the test-code job.

The individual jobs—build-JAR, test-code, and install-JAR—are the actual units of execution.
- The stage attribute assigns the job to its respective phase.
- The script attribute contains the shell commands. This is the direct equivalent to the steps block in a Jenkinsfile or the shell execution block in a Jenkins Freestyle project.

Comparison of Execution Models

The following table illustrates the differences between the legacy Jenkins approach and the migrated GitLab CI/CD approach.

Feature Jenkins (Legacy) GitLab CI/CD (Migrated)
Agent Type Persistent Shell Agent Ephemeral Docker Container / Shell Runner
Configuration UI-based or Jenkinsfile .gitlab-ci.yml (YAML)
Dependency Management Pre-installed on Agent Docker Image / Cached .m2 directory
Execution Logic Steps within Stages Jobs within Stages
Environment Isolation Low (Shared Agent) High (Containerized)
Scalability Limited by Agent Count Highly Scalable via Runners

Broader Ecosystem and Integration Use Cases

Beyond simple Maven builds, GitLab CI/CD provides a wide array of resources and example projects that can be forked and adapted. The versatility of the platform allows it to handle various language stacks and deployment targets.

The following table details specific use cases and the corresponding resources available for integration:

Use Case Resource Goal
Clojure Testing a Clojure application
Game Development Setting up CI/CD for game development
Java with Maven Deploying Maven projects to Artifactory
Java with Spring Boot Deploying a Spring Boot application to Cloud Foundry
Parallel Testing Running parallel tests for Ruby and JavaScript
Python on Heroku Testing and deploying a Python application to Heroku
Review Apps Setting up review apps with NGINX
Ruby on Heroku Testing and deploying a Ruby application to Heroku
Scala on Heroku Testing and deploying a Scala application to Heroku

For Java developers specifically, the ability to deploy Maven projects to Artifactory or Spring Boot applications to Cloud Foundry demonstrates that the migration from Jenkins is not merely about moving a build script, but about integrating the entire software delivery lifecycle into a single platform.

Conclusion: Technical Analysis of the Migration Impact

The migration from Jenkins to GitLab CI/CD for Maven projects is more than a syntax change; it is a transition toward immutable infrastructure. By moving from a persistent Jenkins agent to a containerized GitLab Runner, the "snowflake server" problem—where an agent's state becomes drifted and irreproducible over time—is eliminated.

The implementation of the .m2 repository within the $CI_PROJECT_DIR combined with the $CI_COMMIT_REF_SLUG cache key transforms the dependency management process. In Jenkins, the .m2 folder is typically a hidden directory on the agent's disk that grows indefinitely. In GitLab, the cache is managed, versioned, and tied to the specific branch or commit, ensuring that dependencies are consistent across different pipeline runs.

Furthermore, the shift to a declarative YAML structure allows the build logic to be versioned alongside the code. This provides a clear audit trail of how the build process evolved, which is often obscured in Jenkins' Freestyle projects. The use of MAVEN_CLI_OPTS and MAVEN_OPTS variables provides a clean separation between system-level requirements (TLS versions and repository paths) and build-level requirements (skipping tests), resulting in a maintainable and transparent pipeline.

Sources

  1. Migrate a Maven build from Jenkins to GitLab CI/CD
  2. GitLab CI/CD Examples

Related Posts