The Maven Release Plugin automates the project release process, handling version management, SCM operations, and artifact deployment. This guide covers comprehensive usage, configuration, and customization.
Plugin Configuration
1. Basic POM Configuration
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.company</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<scm>
<connection>scm:git:https://github.com/company/my-project.git</connection>
<developerConnection>scm:git:https://github.com/company/my-project.git</developerConnection>
<url>https://github.com/company/my-project</url>
<tag>HEAD</tag>
</scm>
<distributionManagement>
<repository>
<id>company-releases</id>
<url>https://repo.company.com/repository/maven-releases</url>
</repository>
<snapshotRepository>
<id>company-snapshots</id>
<url>https://repo.company.com/repository/maven-snapshots</url>
</snapshotRepository>
</distributionManagement>
<build>
<plugins>
<!-- Maven Release Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<tagNameFormat>v@{project.version}</tagNameFormat>
<autoVersionSubmodules>true</autoVersionSubmodules>
<releaseProfiles>release</releaseProfiles>
<arguments>-DskipTests</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. Advanced Multi-Module Configuration
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.company</groupId>
<artifactId>parent-project</artifactId>
<version>2.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>core</module>
<module>api</module>
<module>web</module>
</modules>
<scm>
<connection>scm:git:https://github.com/company/parent-project.git</connection>
<developerConnection>scm:git:https://github.com/company/parent-project.git</developerConnection>
<url>https://github.com/company/parent-project</url>
<tag>HEAD</tag>
</scm>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<tagNameFormat>v@{project.version}</tagNameFormat>
<autoVersionSubmodules>true</autoVersionSubmodules>
<pushChanges>true</pushChanges>
<remoteTagging>true</remoteTagging>
<preparationGoals>clean verify</preparationGoals>
<completionGoals>deploy</completionGoals>
<releaseProfiles>release,sign</releaseProfiles>
<arguments>-Dgpg.skip=false -DskipTests</arguments>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Release Process Automation
1. Release Preparation Script
package com.company.release;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* Utility class to prepare and validate Maven release
*/
public class ReleasePreparer {
private static final String POM_XML = "pom.xml";
private static final String RELEASE_PROPERTIES = "release.properties";
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("Usage: ReleasePreparer <releaseVersion> [developmentVersion]");
System.exit(1);
}
String releaseVersion = args[0];
String devVersion = args.length > 1 ? args[1] : incrementVersion(releaseVersion) + "-SNAPSHOT";
try {
ReleasePreparer preparer = new ReleasePreparer();
preparer.prepareRelease(releaseVersion, devVersion);
} catch (ReleaseException e) {
System.err.println("Release preparation failed: " + e.getMessage());
System.exit(1);
}
}
public void prepareRelease(String releaseVersion, String devVersion) throws ReleaseException {
System.out.println("Preparing release: " + releaseVersion);
System.out.println("Next development version: " + devVersion);
// Validate current state
validateCleanWorkingDirectory();
validatePomExists();
validateVersionFormat(releaseVersion);
validateVersionFormat(devVersion);
// Create release properties
createReleaseProperties(releaseVersion, devVersion);
// Validate dependencies
validateDependencies();
System.out.println("Release preparation completed successfully");
System.out.println("Run: mvn release:prepare release:perform");
}
private void validateCleanWorkingDirectory() throws ReleaseException {
try {
Process process = Runtime.getRuntime().exec("git status --porcelain");
String output = readProcessOutput(process);
if (!output.trim().isEmpty()) {
throw new ReleaseException("Working directory is not clean. Commit or stash changes first.");
}
} catch (IOException e) {
throw new ReleaseException("Failed to check git status: " + e.getMessage());
}
}
private void validatePomExists() throws ReleaseException {
if (!Files.exists(Paths.get(POM_XML))) {
throw new ReleaseException("pom.xml not found in current directory");
}
}
private void validateVersionFormat(String version) throws ReleaseException {
if (version == null || version.trim().isEmpty()) {
throw new ReleaseException("Version cannot be empty");
}
// Basic version validation
if (!version.matches("^\\d+\\.\\d+\\.\\d+(-[A-Za-z0-9]+)?(-SNAPSHOT)?$")) {
throw new ReleaseException("Invalid version format: " + version);
}
}
private void createReleaseProperties(String releaseVersion, String devVersion) throws ReleaseException {
Properties props = new Properties();
props.setProperty("project.rel.com.company:my-project", releaseVersion);
props.setProperty("project.dev.com.company:my-project", devVersion);
props.setProperty("scm.tag", "v" + releaseVersion);
props.setProperty("pushChanges", "true");
props.setProperty("preparationGoals", "clean verify");
props.setProperty("completionGoals", "deploy");
try (OutputStream out = Files.newOutputStream(Paths.get(RELEASE_PROPERTIES))) {
props.store(out, "Release properties for version " + releaseVersion);
System.out.println("Created " + RELEASE_PROPERTIES);
} catch (IOException e) {
throw new ReleaseException("Failed to create release.properties: " + e.getMessage());
}
}
private void validateDependencies() throws ReleaseException {
try {
// Check for snapshot dependencies
Process process = Runtime.getRuntime().exec("mvn dependency:tree -Dincludes=*:*-SNAPSHOT");
String output = readProcessOutput(process);
if (output.contains("SNAPSHOT")) {
System.err.println("WARNING: Project contains SNAPSHOT dependencies:");
System.err.println(output);
// Uncomment to fail on snapshot dependencies
// throw new ReleaseException("Cannot release with SNAPSHOT dependencies");
}
} catch (IOException e) {
throw new ReleaseException("Failed to validate dependencies: " + e.getMessage());
}
}
private String readProcessOutput(Process process) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
private static String incrementVersion(String version) {
// Simple version increment - in real scenario, use better logic
String[] parts = version.split("\\.");
int patch = Integer.parseInt(parts[2].split("-")[0]);
return parts[0] + "." + parts[1] + "." + (patch + 1);
}
public static class ReleaseException extends Exception {
public ReleaseException(String message) {
super(message);
}
}
}
2. Release Manager Class
package com.company.release;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Manages the Maven release process programmatically
*/
public class MavenReleaseManager {
private final File projectDirectory;
private final Properties releaseProperties;
public MavenReleaseManager(File projectDirectory) {
this.projectDirectory = projectDirectory;
this.releaseProperties = new Properties();
}
public ReleaseResult prepareRelease(String releaseVersion, String devVersion) {
return prepareRelease(releaseVersion, devVersion, new ReleaseConfig());
}
public ReleaseResult prepareRelease(String releaseVersion, String devVersion, ReleaseConfig config) {
try {
System.out.println("Starting release preparation...");
// Validate inputs
validateReleaseVersion(releaseVersion);
validateDevVersion(devVersion);
// Execute mvn release:prepare
List<String> prepareCommand = buildPrepareCommand(releaseVersion, devVersion, config);
ProcessResult prepareResult = executeMavenCommand(prepareCommand, config.getTimeoutMinutes());
if (!prepareResult.isSuccess()) {
return ReleaseResult.failure("Prepare phase failed: " + prepareResult.getOutput());
}
System.out.println("Release preparation completed successfully");
return ReleaseResult.success(releaseVersion, prepareResult.getOutput());
} catch (Exception e) {
return ReleaseResult.failure("Release preparation failed: " + e.getMessage());
}
}
public ReleaseResult performRelease() {
return performRelease(new ReleaseConfig());
}
public ReleaseResult performRelease(ReleaseConfig config) {
try {
System.out.println("Starting release perform...");
// Execute mvn release:perform
List<String> performCommand = buildPerformCommand(config);
ProcessResult performResult = executeMavenCommand(performCommand, config.getTimeoutMinutes());
if (!performResult.isSuccess()) {
return ReleaseResult.failure("Perform phase failed: " + performResult.getOutput());
}
System.out.println("Release perform completed successfully");
return ReleaseResult.success(null, performResult.getOutput());
} catch (Exception e) {
return ReleaseResult.failure("Release perform failed: " + e.getMessage());
}
}
public ReleaseResult rollbackRelease() {
try {
System.out.println("Rolling back release...");
List<String> rollbackCommand = Arrays.asList(
"mvn", "release:rollback",
"-DautoVersionSubmodules=true"
);
ProcessResult rollbackResult = executeMavenCommand(rollbackCommand, 10);
if (rollbackResult.isSuccess()) {
// Clean up release properties
Files.deleteIfExists(projectDirectory.toPath().resolve("release.properties"));
System.out.println("Release rollback completed successfully");
return ReleaseResult.success("rolledback", rollbackResult.getOutput());
} else {
return ReleaseResult.failure("Rollback failed: " + rollbackResult.getOutput());
}
} catch (Exception e) {
return ReleaseResult.failure("Rollback failed: " + e.getMessage());
}
}
public ReleaseResult cleanRelease() {
try {
System.out.println("Cleaning release files...");
// Delete release properties and backup POMs
Files.deleteIfExists(projectDirectory.toPath().resolve("release.properties"));
Files.deleteIfExists(projectDirectory.toPath().resolve("pom.xml.releaseBackup"));
Files.deleteIfExists(projectDirectory.toPath().resolve("pom.xml.tag"));
Files.deleteIfExists(projectDirectory.toPath().resolve("pom.xml.next"));
Files.deleteIfExists(projectDirectory.toPath().resolve("pom.xml.branch"));
// Clean release plugin working files
Path releasePath = projectDirectory.toPath().resolve("target/release");
if (Files.exists(releasePath)) {
deleteDirectory(releasePath);
}
System.out.println("Release cleanup completed");
return ReleaseResult.success("cleaned", "Release files cleaned successfully");
} catch (Exception e) {
return ReleaseResult.failure("Clean failed: " + e.getMessage());
}
}
// Helper methods
private List<String> buildPrepareCommand(String releaseVersion, String devVersion, ReleaseConfig config) {
List<String> command = new ArrayList<>();
command.add("mvn");
command.add("release:prepare");
command.add("-DreleaseVersion=" + releaseVersion);
command.add("-DdevelopmentVersion=" + devVersion);
command.add("-DautoVersionSubmodules=true");
command.add("-DpushChanges=" + config.isPushChanges());
if (config.isDryRun()) {
command.add("-DdryRun=true");
}
if (config.getPreparationGoals() != null) {
command.add("-DpreparationGoals=" + config.getPreparationGoals());
}
if (config.getArguments() != null) {
command.add(config.getArguments());
}
return command;
}
private List<String> buildPerformCommand(ReleaseConfig config) {
List<String> command = new ArrayList<>();
command.add("mvn");
command.add("release:perform");
command.add("-DautoVersionSubmodules=true");
if (config.getCompletionGoals() != null) {
command.add("-DcompletionGoals=" + config.getCompletionGoals());
}
if (config.getArguments() != null) {
command.add(config.getArguments());
}
if (config.getReleaseProfiles() != null) {
command.add("-P" + config.getReleaseProfiles());
}
return command;
}
private ProcessResult executeMavenCommand(List<String> command, int timeoutMinutes) throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.directory(projectDirectory);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// Read output
String output = readProcessOutput(process);
// Wait for completion
boolean completed = process.waitFor(timeoutMinutes, TimeUnit.MINUTES);
if (!completed) {
process.destroyForcibly();
throw new RuntimeException("Maven command timed out after " + timeoutMinutes + " minutes");
}
int exitCode = process.exitValue();
return new ProcessResult(exitCode, output);
}
private String readProcessOutput(Process process) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
System.out.println(line); // Echo to console
}
return output.toString();
}
}
private void validateReleaseVersion(String version) {
if (version == null || version.endsWith("-SNAPSHOT")) {
throw new IllegalArgumentException("Release version cannot be SNAPSHOT: " + version);
}
}
private void validateDevVersion(String version) {
if (version == null || !version.endsWith("-SNAPSHOT")) {
throw new IllegalArgumentException("Development version must be SNAPSHOT: " + version);
}
}
private void deleteDirectory(Path path) throws IOException {
Files.walk(path)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
// Configuration class
public static class ReleaseConfig {
private boolean dryRun = false;
private boolean pushChanges = true;
private String preparationGoals = "clean verify";
private String completionGoals = "deploy";
private String arguments = "-DskipTests";
private String releaseProfiles = "release";
private int timeoutMinutes = 30;
// Getters and setters
public boolean isDryRun() { return dryRun; }
public void setDryRun(boolean dryRun) { this.dryRun = dryRun; }
public boolean isPushChanges() { return pushChanges; }
public void setPushChanges(boolean pushChanges) { this.pushChanges = pushChanges; }
public String getPreparationGoals() { return preparationGoals; }
public void setPreparationGoals(String preparationGoals) { this.preparationGoals = preparationGoals; }
public String getCompletionGoals() { return completionGoals; }
public void setCompletionGoals(String completionGoals) { this.completionGoals = completionGoals; }
public String getArguments() { return arguments; }
public void setArguments(String arguments) { this.arguments = arguments; }
public String getReleaseProfiles() { return releaseProfiles; }
public void setReleaseProfiles(String releaseProfiles) { this.releaseProfiles = releaseProfiles; }
public int getTimeoutMinutes() { return timeoutMinutes; }
public void setTimeoutMinutes(int timeoutMinutes) { this.timeoutMinutes = timeoutMinutes; }
}
// Result classes
public static class ReleaseResult {
private final boolean success;
private final String version;
private final String message;
private final String output;
private ReleaseResult(boolean success, String version, String message, String output) {
this.success = success;
this.version = version;
this.message = message;
this.output = output;
}
public static ReleaseResult success(String version, String output) {
return new ReleaseResult(true, version, "Release completed successfully", output);
}
public static ReleaseResult failure(String message) {
return new ReleaseResult(false, null, message, null);
}
// Getters
public boolean isSuccess() { return success; }
public String getVersion() { return version; }
public String getMessage() { return message; }
public String getOutput() { return output; }
}
private static class ProcessResult {
private final int exitCode;
private final String output;
public ProcessResult(int exitCode, String output) {
this.exitCode = exitCode;
this.output = output;
}
public boolean isSuccess() {
return exitCode == 0;
}
public String getOutput() {
return output;
}
}
}
3. Release Automation Service
package com.company.release;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.*;
/**
* Spring service for managing Maven releases
*/
@Service
public class ReleaseAutomationService {
private final MavenReleaseManager releaseManager;
private final ReleaseValidationService validationService;
public ReleaseAutomationService() {
this.releaseManager = new MavenReleaseManager(new File("."));
this.validationService = new ReleaseValidationService();
}
public ReleaseResult executeFullRelease(String releaseVersion, String devVersion) {
return executeFullRelease(releaseVersion, devVersion, new MavenReleaseManager.ReleaseConfig());
}
public ReleaseResult executeFullRelease(String releaseVersion, String devVersion,
MavenReleaseManager.ReleaseConfig config) {
try {
// Phase 1: Pre-release validation
ReleaseResult validationResult = validationService.validateReleaseReadiness();
if (!validationResult.isSuccess()) {
return validationResult;
}
// Phase 2: Release preparation
System.out.println("=== RELEASE PREPARATION PHASE ===");
ReleaseResult prepareResult = releaseManager.prepareRelease(releaseVersion, devVersion, config);
if (!prepareResult.isSuccess()) {
System.err.println("Release preparation failed, attempting rollback...");
releaseManager.rollbackRelease();
return prepareResult;
}
// Phase 3: Release performance
System.out.println("=== RELEASE PERFORMANCE PHASE ===");
ReleaseResult performResult = releaseManager.performRelease(config);
if (!performResult.isSuccess()) {
System.err.println("Release perform failed");
// Note: Cannot rollback after successful prepare as commits are pushed
return performResult;
}
// Phase 4: Post-release cleanup
System.out.println("=== POST-RELEASE CLEANUP ===");
releaseManager.cleanRelease();
String successMessage = String.format(
"Release %s completed successfully. Next development version: %s",
releaseVersion, devVersion
);
return ReleaseResult.success(releaseVersion, successMessage);
} catch (Exception e) {
return ReleaseResult.failure("Release process failed: " + e.getMessage());
}
}
public ReleaseResult dryRunRelease(String releaseVersion, String devVersion) {
MavenReleaseManager.ReleaseConfig config = new MavenReleaseManager.ReleaseConfig();
config.setDryRun(true);
config.setPushChanges(false);
return releaseManager.prepareRelease(releaseVersion, devVersion, config);
}
public List<ReleaseHistory> getReleaseHistory() {
// Implementation to read release history from git tags or file
return Collections.emptyList();
}
public ReleaseAnalysis analyzeReleaseImpact(String releaseVersion) {
// Analyze what changed in this release
return new ReleaseAnalysis(releaseVersion);
}
// Inner classes for additional functionality
public static class ReleaseHistory {
private String version;
private Date releaseDate;
private String commitHash;
private String releasedBy;
// Constructors, getters, setters
public ReleaseHistory(String version, Date releaseDate, String commitHash, String releasedBy) {
this.version = version;
this.releaseDate = releaseDate;
this.commitHash = commitHash;
this.releasedBy = releasedBy;
}
// Getters
public String getVersion() { return version; }
public Date getReleaseDate() { return releaseDate; }
public String getCommitHash() { return commitHash; }
public String getReleasedBy() { return releasedBy; }
}
public static class ReleaseAnalysis {
private String version;
private int commitCount;
private int fileChanges;
private List<String> contributors;
private Map<String, Integer> changeTypes;
public ReleaseAnalysis(String version) {
this.version = version;
this.contributors = new ArrayList<>();
this.changeTypes = new HashMap<>();
}
// Getters and setters
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public int getCommitCount() { return commitCount; }
public void setCommitCount(int commitCount) { this.commitCount = commitCount; }
public int getFileChanges() { return fileChanges; }
public void setFileChanges(int fileChanges) { this.fileChanges = fileChanges; }
public List<String> getContributors() { return contributors; }
public void setContributors(List<String> contributors) { this.contributors = contributors; }
public Map<String, Integer> getChangeTypes() { return changeTypes; }
public void setChangeTypes(Map<String, Integer> changeTypes) { this.changeTypes = changeTypes; }
}
}
4. Release Validation Service
package com.company.release;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Validates project state before release
*/
@Service
public class ReleaseValidationService {
public MavenReleaseManager.ReleaseResult validateReleaseReadiness() {
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
// Check 1: Clean working directory
if (!isWorkingDirectoryClean()) {
errors.add("Working directory is not clean. Commit or stash changes first.");
}
// Check 2: POM exists and is valid
if (!isPomValid()) {
errors.add("pom.xml is missing or invalid");
}
// Check 3: No SNAPSHOT dependencies
List<String> snapshotDeps = findSnapshotDependencies();
if (!snapshotDeps.isEmpty()) {
warnings.add("Project contains SNAPSHOT dependencies: " + snapshotDeps);
}
// Check 4: Tests pass
if (!runTests()) {
errors.add("Tests are failing. Fix tests before release.");
}
// Check 5: Git remote is accessible
if (!isGitRemoteAccessible()) {
warnings.add("Cannot verify git remote accessibility");
}
// Check 6: Repository configuration
if (!isRepositoryConfigured()) {
errors.add("Distribution management not configured in pom.xml");
}
if (!errors.isEmpty()) {
String errorMessage = "Release validation failed:\n- " +
String.join("\n- ", errors);
if (!warnings.isEmpty()) {
errorMessage += "\nWarnings:\n- " + String.join("\n- ", warnings);
}
return MavenReleaseManager.ReleaseResult.failure(errorMessage);
}
String message = "Release validation passed";
if (!warnings.isEmpty()) {
message += " with warnings:\n- " + String.join("\n- ", warnings);
}
return MavenReleaseManager.ReleaseResult.success("validated", message);
}
private boolean isWorkingDirectoryClean() {
try {
Process process = Runtime.getRuntime().exec("git status --porcelain");
String output = readProcessOutput(process);
return output.trim().isEmpty();
} catch (IOException e) {
return false;
}
}
private boolean isPomValid() {
Path pomPath = Paths.get("pom.xml");
if (!Files.exists(pomPath)) {
return false;
}
try {
// Basic XML validation
Process process = Runtime.getRuntime().exec(new String[]{"mvn", "validate", "-q"});
return process.waitFor() == 0;
} catch (Exception e) {
return false;
}
}
private List<String> findSnapshotDependencies() {
List<String> snapshots = new ArrayList<>();
try {
Process process = Runtime.getRuntime().exec(
new String[]{"mvn", "dependency:tree", "-Dincludes=*:*:*SNAPSHOT"});
String output = readProcessOutput(process);
// Parse output for snapshot dependencies
String[] lines = output.split("\n");
for (String line : lines) {
if (line.contains("SNAPSHOT") && !line.contains("Building")) {
snapshots.add(line.trim());
}
}
} catch (IOException e) {
// Ignore, return empty list
}
return snapshots;
}
private boolean runTests() {
try {
Process process = Runtime.getRuntime().exec(new String[]{"mvn", "test", "-q"});
return process.waitFor() == 0;
} catch (Exception e) {
return false;
}
}
private boolean isGitRemoteAccessible() {
try {
Process process = Runtime.getRuntime().exec(new String[]{"git", "remote", "-v"});
String output = readProcessOutput(process);
return !output.trim().isEmpty();
} catch (IOException e) {
return false;
}
}
private boolean isRepositoryConfigured() {
try {
String pomContent = new String(Files.readAllBytes(Paths.get("pom.xml")));
return pomContent.contains("distributionManagement") ||
pomContent.contains("repository");
} catch (IOException e) {
return false;
}
}
private String readProcessOutput(Process process) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
}
CI/CD Integration
1. Jenkins Pipeline
pipeline {
agent any
parameters {
string(name: 'RELEASE_VERSION', defaultValue: '', description: 'Release version (e.g., 1.2.0)')
string(name: 'NEXT_VERSION', defaultValue: '', description: 'Next development version (e.g., 1.3.0-SNAPSHOT)')
booleanParam(name: 'DRY_RUN', defaultValue: true, description: 'Dry run without actual release')
}
environment {
MAVEN_HOME = tool 'maven-3.8'
JAVA_HOME = tool 'jdk-17'
}
stages {
stage('Validate') {
steps {
script {
if (!params.RELEASE_VERSION) {
error "RELEASE_VERSION parameter is required"
}
if (!params.NEXT_VERSION) {
error "NEXT_VERSION parameter is required"
}
}
}
}
stage('Checkout') {
steps {
checkout scm
}
}
stage('Release Preparation') {
steps {
script {
if (params.DRY_RUN) {
sh """
${MAVEN_HOME}/bin/mvn release:prepare \
-DreleaseVersion=${params.RELEASE_VERSION} \
-DdevelopmentVersion=${params.NEXT_VERSION} \
-DdryRun=true \
-DpushChanges=false \
-Darguments="-DskipTests"
"""
} else {
sh """
${MAVEN_HOME}/bin/mvn release:prepare \
-DreleaseVersion=${params.RELEASE_VERSION} \
-DdevelopmentVersion=${params.NEXT_VERSION} \
-DpushChanges=true \
-Darguments="-DskipTests" \
-B
"""
}
}
}
}
stage('Release Perform') {
when {
expression { !params.DRY_RUN }
}
steps {
script {
withCredentials([usernamePassword(
credentialsId: 'nexus-credentials',
usernameVariable: 'NEXUS_USERNAME',
passwordVariable: 'NEXUS_PASSWORD'
)]) {
sh """
${MAVEN_HOME}/bin/mvn release:perform \
-Darguments="-DskipTests -Dnexus.username=${NEXUS_USERNAME} -Dnexus.password=${NEXUS_PASSWORD}" \
-B
"""
}
}
}
}
stage('Post Release') {
when {
expression { !params.DRY_RUN }
}
steps {
script {
// Create GitHub release
withCredentials([string(credentialsId: 'github-token', variable: 'GITHUB_TOKEN')]) {
sh """
curl -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/company/my-project/releases \
-d '{
"tag_name": "v${params.RELEASE_VERSION}",
"name": "Release v${params.RELEASE_VERSION}",
"body": "Automated release ${params.RELEASE_VERSION}",
"draft": false,
"prerelease": false
}'
"""
}
// Notify teams
slackSend(
channel: '#releases',
message: "Release ${params.RELEASE_VERSION} completed successfully! 🚀"
)
}
}
}
}
post {
always {
// Cleanup release properties
sh 'rm -f release.properties pom.xml.releaseBackup || true'
}
success {
echo "Release process completed successfully"
}
failure {
script {
// Attempt rollback on failure
sh """
${MAVEN_HOME}/bin/mvn release:rollback -DautoVersionSubmodules=true || true
"""
slackSend(
channel: '#build-failures',
message: "Release ${params.RELEASE_VERSION} failed! ❌"
)
}
}
}
}
2. GitHub Actions Workflow
name: Maven Release
on:
workflow_dispatch:
inputs:
releaseVersion:
description: 'Release version (e.g., 1.2.0)'
required: true
nextVersion:
description: 'Next development version (e.g., 1.3.0-SNAPSHOT)'
required: true
dryRun:
description: 'Dry run without actual release'
type: boolean
default: true
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Set up Maven
uses: stCarolas/setup-maven@v4
with:
maven-version: '3.8.6'
- name: Validate release parameters
run: |
if [ -z "${{ github.event.inputs.releaseVersion }}" ]; then
echo "Error: releaseVersion is required"
exit 1
fi
if [ -z "${{ github.event.inputs.nextVersion }}" ]; then
echo "Error: nextVersion is required"
exit 1
fi
- name: Configure Git
run: |
git config user.name "GitHub Actions"
git config user.email "[email protected]"
- name: Release Preparation
run: |
if [ "${{ github.event.inputs.dryRun }}" = "true" ]; then
mvn release:prepare \
-DreleaseVersion=${{ github.event.inputs.releaseVersion }} \
-DdevelopmentVersion=${{ github.event.inputs.nextVersion }} \
-DdryRun=true \
-DpushChanges=false \
-Darguments="-DskipTests" \
-B
else
mvn release:prepare \
-DreleaseVersion=${{ github.event.inputs.releaseVersion }} \
-DdevelopmentVersion=${{ github.event.inputs.nextVersion }} \
-DpushChanges=true \
-Darguments="-DskipTests" \
-B
fi
- name: Release Perform
if: github.event.inputs.dryRun == 'false'
run: |
mvn release:perform \
-Darguments="-DskipTests" \
-B
env:
NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
- name: Create GitHub Release
if: github.event.inputs.dryRun == 'false'
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.event.inputs.releaseVersion }}
release_name: Release v${{ github.event.inputs.releaseVersion }}
body: Automated release ${{ github.event.inputs.releaseVersion }}
draft: false
prerelease: false
- name: Cleanup
if: always()
run: |
rm -f release.properties pom.xml.releaseBackup || true
- name: Notify Slack
if: github.event.inputs.dryRun == 'false'
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: Release ${{ github.event.inputs.releaseVersion }} ${{ job.status }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Best Practices and Troubleshooting
1. Common Issues and Solutions
package com.company.release;
import java.util.*;
/**
* Troubleshooting guide for Maven Release Plugin issues
*/
public class ReleaseTroubleshooter {
public static Map<String, String> commonIssues = Map.of(
"scm-connection", "Check SCM configuration in pom.xml. Verify git URL and permissions.",
"clean-working-dir", "Ensure no uncommitted changes. Use 'git status' to check.",
"snapshot-dependencies", "Remove or update SNAPSHOT dependencies before release.",
"authentication", "Verify repository credentials and SSH keys for git push.",
"network-timeout", "Increase timeout for large projects: -DconnectionTimeout=240000",
"version-conflict", "Check for duplicate versions in multi-module projects.",
"tag-exists", "Delete existing git tag if re-releasing: git tag -d v1.0.0 && git push --delete origin v1.0.0"
);
public static String troubleshoot(String errorMessage) {
for (Map.Entry<String, String> entry : commonIssues.entrySet()) {
if (errorMessage.toLowerCase().contains(entry.getKey())) {
return "Issue: " + entry.getKey() + "\nSolution: " + entry.getValue();
}
}
return "No specific solution found. Check Maven logs for details.";
}
public static void printCommonCommands() {
System.out.println("""
Common Maven Release Commands:
# Dry run (no changes pushed)
mvn release:prepare -DdryRun=true
# Prepare release with specific versions
mvn release:prepare -DreleaseVersion=1.2.0 -DdevelopmentVersion=1.3.0-SNAPSHOT
# Perform release (deploy artifacts)
mvn release:perform
# Rollback failed release
mvn release:rollback
# Clean release files
rm release.properties pom.xml.releaseBackup
# Skip tests during release
mvn release:prepare -Darguments="-DskipTests"
# Release with specific profiles
mvn release:prepare -Prelease,sign
# Resume interrupted release
mvn release:prepare -Dresume=true
""");
}
}
2. Release Checklist
package com.company.release;
import java.util.*;
/**
* Pre-release checklist validation
*/
public class ReleaseChecklist {
private final List<ChecklistItem> items;
public ReleaseChecklist() {
this.items = Arrays.asList(
new ChecklistItem("Working directory clean", true),
new ChecklistItem("All tests passing", true),
new ChecklistItem("No SNAPSHOT dependencies", false),
new ChecklistItem("Version numbers updated", true),
new ChecklistItem("Changelog updated", false),
new ChecklistItem("Documentation updated", false),
new ChecklistItem("API compatibility verified", false),
new ChecklistItem("Performance tests passed", false),
new ChecklistItem("Security scan clean", false),
new ChecklistItem("Dependency licenses verified", false)
);
}
public ChecklistResult validate() {
List<String> passed = new ArrayList<>();
List<String> failed = new ArrayList<>();
List<String> warnings = new ArrayList<>();
for (ChecklistItem item : items) {
if (item.isVerified()) {
passed.add(item.getDescription());
} else if (item.isRequired()) {
failed.add(item.getDescription());
} else {
warnings.add(item.getDescription());
}
}
return new ChecklistResult(passed, failed, warnings);
}
public static class ChecklistItem {
private final String description;
private final boolean required;
private boolean verified;
public ChecklistItem(String description, boolean required) {
this.description = description;
this.required = required;
this.verified = false;
}
// Getters and setters
public String getDescription() { return description; }
public boolean isRequired() { return required; }
public boolean isVerified() { return verified; }
public void setVerified(boolean verified) { this.verified = verified; }
}
public static class ChecklistResult {
private final List<String> passed;
private final List<String> failed;
private final List<String> warnings;
public ChecklistResult(List<String> passed, List<String> failed, List<String> warnings) {
this.passed = passed;
this.failed = failed;
this.warnings = warnings;
}
public boolean canProceed() {
return failed.isEmpty();
}
public String getSummary() {
return String.format("Passed: %d, Failed: %d, Warnings: %d",
passed.size(), failed.size(), warnings.size());
}
// Getters
public List<String> getPassed() { return passed; }
public List<String> getFailed() { return failed; }
public List<String> getWarnings() { return warnings; }
}
}
Summary
The Maven Release Plugin provides a robust, standardized approach to project releases. Key benefits include:
- Automated Version Management: Handles version updates across modules
- SCM Integration: Automates tagging and commits
- Artifact Deployment: Deploys releases to configured repositories
- Rollback Support: Provides safety nets for failed releases
- CI/CD Integration: Works seamlessly with Jenkins, GitHub Actions, etc.
By combining the Maven Release Plugin with custom Java tooling, you can create a comprehensive release automation system that ensures consistency, reliability, and traceability across your project releases.