The Maven Release Plugin automates the project release process, handling version management, SCM operations, and artifact deployment in a standardized way.
Core Concepts
What is Maven Release Plugin?
- Automates the release process for Maven projects
- Manages version numbers in POM files
- Integrates with Version Control Systems (Git, SVN)
- Creates release tags and updates development versions
- Deploys artifacts to repositories
Key Goals:
- Standardization: Consistent release process across projects
- Automation: Reduce manual errors in release tasks
- Traceability: Clear version history and tags
- Reproducibility: Reliable release artifacts
Plugin Configuration
Basic POM Configuration
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>My Project</name>
<description>A sample project demonstrating Maven Release Plugin</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-release-plugin.version>3.0.0</maven-release-plugin.version>
<scm-plugin.version>1.13.0</scm-plugin.version>
</properties>
<!-- SCM Configuration -->
<scm>
<connection>scm:git:https://github.com/example/my-project.git</connection>
<developerConnection>scm:git:https://github.com/example/my-project.git</developerConnection>
<url>https://github.com/example/my-project</url>
<tag>HEAD</tag>
</scm>
<build>
<plugins>
<!-- Maven Release Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>${maven-release-plugin.version}</version>
<configuration>
<tagNameFormat>v@{project.version}</tagNameFormat>
<autoVersionSubmodules>true</autoVersionSubmodules>
<releaseProfiles>release</releaseProfiles>
<goals>deploy</goals>
<arguments>-DskipTests</arguments>
</configuration>
</plugin>
<!-- SCM Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-scm-plugin</artifactId>
<version>${scm-plugin.version}</version>
</plugin>
</plugins>
</build>
<!-- Distribution Management -->
<distributionManagement>
<repository>
<id>nexus-releases</id>
<url>https://nexus.example.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<url>https://nexus.example.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<!-- Profiles -->
<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>
Advanced Configuration
<!-- Advanced release configuration -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>${maven-release-plugin.version}</version>
<configuration>
<!-- Version control -->
<tagNameFormat>v@{project.version}</tagNameFormat>
<tagBase>https://github.com/example/my-project/tags</tagBase>
<!-- Version management -->
<autoVersionSubmodules>true</autoVersionSubmodules>
<useReleaseProfile>true</useReleaseProfile>
<releaseProfiles>release</releaseProfiles>
<preparationGoals>clean verify</preparationGoals>
<completionGoals>deploy</completionGoals>
<!-- Branch management (for Git flow) -->
<branchName>release/@{project.version}</branchName>
<pushChanges>true</pushChanges>
<remoteTagging>true</remoteTagging>
<!-- Rollback configuration -->
<rollback>true</rollback>
<suppressCommitBeforeTag>false</suppressCommitBeforeTag>
<!-- Additional arguments -->
<arguments>-DskipTests -Dgpg.skip=false</arguments>
<goals>deploy</goals>
</configuration>
</plugin>
Release Process Implementation
1. Release Preparation Service
@Component
@Slf4j
public class ReleasePreparationService {
private final MavenInvoker mavenInvoker;
private final GitService gitService;
private final ProjectValidator projectValidator;
public ReleasePreparationService(MavenInvoker mavenInvoker, GitService gitService,
ProjectValidator projectValidator) {
this.mavenInvoker = mavenInvoker;
this.gitService = gitService;
this.projectValidator = projectValidator;
}
public ReleasePreparationResult prepareRelease(ReleaseRequest request) {
log.info("Preparing release for project: {}", request.getProjectPath());
try {
// 1. Validate project state
ValidationResult validation = projectValidator.validateProject(request);
if (!validation.isValid()) {
return ReleasePreparationResult.failure(validation.getErrors());
}
// 2. Check for uncommitted changes
if (!gitService.isCleanWorkingDirectory(request.getProjectPath())) {
return ReleasePreparationResult.failure(
"Working directory has uncommitted changes");
}
// 3. Run release preparation
MavenResult preparationResult = mavenInvoker.executeGoal(
request.getProjectPath(),
"release:prepare",
buildPreparationProperties(request)
);
if (!preparationResult.isSuccess()) {
return ReleasePreparationResult.failure(
"Release preparation failed: " + preparationResult.getOutput());
}
return ReleasePreparationResult.success(
"Release preparation completed successfully",
extractReleaseVersion(preparationResult.getOutput())
);
} catch (Exception e) {
log.error("Release preparation failed", e);
return ReleasePreparationResult.failure("Preparation failed: " + e.getMessage());
}
}
public ReleaseRollbackResult rollbackPrepare(ReleaseRequest request) {
log.info("Rolling back release preparation for: {}", request.getProjectPath());
try {
MavenResult rollbackResult = mavenInvoker.executeGoal(
request.getProjectPath(),
"release:rollback",
Map.of("releaseVersion", request.getReleaseVersion())
);
if (rollbackResult.isSuccess()) {
return ReleaseRollbackResult.success("Release preparation rolled back successfully");
} else {
return ReleaseRollbackResult.failure(
"Rollback failed: " + rollbackResult.getOutput());
}
} catch (Exception e) {
log.error("Release rollback failed", e);
return ReleaseRollbackResult.failure("Rollback failed: " + e.getMessage());
}
}
private Map<String, String> buildPreparationProperties(ReleaseRequest request) {
Map<String, String> properties = new HashMap<>();
if (request.getReleaseVersion() != null) {
properties.put("releaseVersion", request.getReleaseVersion());
}
if (request.getDevelopmentVersion() != null) {
properties.put("developmentVersion", request.getDevelopmentVersion());
}
if (request.getTag() != null) {
properties.put("tag", request.getTag());
}
properties.put("autoVersionSubmodules", "true");
properties.put("pushChanges", String.valueOf(request.isPushChanges()));
return properties;
}
private String extractReleaseVersion(String output) {
// Parse release version from Maven output
Pattern pattern = Pattern.compile("release version: (\\d+\\.\\d+\\.\\d+)");
Matcher matcher = pattern.matcher(output);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
2. Release Performance Service
@Component
@Slf4j
public class ReleasePerformanceService {
private final MavenInvoker mavenInvoker;
private final ArtifactDeployer artifactDeployer;
public ReleasePerformanceService(MavenInvoker mavenInvoker,
ArtifactDeployer artifactDeployer) {
this.mavenInvoker = mavenInvoker;
this.artifactDeployer = artifactDeployer;
}
public ReleasePerformanceResult performRelease(ReleaseRequest request) {
log.info("Performing release for project: {}", request.getProjectPath());
long startTime = System.currentTimeMillis();
try {
// Execute release:perform
MavenResult performResult = mavenInvoker.executeGoal(
request.getProjectPath(),
"release:perform",
buildPerformanceProperties(request)
);
long duration = System.currentTimeMillis() - startTime;
if (performResult.isSuccess()) {
// Verify artifacts were deployed
boolean artifactsDeployed = artifactDeployer.verifyArtifactsDeployed(
request.getGroupId(),
request.getArtifactId(),
request.getReleaseVersion()
);
return ReleasePerformanceResult.success(
"Release performed successfully",
duration,
artifactsDeployed
);
} else {
return ReleasePerformanceResult.failure(
"Release performance failed: " + performResult.getOutput(),
duration
);
}
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
log.error("Release performance failed", e);
return ReleasePerformanceResult.failure(
"Performance failed: " + e.getMessage(), duration);
}
}
public ReleaseResult performFullRelease(ReleaseRequest request) {
log.info("Starting full release process for: {}", request.getProjectPath());
// 1. Prepare release
ReleasePreparationResult preparation = prepareRelease(request);
if (!preparation.isSuccess()) {
return ReleaseResult.failure("Preparation failed: " + preparation.getMessage());
}
// 2. Perform release
ReleasePerformanceResult performance = performRelease(request);
if (!performance.isSuccess()) {
// Attempt rollback on failure
rollbackPrepare(request);
return ReleaseResult.failure("Performance failed: " + performance.getMessage());
}
return ReleaseResult.success(
"Release completed successfully",
preparation.getReleaseVersion(),
performance.getDuration()
);
}
private Map<String, String> buildPerformanceProperties(ReleaseRequest request) {
Map<String, String> properties = new HashMap<>();
properties.put("releaseProfiles", "release");
properties.put("goals", "deploy");
properties.put("arguments", "-DskipTests");
if (request.getWorkingDirectory() != null) {
properties.put("workingDirectory", request.getWorkingDirectory());
}
return properties;
}
}
3. Maven Invoker Service
@Component
@Slf4j
public class MavenInvoker {
private final Invoker invoker;
public MavenInvoker() {
this.invoker = new DefaultInvoker();
// Configure Maven home
String mavenHome = System.getenv("M2_HOME");
if (mavenHome != null) {
invoker.setMavenHome(new File(mavenHome));
}
// Configure local repository
String localRepo = System.getProperty("maven.repo.local");
if (localRepo != null) {
invoker.setLocalRepositoryDirectory(new File(localRepo));
}
}
public MavenResult executeGoal(String projectPath, String goal,
Map<String, String> properties) {
return executeGoal(projectPath, goal, properties, null);
}
public MavenResult executeGoal(String projectPath, String goal,
Map<String, String> properties, List<String> profiles) {
log.debug("Executing Maven goal: {} on project: {}", goal, projectPath);
InvocationRequest request = new DefaultInvocationRequest();
request.setBaseDirectory(new File(projectPath));
request.setGoals(Collections.singletonList(goal));
// Set properties
if (properties != null && !properties.isEmpty()) {
Properties props = new Properties();
props.putAll(properties);
request.setProperties(props);
}
// Set profiles
if (profiles != null && !profiles.isEmpty()) {
request.setProfiles(profiles);
}
// Configure output
request.setOutputHandler(this::handleOutput);
request.setErrorHandler(this::handleError);
try {
long startTime = System.currentTimeMillis();
InvocationResult result = invoker.execute(request);
long duration = System.currentTimeMillis() - startTime;
return MavenResult.builder()
.success(result.getExitCode() == 0)
.exitCode(result.getExitCode())
.executionException(result.getExecutionException())
.duration(duration)
.build();
} catch (MavenInvocationException e) {
log.error("Maven invocation failed for goal: {}", goal, e);
return MavenResult.builder()
.success(false)
.executionException(e)
.build();
}
}
public MavenResult executeReleasePrepare(String projectPath, ReleaseRequest releaseRequest) {
Map<String, String> properties = new HashMap<>();
if (releaseRequest.getReleaseVersion() != null) {
properties.put("releaseVersion", releaseRequest.getReleaseVersion());
}
if (releaseRequest.getDevelopmentVersion() != null) {
properties.put("developmentVersion", releaseRequest.getDevelopmentVersion());
}
if (releaseRequest.getTag() != null) {
properties.put("tag", releaseRequest.getTag());
}
properties.put("autoVersionSubmodules", "true");
properties.put("pushChanges", String.valueOf(releaseRequest.isPushChanges()));
return executeGoal(projectPath, "release:prepare", properties);
}
public MavenResult executeReleasePerform(String projectPath, ReleaseRequest releaseRequest) {
Map<String, String> properties = new HashMap<>();
properties.put("releaseProfiles", "release");
properties.put("goals", "deploy");
List<String> profiles = Arrays.asList("release");
return executeGoal(projectPath, "release:perform", properties, profiles);
}
private void handleOutput(String message) {
log.info("Maven Output: {}", message);
}
private void handleError(String message) {
log.error("Maven Error: {}", message);
}
}
4. Git Integration Service
@Component
@Slf4j
public class GitService {
public boolean isCleanWorkingDirectory(String projectPath) {
try {
Git git = Git.open(new File(projectPath));
Status status = git.status().call();
return status.isClean();
} catch (Exception e) {
log.error("Failed to check Git status for: {}", projectPath, e);
return false;
}
}
public String getCurrentBranch(String projectPath) {
try {
Git git = Git.open(new File(projectPath));
return git.getRepository().getBranch();
} catch (Exception e) {
log.error("Failed to get current branch for: {}", projectPath, e);
return null;
}
}
public boolean verifyRemoteConnection(String projectPath) {
try {
Git git = Git.open(new File(projectPath));
git.fetch().call();
return true;
} catch (Exception e) {
log.error("Failed to verify remote connection for: {}", projectPath, e);
return false;
}
}
public List<String> getRecentTags(String projectPath, int count) {
try {
Git git = Git.open(new File(projectPath));
List<Ref> tags = git.tagList().call();
return tags.stream()
.map(Ref::getName)
.map(name -> name.replace("refs/tags/", ""))
.sorted(Comparator.reverseOrder())
.limit(count)
.collect(Collectors.toList());
} catch (Exception e) {
log.error("Failed to get tags for: {}", projectPath, e);
return Collections.emptyList();
}
}
public boolean tagExists(String projectPath, String tagName) {
try {
Git git = Git.open(new File(projectPath));
List<Ref> tags = git.tagList().call();
return tags.stream()
.anyMatch(tag -> tag.getName().equals("refs/tags/" + tagName));
} catch (Exception e) {
log.error("Failed to check tag existence for: {}", projectPath, e);
return false;
}
}
}
5. Project Validation Service
@Component
@Slf4j
public class ProjectValidator {
public ValidationResult validateProject(ReleaseRequest request) {
List<String> errors = new ArrayList<>();
String projectPath = request.getProjectPath();
// Check if project directory exists
File projectDir = new File(projectPath);
if (!projectDir.exists()) {
errors.add("Project directory does not exist: " + projectPath);
return ValidationResult.invalid(errors);
}
// Check if pom.xml exists
File pomFile = new File(projectPath, "pom.xml");
if (!pomFile.exists()) {
errors.add("pom.xml not found in project directory");
return ValidationResult.invalid(errors);
}
// Validate SCM configuration
ValidationResult scmValidation = validateScmConfiguration(pomFile);
if (!scmValidation.isValid()) {
errors.addAll(scmValidation.getErrors());
}
// Validate distribution management
ValidationResult dmValidation = validateDistributionManagement(pomFile);
if (!dmValidation.isValid()) {
errors.addAll(dmValidation.getErrors());
}
// Validate version format
if (request.getReleaseVersion() != null) {
ValidationResult versionValidation = validateVersionFormat(request.getReleaseVersion());
if (!versionValidation.isValid()) {
errors.addAll(versionValidation.getErrors());
}
}
return errors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(errors);
}
private ValidationResult validateScmConfiguration(File pomFile) {
List<String> errors = new ArrayList<>();
try {
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(new FileReader(pomFile));
Scm scm = model.getScm();
if (scm == null) {
errors.add("SCM configuration is missing in pom.xml");
} else {
if (scm.getConnection() == null) {
errors.add("SCM connection is missing");
}
if (scm.getDeveloperConnection() == null) {
errors.add("SCM developer connection is missing");
}
if (scm.getUrl() == null) {
errors.add("SCM URL is missing");
}
}
} catch (Exception e) {
errors.add("Failed to read pom.xml: " + e.getMessage());
}
return errors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(errors);
}
private ValidationResult validateDistributionManagement(File pomFile) {
List<String> errors = new ArrayList<>();
try {
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(new FileReader(pomFile));
DistributionManagement dm = model.getDistributionManagement();
if (dm == null) {
errors.add("Distribution management configuration is missing");
} else {
if (dm.getRepository() == null) {
errors.add("Release repository is not configured");
}
if (dm.getSnapshotRepository() == null) {
errors.add("Snapshot repository is not configured");
}
}
} catch (Exception e) {
errors.add("Failed to read distribution management: " + e.getMessage());
}
return errors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(errors);
}
private ValidationResult validateVersionFormat(String version) {
List<String> errors = new ArrayList<>();
// Basic version format validation
if (!version.matches("^\\d+\\.\\d+\\.\\d+$")) {
errors.add("Invalid version format: " + version + ". Expected format: X.Y.Z");
}
// Check if version doesn't contain SNAPSHOT
if (version.toUpperCase().contains("SNAPSHOT")) {
errors.add("Release version should not contain SNAPSHOT: " + version);
}
return errors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(errors);
}
}
Release Management Models
Data Transfer Objects
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReleaseRequest {
@NotBlank
private String projectPath;
private String releaseVersion;
private String developmentVersion;
private String tag;
private String workingDirectory;
private String groupId;
private String artifactId;
@Builder.Default
private boolean pushChanges = true;
@Builder.Default
private boolean dryRun = false;
private Map<String, String> additionalProperties;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MavenResult {
private boolean success;
private int exitCode;
private Exception executionException;
private String output;
private long duration;
public static MavenResult success(long duration) {
return MavenResult.builder()
.success(true)
.duration(duration)
.build();
}
public static MavenResult failure(String error, long duration) {
return MavenResult.builder()
.success(false)
.output(error)
.duration(duration)
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReleasePreparationResult {
private boolean success;
private String message;
private String releaseVersion;
private List<String> errors;
public static ReleasePreparationResult success(String message, String releaseVersion) {
return ReleasePreparationResult.builder()
.success(true)
.message(message)
.releaseVersion(releaseVersion)
.errors(Collections.emptyList())
.build();
}
public static ReleasePreparationResult failure(String error) {
return ReleasePreparationResult.builder()
.success(false)
.message(error)
.errors(Collections.singletonList(error))
.build();
}
public static ReleasePreparationResult failure(List<String> errors) {
return ReleasePreparationResult.builder()
.success(false)
.message(String.join(", ", errors))
.errors(errors)
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReleasePerformanceResult {
private boolean success;
private String message;
private long duration;
private boolean artifactsDeployed;
public static ReleasePerformanceResult success(String message, long duration,
boolean artifactsDeployed) {
return ReleasePerformanceResult.builder()
.success(true)
.message(message)
.duration(duration)
.artifactsDeployed(artifactsDeployed)
.build();
}
public static ReleasePerformanceResult failure(String message, long duration) {
return ReleasePerformanceResult.builder()
.success(false)
.message(message)
.duration(duration)
.artifactsDeployed(false)
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReleaseResult {
private boolean success;
private String message;
private String releaseVersion;
private long preparationDuration;
private long performanceDuration;
private Instant timestamp;
public static ReleaseResult success(String message, String releaseVersion,
long performanceDuration) {
return ReleaseResult.builder()
.success(true)
.message(message)
.releaseVersion(releaseVersion)
.performanceDuration(performanceDuration)
.timestamp(Instant.now())
.build();
}
public static ReleaseResult failure(String message) {
return ReleaseResult.builder()
.success(false)
.message(message)
.timestamp(Instant.now())
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReleaseRollbackResult {
private boolean success;
private String message;
public static ReleaseRollbackResult success(String message) {
return ReleaseRollbackResult.builder()
.success(true)
.message(message)
.build();
}
public static ReleaseRollbackResult failure(String message) {
return ReleaseRollbackResult.builder()
.success(false)
.message(message)
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ValidationResult {
private boolean valid;
private List<String> errors;
private List<String> warnings;
public static ValidationResult valid() {
return ValidationResult.builder()
.valid(true)
.errors(Collections.emptyList())
.warnings(Collections.emptyList())
.build();
}
public static ValidationResult invalid(List<String> errors) {
return ValidationResult.builder()
.valid(false)
.errors(errors)
.warnings(Collections.emptyList())
.build();
}
public static ValidationResult withWarnings(List<String> warnings) {
return ValidationResult.builder()
.valid(true)
.errors(Collections.emptyList())
.warnings(warnings)
.build();
}
}
Release Manager Service
@Service
@Slf4j
public class ReleaseManagerService {
private final ReleasePreparationService preparationService;
private final ReleasePerformanceService performanceService;
private final GitService gitService;
private final ProjectValidator projectValidator;
private final SlackAlertService slackAlertService;
public ReleaseManagerService(ReleasePreparationService preparationService,
ReleasePerformanceService performanceService,
GitService gitService,
ProjectValidator projectValidator,
SlackAlertService slackAlertService) {
this.preparationService = preparationService;
this.performanceService = performanceService;
this.gitService = gitService;
this.projectValidator = projectValidator;
this.slackAlertService = slackAlertService;
}
public ReleaseResult executeRelease(ReleaseRequest request) {
log.info("Executing release for project: {}", request.getProjectPath());
try {
// Pre-release validation
ValidationResult validation = preReleaseValidation(request);
if (!validation.isValid()) {
return ReleaseResult.failure("Pre-release validation failed: " +
String.join(", ", validation.getErrors()));
}
// Execute full release
ReleaseResult result = performanceService.performFullRelease(request);
// Send notification
sendReleaseNotification(request, result);
return result;
} catch (Exception e) {
log.error("Release execution failed", e);
sendReleaseFailureNotification(request, e.getMessage());
return ReleaseResult.failure("Release execution failed: " + e.getMessage());
}
}
public ReleaseResult dryRunRelease(ReleaseRequest request) {
log.info("Performing dry-run release for project: {}", request.getProjectPath());
try {
// Set dry-run flag
request.setDryRun(true);
// Pre-release validation
ValidationResult validation = preReleaseValidation(request);
if (!validation.isValid()) {
return ReleaseResult.failure("Dry-run validation failed: " +
String.join(", ", validation.getErrors()));
}
// Only run preparation (which does most of the validation)
ReleasePreparationResult preparation = preparationService.prepareRelease(request);
if (preparation.isSuccess()) {
// Rollback the dry-run changes
preparationService.rollbackPrepare(request);
return ReleaseResult.success(
"Dry-run completed successfully - no changes were made",
preparation.getReleaseVersion(),
0
);
} else {
return ReleaseResult.failure("Dry-run failed: " + preparation.getMessage());
}
} catch (Exception e) {
log.error("Dry-run release failed", e);
return ReleaseResult.failure("Dry-run failed: " + e.getMessage());
}
}
public List<ReleaseHistoryItem> getReleaseHistory(String projectPath, int limit) {
List<ReleaseHistoryItem> history = new ArrayList<>();
try {
// Get recent tags from Git
List<String> recentTags = gitService.getRecentTags(projectPath, limit);
for (String tag : recentTags) {
ReleaseHistoryItem item = ReleaseHistoryItem.builder()
.tag(tag)
.timestamp(getTagTimestamp(projectPath, tag))
.build();
history.add(item);
}
} catch (Exception e) {
log.error("Failed to get release history for: {}", projectPath, e);
}
return history;
}
private ValidationResult preReleaseValidation(ReleaseRequest request) {
List<String> errors = new ArrayList<>();
// Basic project validation
ValidationResult projectValidation = projectValidator.validateProject(request);
if (!projectValidation.isValid()) {
errors.addAll(projectValidation.getErrors());
}
// Git validation
if (!gitService.isCleanWorkingDirectory(request.getProjectPath())) {
errors.add("Working directory has uncommitted changes");
}
if (!gitService.verifyRemoteConnection(request.getProjectPath())) {
errors.add("Cannot connect to remote repository");
}
// Version validation
if (request.getReleaseVersion() != null &&
gitService.tagExists(request.getProjectPath(), request.getReleaseVersion())) {
errors.add("Tag already exists: " + request.getReleaseVersion());
}
return errors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(errors);
}
private Instant getTagTimestamp(String projectPath, String tag) {
// Implementation to get tag timestamp from Git
// This would use JGit to get the tag's commit timestamp
return Instant.now(); // Simplified
}
private void sendReleaseNotification(ReleaseRequest request, ReleaseResult result) {
if (result.isSuccess()) {
slackAlertService.sendRichAlert(AlertRequest.builder()
.level(AlertRequest.AlertLevel.INFO)
.title("Release Successful")
.message(String.format("Release %s completed successfully",
result.getReleaseVersion()))
.details(Map.of(
"project", request.getProjectPath(),
"version", result.getReleaseVersion(),
"duration", result.getPerformanceDuration() + "ms"
))
.build());
}
}
private void sendReleaseFailureNotification(ReleaseRequest request, String error) {
slackAlertService.sendRichAlert(AlertRequest.builder()
.level(AlertRequest.AlertLevel.ERROR)
.title("Release Failed")
.message(String.format("Release for %s failed", request.getProjectPath()))
.details(Map.of(
"project", request.getProjectPath(),
"error", error
))
.build());
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ReleaseHistoryItem {
private String tag;
private Instant timestamp;
private String commitHash;
private String author;
}
REST Controller
@RestController
@RequestMapping("/api/releases")
@Slf4j
public class ReleaseController {
private final ReleaseManagerService releaseManager;
public ReleaseController(ReleaseManagerService releaseManager) {
this.releaseManager = releaseManager;
}
@PostMapping("/execute")
public ResponseEntity<ReleaseResult> executeRelease(@Valid @RequestBody ReleaseRequest request) {
log.info("Received release request for project: {}", request.getProjectPath());
ReleaseResult result = releaseManager.executeRelease(request);
if (result.isSuccess()) {
return ResponseEntity.ok(result);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
}
@PostMapping("/dry-run")
public ResponseEntity<ReleaseResult> dryRunRelease(@Valid @RequestBody ReleaseRequest request) {
log.info("Received dry-run request for project: {}", request.getProjectPath());
ReleaseResult result = releaseManager.dryRunRelease(request);
if (result.isSuccess()) {
return ResponseEntity.ok(result);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
}
@GetMapping("/history/{projectPath}")
public ResponseEntity<List<ReleaseHistoryItem>> getReleaseHistory(
@PathVariable String projectPath,
@RequestParam(defaultValue = "10") int limit) {
List<ReleaseHistoryItem> history = releaseManager.getReleaseHistory(projectPath, limit);
return ResponseEntity.ok(history);
}
@PostMapping("/validate")
public ResponseEntity<ValidationResult> validateRelease(@Valid @RequestBody ReleaseRequest request) {
// This would call the validation logic directly
return ResponseEntity.ok(ValidationResult.valid()); // Simplified
}
}
Advanced Release Strategies
1. Multi-Module Release Handler
@Component
@Slf4j
public class MultiModuleReleaseHandler {
private final ReleaseManagerService releaseManager;
private final MavenInvoker mavenInvoker;
public MultiModuleReleaseHandler(ReleaseManagerService releaseManager,
MavenInvoker mavenInvoker) {
this.releaseManager = releaseManager;
this.mavenInvoker = mavenInvoker;
}
public MultiModuleReleaseResult releaseMultiModuleProject(ReleaseRequest request) {
log.info("Releasing multi-module project: {}", request.getProjectPath());
List<ModuleReleaseResult> moduleResults = new ArrayList<>();
try {
// First, release the parent POM
ModuleReleaseResult parentResult = releaseParentModule(request);
moduleResults.add(parentResult);
if (!parentResult.isSuccess()) {
return MultiModuleReleaseResult.failure(
"Parent module release failed", moduleResults);
}
// Then release all submodules
List<String> submodules = getSubmodules(request.getProjectPath());
for (String submodule : submodules) {
ModuleReleaseResult moduleResult = releaseSubmodule(request, submodule);
moduleResults.add(moduleResult);
if (!moduleResult.isSuccess()) {
// Continue with other modules or abort based on strategy
log.warn("Submodule release failed: {}", submodule);
}
}
return MultiModuleReleaseResult.success("Multi-module release completed", moduleResults);
} catch (Exception e) {
log.error("Multi-module release failed", e);
return MultiModuleReleaseResult.failure(
"Multi-module release failed: " + e.getMessage(), moduleResults);
}
}
private ModuleReleaseResult releaseParentModule(ReleaseRequest request) {
ReleaseRequest parentRequest = createParentReleaseRequest(request);
ReleaseResult result = releaseManager.executeRelease(parentRequest);
return ModuleReleaseResult.builder()
.moduleName("parent")
.success(result.isSuccess())
.message(result.getMessage())
.version(result.getReleaseVersion())
.build();
}
private ModuleReleaseResult releaseSubmodule(ReleaseRequest parentRequest, String submodule) {
String submodulePath = parentRequest.getProjectPath() + "/" + submodule;
ReleaseRequest submoduleRequest = ReleaseRequest.builder()
.projectPath(submodulePath)
.releaseVersion(parentRequest.getReleaseVersion())
.developmentVersion(parentRequest.getDevelopmentVersion())
.pushChanges(parentRequest.isPushChanges())
.build();
ReleaseResult result = releaseManager.executeRelease(submoduleRequest);
return ModuleReleaseResult.builder()
.moduleName(submodule)
.success(result.isSuccess())
.message(result.getMessage())
.version(result.getReleaseVersion())
.build();
}
private List<String> getSubmodules(String projectPath) {
// Parse pom.xml to get submodules
// This would read the parent POM and extract module names
return Arrays.asList("module1", "module2", "module3"); // Simplified
}
private ReleaseRequest createParentReleaseRequest(ReleaseRequest original) {
return ReleaseRequest.builder()
.projectPath(original.getProjectPath())
.releaseVersion(original.getReleaseVersion())
.developmentVersion(original.getDevelopmentVersion())
.pushChanges(original.isPushChanges())
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class MultiModuleReleaseResult {
private boolean success;
private String message;
private List<ModuleReleaseResult> moduleResults;
private Instant timestamp;
public static MultiModuleReleaseResult success(String message,
List<ModuleReleaseResult> moduleResults) {
return MultiModuleReleaseResult.builder()
.success(true)
.message(message)
.moduleResults(moduleResults)
.timestamp(Instant.now())
.build();
}
public static MultiModuleReleaseResult failure(String message,
List<ModuleReleaseResult> moduleResults) {
return MultiModuleReleaseResult.builder()
.success(false)
.message(message)
.moduleResults(moduleResults)
.timestamp(Instant.now())
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ModuleReleaseResult {
private String moduleName;
private boolean success;
private String message;
private String version;
private long duration;
}
2. Release Version Calculator
@Component
@Slf4j
public class ReleaseVersionCalculator {
public VersionCalculation calculateNextVersions(String currentVersion,
VersionStrategy strategy) {
try {
String releaseVersion;
String nextDevelopmentVersion;
switch (strategy) {
case MAJOR:
releaseVersion = incrementMajor(currentVersion);
nextDevelopmentVersion = releaseVersion + "-SNAPSHOT";
break;
case MINOR:
releaseVersion = incrementMinor(currentVersion);
nextDevelopmentVersion = releaseVersion + "-SNAPSHOT";
break;
case PATCH:
releaseVersion = incrementPatch(currentVersion);
nextDevelopmentVersion = releaseVersion + "-SNAPSHOT";
break;
case CUSTOM:
// For custom, we expect the versions to be provided
return VersionCalculation.builder()
.strategy(strategy)
.build();
default:
throw new IllegalArgumentException("Unknown version strategy: " + strategy);
}
return VersionCalculation.builder()
.strategy(strategy)
.currentVersion(currentVersion)
.releaseVersion(releaseVersion)
.nextDevelopmentVersion(nextDevelopmentVersion)
.build();
} catch (Exception e) {
log.error("Version calculation failed", e);
throw new VersionCalculationException("Failed to calculate versions", e);
}
}
public boolean isValidVersion(String version) {
return version.matches("^\\d+\\.\\d+\\.\\d+(-SNAPSHOT)?$");
}
public String stripSnapshot(String version) {
if (version == null) return null;
return version.replace("-SNAPSHOT", "");
}
public String ensureSnapshot(String version) {
if (version == null) return null;
if (version.endsWith("-SNAPSHOT")) {
return version;
}
return version + "-SNAPSHOT";
}
private String incrementMajor(String version) {
String cleanVersion = stripSnapshot(version);
String[] parts = cleanVersion.split("\\.");
int major = Integer.parseInt(parts[0]) + 1;
return major + ".0.0";
}
private String incrementMinor(String version) {
String cleanVersion = stripSnapshot(version);
String[] parts = cleanVersion.split("\\.");
int minor = Integer.parseInt(parts[1]) + 1;
return parts[0] + "." + minor + ".0";
}
private String incrementPatch(String version) {
String cleanVersion = stripSnapshot(version);
String[] parts = cleanVersion.split("\\.");
int patch = Integer.parseInt(parts[2]) + 1;
return parts[0] + "." + parts[1] + "." + patch;
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class VersionCalculation {
private VersionStrategy strategy;
private String currentVersion;
private String releaseVersion;
private String nextDevelopmentVersion;
public boolean isValid() {
return releaseVersion != null && nextDevelopmentVersion != null;
}
}
enum VersionStrategy {
MAJOR, MINOR, PATCH, CUSTOM
}
class VersionCalculationException extends RuntimeException {
public VersionCalculationException(String message) {
super(message);
}
public VersionCalculationException(String message, Throwable cause) {
super(message, cause);
}
}
Configuration
# application.yml
release:
maven:
home: ${M2_HOME:/usr/share/maven}
settings: ${M2_SETTINGS:~/.m2/settings.xml}
git:
enabled: true
push-changes: true
require-clean-workspace: true
validation:
enabled: true
check-dependencies: true
check-scm: true
notifications:
slack:
enabled: true
webhook-url: ${SLACK_WEBHOOK_URL:}
email:
enabled: false
logging:
level:
com.example.release: INFO
Best Practices
- Always Use Dry Runs: Test the release process with dry-run first
- Clean Workspace: Ensure no uncommitted changes before release
- Backup Configuration: Backup settings.xml and other config files
- Monitor Dependencies: Check for snapshot dependencies in release
- Use Release Profiles: Separate build configurations for releases
- Automate Validation: Implement pre-release validation checks
@Component
public class ReleaseBestPracticesValidator {
public List<String> validateBestPractices(ReleaseRequest request) {
List<String> warnings = new ArrayList<>();
// Check for snapshot dependencies
if (hasSnapshotDependencies(request.getProjectPath())) {
warnings.add("Project has SNAPSHOT dependencies");
}
// Check if tests are enabled
if (request.getAdditionalProperties() != null &&
request.getAdditionalProperties().containsKey("skipTests") &&
"true".equals(request.getAdditionalProperties().get("skipTests"))) {
warnings.add("Tests are disabled for release");
}
// Check if signing is configured
if (!isSigningConfigured(request.getProjectPath())) {
warnings.add("Artifact signing is not configured");
}
return warnings;
}
private boolean hasSnapshotDependencies(String projectPath) {
// Check pom.xml for SNAPSHOT dependencies
return false; // Implementation would parse POM
}
private boolean isSigningConfigured(String projectPath) {
// Check if GPG signing is configured in POM
return false; // Implementation would check for maven-gpg-plugin
}
}
Conclusion
The Maven Release Plugin integration in Java provides:
- Automated Release Process: Streamlined, consistent releases
- Version Management: Automatic version number handling
- SCM Integration: Git operations and tagging
- Validation & Safety: Comprehensive pre-release checks
- Multi-module Support: Coordinated releases of complex projects
- Monitoring & Alerting: Integration with notification systems
This implementation enables teams to automate their Maven release processes, reduce manual errors, and maintain consistent release practices across projects. The combination of programmatic control, validation, and integration with other systems creates a robust release management solution.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.