google-site-verification: google61fe8ba583a51912.html
Semantic Versioning in Java: Implementation and Automation

Semantic Versioning (SemVer) is a versioning scheme that provides meaning to version numbers, helping communicate the nature of changes between releases. This guide covers implementing SemVer parsing, validation, and automation in Java applications.


Core Concepts

Semantic Versioning Format:

MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]

Version Components:

  • MAJOR: Incompatible API changes
  • MINOR: Backward-compatible new functionality
  • PATCH: Backward-compatible bug fixes
  • PRERELEASE: Pre-release versions (alpha, beta, rc)
  • BUILD: Build metadata (ignored in version comparisons)

Dependencies and Setup

Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <maven-plugin-api.version>3.9.5</maven-plugin-api.version> </properties> <dependencies> <!-- Spring Boot (for web applications) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> <optional>true</optional> </dependency> <!-- Maven Plugin API (for Maven integration) --> <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-plugin-api</artifactId> <version>${maven-plugin-api.version}</version> <optional>true</optional> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> </dependencies>

Core Implementation

1. Semantic Version Model
public class SemanticVersion implements Comparable<SemanticVersion> { private final int major; private final int minor; private final int patch; private final String preRelease; private final String buildMetadata; private static final Pattern SEMVER_PATTERN = Pattern.compile( "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" + "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + "(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" ); public SemanticVersion(int major, int minor, int patch) { this(major, minor, patch, null, null); } public SemanticVersion(int major, int minor, int patch, String preRelease, String buildMetadata) { if (major < 0 || minor < 0 || patch < 0) { throw new IllegalArgumentException("Version components cannot be negative"); } validatePreRelease(preRelease); validateBuildMetadata(buildMetadata); this.major = major; this.minor = minor; this.patch = patch; this.preRelease = preRelease; this.buildMetadata = buildMetadata; } public static SemanticVersion of(String version) { if (version == null || version.trim().isEmpty()) { throw new IllegalArgumentException("Version string cannot be null or empty"); } Matcher matcher = SEMVER_PATTERN.matcher(version.trim()); if (!matcher.matches()) { throw new IllegalArgumentException("Invalid semantic version: " + version); } int major = Integer.parseInt(matcher.group(1)); int minor = Integer.parseInt(matcher.group(2)); int patch = Integer.parseInt(matcher.group(3)); String preRelease = matcher.group(4); String buildMetadata = matcher.group(5); return new SemanticVersion(major, minor, patch, preRelease, buildMetadata); } private void validatePreRelease(String preRelease) { if (preRelease == null || preRelease.isEmpty()) { return; } String[] identifiers = preRelease.split("\\."); for (String identifier : identifiers) { if (identifier.isEmpty()) { throw new IllegalArgumentException("Pre-release identifier cannot be empty"); } // Check if identifier is numeric-only if (identifier.matches("\\d+")) { // Numeric identifiers must not contain leading zeros if (identifier.length() > 1 && identifier.startsWith("0")) { throw new IllegalArgumentException("Numeric pre-release identifier cannot have leading zeros: " + identifier); } } else { // Non-numeric identifiers should be alphanumeric with hyphens if (!identifier.matches("[0-9A-Za-z-]+")) { throw new IllegalArgumentException("Invalid pre-release identifier: " + identifier); } } } } private void validateBuildMetadata(String buildMetadata) { if (buildMetadata == null || buildMetadata.isEmpty()) { return; } String[] identifiers = buildMetadata.split("\\."); for (String identifier : identifiers) { if (identifier.isEmpty()) { throw new IllegalArgumentException("Build metadata identifier cannot be empty"); } if (!identifier.matches("[0-9A-Za-z-]+")) { throw new IllegalArgumentException("Invalid build metadata identifier: " + identifier); } } } // Getters public int getMajor() { return major; } public int getMinor() { return minor; } public int getPatch() { return patch; } public String getPreRelease() { return preRelease; } public String getBuildMetadata() { return buildMetadata; } public boolean isPreRelease() { return preRelease != null && !preRelease.isEmpty(); } public boolean isStable() { return !isPreRelease(); } public String getVersion() { StringBuilder sb = new StringBuilder(); sb.append(major).append('.').append(minor).append('.').append(patch); if (preRelease != null && !preRelease.isEmpty()) { sb.append('-').append(preRelease); } if (buildMetadata != null && !buildMetadata.isEmpty()) { sb.append('+').append(buildMetadata); } return sb.toString(); } @Override public String toString() { return getVersion(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SemanticVersion that = (SemanticVersion) o; return major == that.major && minor == that.minor && patch == that.patch && Objects.equals(preRelease, that.preRelease); // Build metadata is ignored in equality check } @Override public int hashCode() { return Objects.hash(major, minor, patch, preRelease); } @Override public int compareTo(SemanticVersion other) { if (other == null) return 1; // Compare major, minor, patch int result = Integer.compare(major, other.major); if (result != 0) return result; result = Integer.compare(minor, other.minor); if (result != 0) return result; result = Integer.compare(patch, other.patch); if (result != 0) return result; // Pre-release versions have lower precedence if (preRelease == null && other.preRelease == null) return 0; if (preRelease == null) return 1; // No pre-release > has pre-release if (other.preRelease == null) return -1; // Has pre-release < no pre-release return comparePreRelease(preRelease, other.preRelease); } private int comparePreRelease(String pre1, String pre2) { String[] identifiers1 = pre1.split("\\."); String[] identifiers2 = pre2.split("\\."); int minLength = Math.min(identifiers1.length, identifiers2.length); for (int i = 0; i < minLength; i++) { String id1 = identifiers1[i]; String id2 = identifiers2[i]; int result = compareIdentifier(id1, id2); if (result != 0) return result; } // If all identifiers are equal, longer pre-release string has higher precedence return Integer.compare(identifiers1.length, identifiers2.length); } private int compareIdentifier(String id1, String id2) { boolean isNumeric1 = id1.matches("\\d+"); boolean isNumeric2 = id2.matches("\\d+"); if (isNumeric1 && isNumeric2) { // Compare numerically return Integer.compare(Integer.parseInt(id1), Integer.parseInt(id2)); } else if (isNumeric1) { // Numeric identifiers have lower precedence than non-numeric return -1; } else if (isNumeric2) { // Non-numeric identifiers have higher precedence than numeric return 1; } else { // Compare lexically in ASCII sort order return id1.compareTo(id2); } } // Version increment methods public SemanticVersion incrementMajor() { return new SemanticVersion(major + 1, 0, 0); } public SemanticVersion incrementMajor(String preRelease) { return new SemanticVersion(major + 1, 0, 0, preRelease, null); } public SemanticVersion incrementMinor() { return new SemanticVersion(major, minor + 1, 0); } public SemanticVersion incrementMinor(String preRelease) { return new SemanticVersion(major, minor + 1, 0, preRelease, null); } public SemanticVersion incrementPatch() { return new SemanticVersion(major, minor, patch + 1); } public SemanticVersion incrementPatch(String preRelease) { return new SemanticVersion(major, minor, patch + 1, preRelease, null); } public SemanticVersion withPreRelease(String preRelease) { return new SemanticVersion(major, minor, patch, preRelease, buildMetadata); } public SemanticVersion withBuildMetadata(String buildMetadata) { return new SemanticVersion(major, minor, patch, preRelease, buildMetadata); } public SemanticVersion withoutPreRelease() { return new SemanticVersion(major, minor, patch, null, buildMetadata); } public SemanticVersion withoutBuildMetadata() { return new SemanticVersion(major, minor, patch, preRelease, null); } // Version range checking public boolean isCompatibleWith(SemanticVersion other) { if (other == null) return false; return this.major == other.major; } public boolean isNewerThan(SemanticVersion other) { return this.compareTo(other) > 0; } public boolean isOlderThan(SemanticVersion other) { return this.compareTo(other) < 0; } }
2. Version Range Support
public class VersionRange { private final SemanticVersion minVersion; private final SemanticVersion maxVersion; private final boolean minInclusive; private final boolean maxInclusive; private static final Pattern RANGE_PATTERN = Pattern.compile( "^([\\[(])\\s*([^,\\s]+)?\\s*,\\s*([^,\\s]+)?\\s*([\\])])$" ); public VersionRange(SemanticVersion minVersion, SemanticVersion maxVersion, boolean minInclusive, boolean maxInclusive) { this.minVersion = minVersion; this.maxVersion = maxVersion; this.minInclusive = minInclusive; this.maxInclusive = maxInclusive; if (minVersion != null && maxVersion != null && minVersion.compareTo(maxVersion) > 0) { throw new IllegalArgumentException("Min version cannot be greater than max version"); } } public static VersionRange exact(SemanticVersion version) { return new VersionRange(version, version, true, true); } public static VersionRange atLeast(SemanticVersion version) { return new VersionRange(version, null, true, false); } public static VersionRange atMost(SemanticVersion version) { return new VersionRange(null, version, false, true); } public static VersionRange between(SemanticVersion min, SemanticVersion max) { return new VersionRange(min, max, true, true); } public static VersionRange of(String range) { if (range == null || range.trim().isEmpty()) { throw new IllegalArgumentException("Range string cannot be null or empty"); } String trimmed = range.trim(); // Handle exact version if (!trimmed.contains(",")) { try { SemanticVersion exact = SemanticVersion.of(trimmed); return exact(exact); } catch (IllegalArgumentException e) { // Not an exact version, try parsing as range } } Matcher matcher = RANGE_PATTERN.matcher(trimmed); if (!matcher.matches()) { throw new IllegalArgumentException("Invalid version range format: " + range); } String leftBracket = matcher.group(1); String minStr = matcher.group(2); String maxStr = matcher.group(3); String rightBracket = matcher.group(4); SemanticVersion min = minStr != null ? SemanticVersion.of(minStr) : null; SemanticVersion max = maxStr != null ? SemanticVersion.of(maxStr) : null; boolean minInclusive = "[".equals(leftBracket); boolean maxInclusive = "]".equals(rightBracket); return new VersionRange(min, max, minInclusive, maxInclusive); } public boolean contains(SemanticVersion version) { if (version == null) return false; boolean minOk = true; boolean maxOk = true; if (minVersion != null) { int cmp = version.compareTo(minVersion); minOk = minInclusive ? cmp >= 0 : cmp > 0; } if (maxVersion != null) { int cmp = version.compareTo(maxVersion); maxOk = maxInclusive ? cmp <= 0 : cmp < 0; } return minOk && maxOk; } public SemanticVersion getMinVersion() { return minVersion; } public SemanticVersion getMaxVersion() { return maxVersion; } public boolean isMinInclusive() { return minInclusive; } public boolean isMaxInclusive() { return maxInclusive; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(minInclusive ? '[' : '('); if (minVersion != null) { sb.append(minVersion); } sb.append(','); if (maxVersion != null) { sb.append(maxVersion); } sb.append(maxInclusive ? ']' : ')'); return sb.toString(); } }
3. Version Management Service
@Service @Slf4j public class VersionManagementService { private final GitService gitService; private final VersionHistoryRepository versionHistoryRepository; public VersionManagementService(GitService gitService, VersionHistoryRepository versionHistoryRepository) { this.gitService = gitService; this.versionHistoryRepository = versionHistoryRepository; } public SemanticVersion determineNextVersion(VersionIncrementStrategy strategy, String currentVersion, List<CommitMessage> commitMessages) { SemanticVersion current = SemanticVersion.of(currentVersion); switch (strategy) { case MAJOR: return current.incrementMajor(); case MINOR: return current.incrementMinor(); case PATCH: return current.incrementPatch(); case AUTO: return autoDetermineNextVersion(current, commitMessages); case PRE_RELEASE: return determinePreReleaseVersion(current, commitMessages); default: throw new IllegalArgumentException("Unknown strategy: " + strategy); } } private SemanticVersion autoDetermineNextVersion(SemanticVersion current, List<CommitMessage> commitMessages) { VersionIncrement maxIncrement = VersionIncrement.PATCH; for (CommitMessage commit : commitMessages) { VersionIncrement increment = analyzeCommitType(commit); if (increment.ordinal() > maxIncrement.ordinal()) { maxIncrement = increment; } } switch (maxIncrement) { case MAJOR: return current.incrementMajor(); case MINOR: return current.incrementMinor(); case PATCH: return current.incrementPatch(); default: return current.incrementPatch(); } } private SemanticVersion determinePreReleaseVersion(SemanticVersion current, List<CommitMessage> commitMessages) { SemanticVersion baseVersion = autoDetermineNextVersion(current, commitMessages); // Get existing pre-release versions for this base List<SemanticVersion> preReleases = versionHistoryRepository .findPreReleasesForVersion(baseVersion.getMajor(), baseVersion.getMinor(), baseVersion.getPatch()); // Determine next pre-release identifier String nextPreRelease = determineNextPreReleaseIdentifier(preReleases); return baseVersion.withPreRelease(nextPreRelease); } private VersionIncrement analyzeCommitType(CommitMessage commit) { String message = commit.getMessage().toLowerCase(); // Conventional Commits pattern matching if (message.startsWith("feat:") || message.startsWith("feat(")) { return VersionIncrement.MINOR; } else if (message.startsWith("fix:") || message.startsWith("fix(")) { return VersionIncrement.PATCH; } else if (message.startsWith("break:") || message.startsWith("break(") || message.contains("breaking change")) { return VersionIncrement.MAJOR; } else if (message.startsWith("perf:") || message.startsWith("perf(")) { return VersionIncrement.PATCH; } else if (message.startsWith("refactor:") || message.startsWith("refactor(")) { return VersionIncrement.PATCH; } else if (message.startsWith("docs:") || message.startsWith("docs(")) { return VersionIncrement.NONE; } else if (message.startsWith("style:") || message.startsWith("style(")) { return VersionIncrement.NONE; } else if (message.startsWith("test:") || message.startsWith("test(")) { return VersionIncrement.NONE; } else if (message.startsWith("chore:") || message.startsWith("chore(")) { return VersionIncrement.NONE; } return VersionIncrement.PATCH; // Default to patch for unknown types } private String determineNextPreReleaseIdentifier(List<SemanticVersion> existingPreReleases) { if (existingPreReleases.isEmpty()) { return "alpha.1"; } // Find the highest numeric identifier for each pre-release type Map<String, Integer> maxIdentifiers = new HashMap<>(); for (SemanticVersion version : existingPreReleases) { String preRelease = version.getPreRelease(); if (preRelease != null) { String[] parts = preRelease.split("\\."); String type = parts[0]; if (parts.length > 1 && parts[1].matches("\\d+")) { int number = Integer.parseInt(parts[1]); maxIdentifiers.merge(type, number, Math::max); } } } // Use alpha as default type String type = "alpha"; int nextNumber = maxIdentifiers.getOrDefault(type, 0) + 1; return type + "." + nextNumber; } public VersionBumpResult bumpVersion(VersionBumpRequest request) { try { log.info("Bumping version for project: {}", request.getProjectName()); // Determine next version SemanticVersion currentVersion = SemanticVersion.of(request.getCurrentVersion()); SemanticVersion nextVersion = determineNextVersion( request.getStrategy(), request.getCurrentVersion(), request.getCommitMessages() ); // Update version files List<VersionFileUpdate> fileUpdates = updateVersionFiles( request.getProjectRoot(), currentVersion, nextVersion ); // Create Git commit String commitMessage = String.format("chore: release %s", nextVersion); String commitHash = gitService.commitChanges(commitMessage); // Create Git tag String tagName = "v" + nextVersion; gitService.createTag(tagName, commitHash); // Record version history VersionHistory history = VersionHistory.builder() .projectName(request.getProjectName()) .version(nextVersion.toString()) .commitHash(commitHash) .bumpStrategy(request.getStrategy()) .timestamp(Instant.now()) .build(); versionHistoryRepository.save(history); return VersionBumpResult.success(nextVersion.toString(), fileUpdates, commitHash, tagName); } catch (Exception e) { log.error("Failed to bump version: {}", e.getMessage(), e); return VersionBumpResult.failure(e.getMessage()); } } private List<VersionFileUpdate> updateVersionFiles(Path projectRoot, SemanticVersion currentVersion, SemanticVersion nextVersion) throws IOException { List<VersionFileUpdate> updates = new ArrayList<>(); // Update pom.xml for Maven projects Path pomPath = projectRoot.resolve("pom.xml"); if (Files.exists(pomPath)) { updateMavenVersion(pomPath, currentVersion, nextVersion); updates.add(new VersionFileUpdate("pom.xml", "Maven project version")); } // Update package.json for Node.js projects Path packageJsonPath = projectRoot.resolve("package.json"); if (Files.exists(packageJsonPath)) { updatePackageJsonVersion(packageJsonPath, currentVersion, nextVersion); updates.add(new VersionFileUpdate("package.json", "Node.js package version")); } // Update build.gradle for Gradle projects Path gradlePath = projectRoot.resolve("build.gradle"); if (Files.exists(gradlePath)) { updateGradleVersion(gradlePath, currentVersion, nextVersion); updates.add(new VersionFileUpdate("build.gradle", "Gradle project version")); } // Update version.properties for custom version files Path versionPropertiesPath = projectRoot.resolve("version.properties"); if (Files.exists(versionPropertiesPath)) { updatePropertiesVersion(versionPropertiesPath, currentVersion, nextVersion); updates.add(new VersionFileUpdate("version.properties", "Application version")); } return updates; } private void updateMavenVersion(Path pomPath, SemanticVersion currentVersion, SemanticVersion nextVersion) throws IOException { String content = Files.readString(pomPath); // Update version in <version> tags String updatedContent = content.replaceAll( "<version>" + Pattern.quote(currentVersion.toString()) + "</version>", "<version>" + nextVersion.toString() + "</version>" ); Files.writeString(pomPath, updatedContent); } private void updatePackageJsonVersion(Path packageJsonPath, SemanticVersion currentVersion, SemanticVersion nextVersion) throws IOException { String content = Files.readString(packageJsonPath); // Update version in JSON String updatedContent = content.replaceAll( "\"version\":\\s*\"" + Pattern.quote(currentVersion.toString()) + "\"", "\"version\": \"" + nextVersion.toString() + "\"" ); Files.writeString(packageJsonPath, updatedContent); } // Similar methods for Gradle and properties files... public enum VersionIncrementStrategy { MAJOR, MINOR, PATCH, AUTO, PRE_RELEASE } public enum VersionIncrement { NONE, PATCH, MINOR, MAJOR } }
4. Data Transfer Objects
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class VersionBumpRequest { private String projectName; private String currentVersion; private VersionManagementService.VersionIncrementStrategy strategy; private Path projectRoot; private List<CommitMessage> commitMessages; private String branch; private boolean createTag; private boolean pushChanges; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class VersionBumpResult { private boolean success; private String message; private String newVersion; private String commitHash; private String tagName; private List<VersionFileUpdate> updatedFiles; private Instant timestamp; public static VersionBumpResult success(String newVersion, List<VersionFileUpdate> updatedFiles, String commitHash, String tagName) { return VersionBumpResult.builder() .success(true) .message("Version bumped successfully") .newVersion(newVersion) .updatedFiles(updatedFiles) .commitHash(commitHash) .tagName(tagName) .timestamp(Instant.now()) .build(); } public static VersionBumpResult failure(String error) { return VersionBumpResult.builder() .success(false) .message("Version bump failed: " + error) .timestamp(Instant.now()) .build(); } } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class VersionFileUpdate { private String filename; private String description; private boolean successful; private String error; public VersionFileUpdate(String filename, String description) { this.filename = filename; this.description = description; this.successful = true; } } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommitMessage { private String hash; private String message; private String author; private Instant timestamp; private List<String> changedFiles; } @Entity @Table(name = "version_history") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class VersionHistory { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String projectName; private String version; private String commitHash; @Enumerated(EnumType.STRING) private VersionManagementService.VersionIncrementStrategy bumpStrategy; private Instant timestamp; private String releasedBy; @Column(length = 1000) private String releaseNotes; }
5. Git Service Integration
@Service @Slf4j public class GitService { public String commitChanges(String message) throws GitAPIException, IOException { try (Git git = Git.open(Paths.get(".").toFile())) { // Add all changes git.add().addFilepattern(".").call(); // Commit RevCommit commit = git.commit() .setMessage(message) .setAuthor("Version Bot", "[email protected]") .call(); log.info("Created commit: {} - {}", commit.getId().getName(), message); return commit.getId().getName(); } } public void createTag(String tagName, String commitHash) throws GitAPIException, IOException { try (Git git = Git.open(Paths.get(".").toFile())) { git.tag() .setName(tagName) .setObjectId(git.getRepository().resolve(commitHash)) .call(); log.info("Created tag: {}", tagName); } } public void pushChanges() throws GitAPIException, IOException { try (Git git = Git.open(Paths.get(".").toFile())) { git.push().call(); log.info("Pushed changes to remote"); } } public void pushTag(String tagName) throws GitAPIException, IOException { try (Git git = Git.open(Paths.get(".").toFile())) { git.push().setPushTags().call(); log.info("Pushed tag to remote: {}", tagName); } } public List<CommitMessage> getCommitsSince(String sinceVersion) throws GitAPIException, IOException { try (Git git = Git.open(Paths.get(".").toFile())) { String sinceTag = "v" + sinceVersion; Iterable<RevCommit> commits = git.log() .add(git.getRepository().resolve("HEAD")) .not(git.getRepository().resolve(sinceTag)) .call(); List<CommitMessage> commitMessages = new ArrayList<>(); for (RevCommit commit : commits) { CommitMessage msg = CommitMessage.builder() .hash(commit.getId().getName()) .message(commit.getFullMessage()) .author(commit.getAuthorIdent().getName()) .timestamp(commit.getAuthorIdent().getWhen().toInstant()) .build(); commitMessages.add(msg); } return commitMessages; } } }

REST API Controllers

1. Version Management API
@RestController @RequestMapping("/api/versions") @Slf4j public class VersionController { private final VersionManagementService versionService; public VersionController(VersionManagementService versionService) { this.versionService = versionService; } @PostMapping("/bump") public ResponseEntity<VersionBumpResult> bumpVersion(@RequestBody VersionBumpRequest request) { log.info("Received version bump request for project: {}", request.getProjectName()); try { VersionBumpResult result = versionService.bumpVersion(request); if (result.isSuccess()) { return ResponseEntity.ok(result); } else { return ResponseEntity.badRequest().body(result); } } catch (Exception e) { log.error("Version bump failed: {}", e.getMessage(), e); return ResponseEntity.internalServerError() .body(VersionBumpResult.failure("Internal server error: " + e.getMessage())); } } @PostMapping("/validate") public ResponseEntity<ValidationResult> validateVersion(@RequestBody VersionValidationRequest request) { try { SemanticVersion version = SemanticVersion.of(request.getVersion()); ValidationResult result = ValidationResult.builder() .valid(true) .version(version.toString()) .message("Valid semantic version") .build(); return ResponseEntity.ok(result); } catch (IllegalArgumentException e) { ValidationResult result = ValidationResult.builder() .valid(false) .version(request.getVersion()) .message("Invalid semantic version: " + e.getMessage()) .build(); return ResponseEntity.badRequest().body(result); } } @GetMapping("/compare") public ResponseEntity<VersionComparison> compareVersions( @RequestParam String version1, @RequestParam String version2) { try { SemanticVersion v1 = SemanticVersion.of(version1); SemanticVersion v2 = SemanticVersion.of(version2); int comparison = v1.compareTo(v2); String relationship; if (comparison < 0) { relationship = version1 + " is older than " + version2; } else if (comparison > 0) { relationship = version1 + " is newer than " + version2; } else { relationship = version1 + " is equal to " + version2; } VersionComparison result = VersionComparison.builder() .version1(version1) .version2(version2) .comparisonResult(comparison) .relationship(relationship) .compatible(v1.isCompatibleWith(v2)) .build(); return ResponseEntity.ok(result); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body( VersionComparison.builder() .error("Invalid version format: " + e.getMessage()) .build() ); } } @GetMapping("/next") public ResponseEntity<NextVersionResponse> suggestNextVersion( @RequestParam String currentVersion, @RequestParam(defaultValue = "AUTO") VersionManagementService.VersionIncrementStrategy strategy, @RequestParam(required = false) String commitMessages) { try { List<CommitMessage> messages = parseCommitMessages(commitMessages); SemanticVersion nextVersion = versionService.determineNextVersion( strategy, currentVersion, messages); NextVersionResponse response = NextVersionResponse.builder() .currentVersion(currentVersion) .suggestedVersion(nextVersion.toString()) .strategy(strategy) .build(); return ResponseEntity.ok(response); } catch (Exception e) { return ResponseEntity.badRequest().body( NextVersionResponse.builder() .error(e.getMessage()) .build() ); } } private List<CommitMessage> parseCommitMessages(String commitMessages) { if (commitMessages == null || commitMessages.trim().isEmpty()) { return List.of(); } // Simple parsing - in reality, you'd parse from Git log or JSON return Arrays.stream(commitMessages.split("\n")) .map(msg -> CommitMessage.builder().message(msg).build()) .collect(Collectors.toList()); } } @Data @Builder @NoArgsConstructor @AllArgsConstructor class VersionValidationRequest { private String version; } @Data @Builder @NoArgsConstructor @AllArgsConstructor class ValidationResult { private boolean valid; private String version; private String message; } @Data @Builder @NoArgsConstructor @AllArgsConstructor class VersionComparison { private String version1; private String version2; private int comparisonResult; private String relationship; private Boolean compatible; private String error; } @Data @Builder @NoArgsConstructor @AllArgsConstructor class NextVersionResponse { private String currentVersion; private String suggestedVersion; private VersionManagementService.VersionIncrementStrategy strategy; private String error; }
2. Version History API
@RestController @RequestMapping("/api/version-history") @Slf4j public class VersionHistoryController { private final VersionHistoryRepository historyRepository; public VersionHistoryController(VersionHistoryRepository historyRepository) { this.historyRepository = historyRepository; } @GetMapping("/{projectName}") public ResponseEntity<List<VersionHistory>> getVersionHistory(@PathVariable String projectName) { List<VersionHistory> history = historyRepository.findByProjectNameOrderByTimestampDesc(projectName); return ResponseEntity.ok(history); } @GetMapping("/{projectName}/latest") public ResponseEntity<VersionHistory> getLatestVersion(@PathVariable String projectName) { return historyRepository.findTopByProjectNameOrderByTimestampDesc(projectName) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping("/{projectName}") public ResponseEntity<VersionHistory> recordVersionRelease( @PathVariable String projectName, @RequestBody ReleaseVersionRequest request) { VersionHistory history = VersionHistory.builder() .projectName(projectName) .version(request.getVersion()) .commitHash(request.getCommitHash()) .bumpStrategy(request.getStrategy()) .releasedBy(request.getReleasedBy()) .releaseNotes(request.getReleaseNotes()) .timestamp(Instant.now()) .build(); VersionHistory saved = historyRepository.save(history); return ResponseEntity.status(HttpStatus.CREATED).body(saved); } } @Data @NoArgsConstructor @AllArgsConstructor class ReleaseVersionRequest { private String version; private String commitHash; private VersionManagementService.VersionIncrementStrategy strategy; private String releasedBy; private String releaseNotes; }

Maven Plugin Integration

1. Semantic Version Maven Plugin
@Mojo(name = "bump-version", defaultPhase = LifecyclePhase.VALIDATE) public class VersionBumpMojo extends AbstractMojo { @Parameter(property = "bumpStrategy", defaultValue = "AUTO") private String bumpStrategy; @Parameter(property = "createTag", defaultValue = "true") private boolean createTag; @Parameter(property = "pushChanges", defaultValue = "false") private boolean pushChanges; @Parameter(property = "project", required = true, readonly = true) private MavenProject project; @Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().info("Bumping version using strategy: " + bumpStrategy); try { VersionManagementService versionService = createVersionService(); VersionBumpRequest request = VersionBumpRequest.builder() .projectName(project.getArtifactId()) .currentVersion(project.getVersion()) .strategy(VersionManagementService.VersionIncrementStrategy.valueOf(bumpStrategy.toUpperCase())) .projectRoot(project.getBasedir().toPath()) .createTag(createTag) .pushChanges(pushChanges) .build(); VersionBumpResult result = versionService.bumpVersion(request); if (result.isSuccess()) { getLog().info("Successfully bumped version to: " + result.getNewVersion()); // Update Maven project version project.setVersion(result.getNewVersion()); } else { throw new MojoFailureException("Version bump failed: " + result.getMessage()); } } catch (Exception e) { throw new MojoExecutionException("Failed to bump version", e); } } private VersionManagementService createVersionService() { // Create service instance with dependencies return new VersionManagementService(new GitService(), new InMemoryVersionHistoryRepository()); } }
2. Plugin Configuration in pom.xml
<build> <plugins> <plugin> <groupId>com.example</groupId> <artifactId>semantic-version-maven-plugin</artifactId> <version>1.0.0</version> <configuration> <bumpStrategy>AUTO</bumpStrategy> <createTag>true</createTag> <pushChanges>false</pushChanges> </configuration> </plugin> </plugins> </build>

Testing

1. Unit Tests
class SemanticVersionTest { @Test void shouldParseValidSemanticVersion() { // Given String versionString = "1.2.3"; // When SemanticVersion version = SemanticVersion.of(versionString); // Then assertThat(version.getMajor()).isEqualTo(1); assertThat(version.getMinor()).isEqualTo(2); assertThat(version.getPatch()).isEqualTo(3); assertThat(version.isPreRelease()).isFalse(); assertThat(version.toString()).isEqualTo("1.2.3"); } @Test void shouldParseVersionWithPreRelease() { // Given String versionString = "1.2.3-alpha.1"; // When SemanticVersion version = SemanticVersion.of(versionString); // Then assertThat(version.getMajor()).isEqualTo(1); assertThat(version.getMinor()).isEqualTo(2); assertThat(version.getPatch()).isEqualTo(3); assertThat(version.getPreRelease()).isEqualTo("alpha.1"); assertThat(version.isPreRelease()).isTrue(); } @Test void shouldCompareVersionsCorrectly() { // Given SemanticVersion v1 = SemanticVersion.of("1.2.3"); SemanticVersion v2 = SemanticVersion.of("1.2.4"); SemanticVersion v3 = SemanticVersion.of("1.2.3-alpha"); // Then assertThat(v1).isLessThan(v2); assertThat(v3).isLessThan(v1); assertThat(v1).isEqualTo(SemanticVersion.of("1.2.3")); } @Test void shouldIncrementVersions() { // Given SemanticVersion version = SemanticVersion.of("1.2.3"); // When SemanticVersion major = version.incrementMajor(); SemanticVersion minor = version.incrementMinor(); SemanticVersion patch = version.incrementPatch(); // Then assertThat(major).isEqualTo(SemanticVersion.of("2.0.0")); assertThat(minor).isEqualTo(SemanticVersion.of("1.3.0")); assertThat(patch).isEqualTo(SemanticVersion.of("1.2.4")); } @Test void shouldRejectInvalidVersions() { assertThatThrownBy(() -> SemanticVersion.of("1.2")) .isInstanceOf(IllegalArgumentException.class); assertThatThrownBy(() -> SemanticVersion.of("1.2.3.4")) .isInstanceOf(IllegalArgumentException.class); assertThatThrownBy(() -> SemanticVersion.of("1.2.3-01")) .isInstanceOf(IllegalArgumentException.class); } } @SpringBootTest class VersionManagementServiceTest { @Autowired private VersionManagementService versionService; @Test void shouldDetermineNextVersionBasedOnCommits() { // Given List<CommitMessage> commits = Arrays.asList( CommitMessage.builder().message("feat: add new API endpoint").build(), CommitMessage.builder().message("fix: resolve null pointer exception").build() ); // When SemanticVersion nextVersion = versionService.determineNextVersion( VersionManagementService.VersionIncrementStrategy.AUTO, "1.2.3", commits ); // Then - should bump minor because of feature commit assertThat(nextVersion).isEqualTo(SemanticVersion.of("1.3.0")); } }

Best Practices

1. Configuration
# application.yml version: management: auto-bump: true create-tags: true push-changes: false default-strategy: AUTO pre-release-types: - alpha - beta - rc commit-types: major: - "break:" - "breaking change" minor: - "feat:" patch: - "fix:" - "perf:" - "refactor:" git: author: name: "Version Bot" email: "[email protected]"
2. Integration with CI/CD
@Component @Slf4j public class CiCdVersionIntegration { private final VersionManagementService versionService; public CiCdVersionIntegration(VersionManagementService versionService) { this.versionService = versionService; } @EventListener public void onBuildSuccess(BuildSuccessEvent event) { if (shouldBumpVersion(event)) { try { VersionBumpRequest request = createBumpRequest(event); VersionBumpResult result = versionService.bumpVersion(request); if (result.isSuccess()) { log.info("Automatically bumped version to: {}", result.getNewVersion()); // Trigger deployment or next pipeline stage } } catch (Exception e) { log.error("Automatic version bump failed: {}", e.getMessage()); } } } private boolean shouldBumpVersion(BuildSuccessEvent event) { return event.getBranch().equals("main") || event.getBranch().equals("master") || event.getBranch().startsWith("release/"); } private VersionBumpRequest createBumpRequest(BuildSuccessEvent event) { return VersionBumpRequest.builder() .projectName(event.getProjectName()) .currentVersion(event.getCurrentVersion()) .strategy(VersionManagementService.VersionIncrementStrategy.AUTO) .commitMessages(event.getCommits()) .createTag(true) .pushChanges(true) .build(); } }

Conclusion

Implementing Semantic Versioning in Java provides:

  • Standardized versioning across all projects
  • Automated version bumps based on commit messages
  • Integration with build tools (Maven, Gradle)
  • Git integration with automatic tagging
  • REST APIs for version management
  • Comprehensive validation and comparison

Key benefits include:

  1. Clear communication of change impact through version numbers
  2. Automated release process reducing human error
  3. Integration with CI/CD pipelines
  4. Historical tracking of version changes
  5. Support for pre-releases and build metadata

This implementation can be extended with additional features like:

  • Integration with artifact repositories (Nexus, Artifactory)
  • Support for multi-module projects
  • Advanced version range resolution for dependencies
  • Integration with issue tracking systems (Jira, GitHub Issues)
  • Automated changelog generation

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper