Class: Makit::Version
- Inherits:
-
Object
- Object
- Makit::Version
- Defined in:
- lib/makit/version.rb,
lib/makit/version_util.rb
Overview
Version utility class for handling version comparisons
Class Method Summary collapse
-
.bump ⇒ String
Bump the patch version in the SSOT version file.
-
.detect_from_file(filename, regex) ⇒ String?
Detect version using a regex pattern in a specific file.
-
.extract_semver_from_version(version_string) ⇒ String
Extract SemVer (3-part) from version string (handles 4-part).
-
.extract_version_from_ssot_file(file_path) ⇒ Object
Extract version from SSOT file based on file type.
-
.find_project_root(start_dir) ⇒ Object
Find project root by looking for common markers.
-
.find_ssot_version_file(project_root) ⇒ String?
Find the SSOT version file in the project root.
-
.get_highest_version(versions) ⇒ String?
Get the highest version from a list of version strings.
-
.get_version_from_file(path) ⇒ String
Extract version number from a file based on its extension.
-
.info ⇒ nil
Display version information for the current project.
-
.normalize_version_for_parsing(version_string) ⇒ String
Normalize version string for parsing (strip metadata, handle 3/4-part).
-
.parse(version_string) ⇒ Hash
Parse a semantic version string into its components.
-
.semver_to_four_part(version_string) ⇒ String
Convert SemVer to 4-part format.
-
.set_version_in_directory_build_props(filename, new_version) ⇒ nil
Update Directory.Build.props file with new version.
-
.set_version_in_file(filename, version) ⇒ nil
Update version number in a file based on its extension.
-
.set_version_in_files(glob_pattern, version) ⇒ nil
Update version number in multiple files matching a glob pattern.
-
.version ⇒ String
Attempt to detect the version from the SSOT (Single Source of Truth) version file Uses the same logic as Makit::Version.info to find version files in priority order: *.gemspec, Directory.Build.props, Cargo.toml, package.json, pyproject.toml, pom.xml Falls back to makit.gemspec only if we’re in the makit gem directory.
Class Method Details
.bump ⇒ String
Bump the patch version in the SSOT version file
Finds the Single Source of Truth (SSOT) version file in the project root, reads the current version, increments the patch version, and updates the file.
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/makit/version.rb', line 375 def self.bump # Find the SSOT version file project_root = begin require_relative "directories" unless defined?(Makit::Directories) Makit::Directories::PROJECT_ROOT rescue NameError, LoadError find_project_root(Dir.pwd) end raise "Project root not found" if project_root.nil? || !Dir.exist?(project_root) version_file = find_ssot_version_file(project_root) if version_file.nil? warn " Warning: No version file found in project root: #{project_root}" warn " You may define a constant VERSION_FILE to manually set the version file path" raise "Cannot bump version: no version file found" end # Read current version current_version = extract_version_from_ssot_file(version_file) raise "Version not found in #{version_file}" if current_version.nil? # Parse and bump patch version parsed = parse(current_version) new_version = "#{parsed[:major]}.#{parsed[:minor]}.#{parsed[:patch] + 1}" # Update the version file set_version_in_file(version_file, new_version) # Verify the update updated_version = extract_version_from_ssot_file(version_file) if updated_version != new_version raise "Version bump failed: expected #{new_version}, got #{updated_version}" end new_version end |
.detect_from_file(filename, regex) ⇒ String?
Detect version using a regex pattern in a specific file
225 226 227 228 229 230 |
# File 'lib/makit/version.rb', line 225 def self.detect_from_file(filename, regex) raise "unable to find version in #{filename}" unless File.exist?(filename) match = File.read(filename).match(regex) match.captures[0] if !match.nil? && match.captures.length.positive? end |
.extract_semver_from_version(version_string) ⇒ String
Extract SemVer (3-part) from version string (handles 4-part)
If version is 4-part (x.y.z.w), extracts first 3 parts (x.y.z). If 3-part, returns as-is. Strips pre-release and build metadata.
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 |
# File 'lib/makit/version.rb', line 504 def self.extract_semver_from_version(version_string) normalized = normalize_version_for_parsing(version_string) return nil if normalized.nil? # Strip pre-release suffix (e.g., "-alpha", "-beta.1") normalized = normalized.split("-").first if normalized.include?("-") # Split by dots to check if 4-part parts = normalized.split(".") # If 4-part, extract first 3 parts if parts.length >= 4 "#{parts[0]}.#{parts[1]}.#{parts[2]}" elsif parts.length == 3 normalized else # Invalid format, return as-is (will be caught by parse method) normalized end end |
.extract_version_from_ssot_file(file_path) ⇒ Object
Extract version from SSOT file based on file type
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 |
# File 'lib/makit/version.rb', line 615 def self.extract_version_from_ssot_file(file_path) case File.basename(file_path) when /\.gemspec$/ # Extract from gemspec: spec.version = "x.y.z" content = File.read(file_path) match = content.match(/spec\.version\s*=\s*["']([^"']+)["']/) match ? match[1] : nil when "Directory.Build.props" # Extract from Directory.Build.props: <Version>x.y.z</Version> content = File.read(file_path) match = content.match(%r{<Version>([^<]+)</Version>}) if match # Trim whitespace and normalize (strip build metadata) normalize_version_for_parsing(match[1]) else nil end when "Cargo.toml" # Extract from Cargo.toml: version = "x.y.z" content = File.read(file_path) match = content.match(/version\s*=\s*["']([^"']+)["']/) match ? match[1] : nil when "package.json" # Extract from package.json: "version": "x.y.z" require "json" json = JSON.parse(File.read(file_path)) json["version"] when "pyproject.toml" # Extract from pyproject.toml: version = "x.y.z" (in [project] or [tool.poetry] section) content = File.read(file_path) # Try [project] section first match = content.match(/\[project\]\s*version\s*=\s*["']([^"']+)["']/) return match[1] if match # Try [tool.poetry] section match = content.match(/\[tool\.poetry\]\s*version\s*=\s*["']([^"']+)["']/) match ? match[1] : nil when "pom.xml" # Extract from pom.xml: <version>x.y.z</version> content = File.read(file_path) match = content.match(%r{<version>([^<]+)</version>}) match ? match[1] : nil else nil end end |
.find_project_root(start_dir) ⇒ Object
Find project root by looking for common markers
455 456 457 458 459 460 461 462 463 464 465 466 |
# File 'lib/makit/version.rb', line 455 def self.find_project_root(start_dir) current = File.(start_dir) root = File.("/") while current != root markers = ["Rakefile", "rakefile.rb", ".gitignore", ".git"] return current if markers.any? { |marker| File.exist?(File.join(current, marker)) } current = File.dirname(current) end nil end |
.find_ssot_version_file(project_root) ⇒ String?
Find the SSOT version file in the project root
Checks for VERSION_FILE constant first (if defined), then searches for common version files.
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 |
# File 'lib/makit/version.rb', line 421 def self.find_ssot_version_file(project_root) # Normalize project_root for file operations (Ruby's File methods work with forward slashes) normalized_root = project_root.gsub(/\\/, "/") # Check for manually defined VERSION_FILE constant first if defined?(VERSION_FILE) && !VERSION_FILE.nil? version_file = File.(VERSION_FILE, normalized_root) return version_file if File.exist?(version_file) warn " Warning: VERSION_FILE constant points to non-existent file: #{VERSION_FILE}" end # Priority order for version files (SSOT) version_file_patterns = [ "*.gemspec", # Ruby gems "Directory.Build.props", # .NET projects "Cargo.toml", # Rust projects "package.json", # Node.js projects "pyproject.toml", # Python projects "pom.xml" # Maven/Java projects ] version_file_patterns.each do |pattern| matches = Dir.glob(File.join(normalized_root, pattern)) next if matches.empty? # For gemspec, prefer the one matching the project name or take the first version_file = matches.first return version_file if version_file end nil end |
.get_highest_version(versions) ⇒ String?
Get the highest version from a list of version strings
187 188 189 |
# File 'lib/makit/version.rb', line 187 def self.get_highest_version(versions) versions.max { |a, b| Gem::Version.new(a) <=> Gem::Version.new(b) } end |
.get_version_from_file(path) ⇒ String
Extract version number from a file based on its extension
Supports multiple file formats:
-
.csproj files: ‘<Version>x.y.z</Version>`
-
.wxs files: ‘Version=“x.y.z”`
-
.yml files: ‘VERSION: “x.y.z”`
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/makit/version.rb', line 201 def self.get_version_from_file(path) raise "file #{path}does not exist" unless File.exist?(path) extension = File.extname(path) case extension when ".csproj" Makit::Version.detect_from_file(path, /<Version>([-\w\d.]+)</) when ".wxs" Makit::Version.detect_from_file(path, / Version="([\d.]+)"/) when ".yml" Makit::Version.detect_from_file(path, /VERSION:\s*["']?([\d.]+)["']?/) when ".rb" Makit::Version.detect_from_file(path, /VERSION = "([\d.]+)"/) else raise "unrecognized file type" end end |
.info ⇒ nil
Display version information for the current project
Finds the Single Source of Truth (SSOT) version file in the project root and displays the file path and current version value.
Searches for common version files in priority order:
-
*.gemspec (Ruby gems)
-
Directory.Build.props (.NET projects)
-
Cargo.toml (Rust projects)
-
package.json (Node.js projects)
-
pyproject.toml (Python projects)
-
pom.xml (Maven/Java projects)
If no version file is found, issues a warning and suggests defining a VERSION_FILE constant to manually specify the version file path.
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/makit/version.rb', line 322 def self.info # Access Directories lazily to avoid circular dependency project_root = begin require_relative "directories" unless defined?(Makit::Directories) Makit::Directories::PROJECT_ROOT rescue NameError, LoadError # Fallback: try to find project root from current directory find_project_root(Dir.pwd) end raise "Project root not found" if project_root.nil? || !Dir.exist?(project_root) version_file = find_ssot_version_file(project_root) if version_file.nil? warn " Warning: No version file found in project root: #{project_root}" warn " You may define a constant VERSION_FILE to manually set the version file path" return end # Extract version based on file type version = extract_version_from_ssot_file(version_file) raise "Version not found in #{version_file}" if version.nil? # Display information with relative path (Windows-safe path handling) # Normalize both paths to forward slashes for comparison, then convert back if needed normalized_version_file = version_file.gsub(/\\/, "/") normalized_project_root = project_root.gsub(/\\/, "/") relative_path = normalized_version_file.sub(normalized_project_root + "/", "") puts " Version File: #{relative_path}" puts " Version: #{version}" end |
.normalize_version_for_parsing(version_string) ⇒ String
Normalize version string for parsing (strip metadata, handle 3/4-part)
Strips build metadata (+abc123), handles 3-part and 4-part versions, and trims whitespace.
480 481 482 483 484 485 486 487 488 489 490 |
# File 'lib/makit/version.rb', line 480 def self.normalize_version_for_parsing(version_string) return nil if version_string.nil? # Trim whitespace normalized = version_string.strip # Strip build metadata (e.g., "+abc123") normalized = normalized.split("+").first if normalized.include?("+") normalized end |
.parse(version_string) ⇒ Hash
Parse a semantic version string into its components
Parses a version string following Semantic Versioning (SemVer) format: MAJOR.MINOR.PATCH
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/makit/version.rb', line 146 def self.parse(version_string) parts = version_string.split(".") if parts.length < 3 raise "Invalid version format: #{version_string}. Expected format: Major.Minor.Patch" end major = parts[0].to_i minor = parts[1].to_i patch = parts[2].to_i # Handle pre-release suffixes (e.g., "0.1.0-preview" -> patch = 0, suffix = "-preview") # Note: If suffix contains dots (e.g., "1.2.3-preview.1"), the split by "." will separate # the patch and suffix parts, so only the part before the first "-" in parts[2] is used as patch. patch_part = parts[2] if patch_part.include?("-") patch, suffix = patch_part.split("-", 2) patch = patch.to_i suffix = "-#{suffix}" else suffix = "" end { major: major, minor: minor, patch: patch, suffix: suffix } end |
.semver_to_four_part(version_string) ⇒ String
Convert SemVer to 4-part format
Converts “x.y.z” to “x.y.z.0”. Handles pre-release suffixes by stripping them.
535 536 537 538 539 540 541 542 543 544 545 546 547 548 |
# File 'lib/makit/version.rb', line 535 def self.semver_to_four_part(version_string) # Extract SemVer first (handles pre-release suffixes) semver = extract_semver_from_version(version_string) return nil if semver.nil? # Ensure it's 3-part, then append .0 parts = semver.split(".") if parts.length == 3 "#{parts[0]}.#{parts[1]}.#{parts[2]}.0" else # If already 4-part or invalid, return as-is semver end end |
.set_version_in_directory_build_props(filename, new_version) ⇒ nil
Update Directory.Build.props file with new version
Reads file, updates <Version>, <AssemblyVersion>, and <FileVersion> elements in all PropertyGroups, preserves XML structure, and performs atomic operation.
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 |
# File 'lib/makit/version.rb', line 559 def self.set_version_in_directory_build_props(filename, new_version) # Read file content content = File.read(filename) # Validate that <Version> element exists unless content.match(%r{<Version>([^<]+)</Version>}) raise "Directory.Build.props file does not contain <Version> element: #{filename}" end # Extract SemVer from new_version (in case it's 4-part or has metadata) semver_version = extract_semver_from_version(new_version) raise "Invalid version format: #{new_version}" if semver_version.nil? # Convert to 4-part format for AssemblyVersion and FileVersion four_part_version = semver_to_four_part(semver_version) raise "Failed to convert version to 4-part format: #{semver_version}" if four_part_version.nil? # Prepare updated content (start with original) updated_content = content.dup # Update <Version> element (preserve XML structure) # Pattern: captures opening tag, whitespace, content, whitespace, closing tag version_pattern = %r{(<Version>)\s*([^<]+?)\s*(</Version>)} if updated_content.match(version_pattern) updated_content = updated_content.gsub(version_pattern) do |match| "#{$1}#{semver_version}#{$3}" end else raise "Failed to update <Version> element in #{filename}" end # Update <AssemblyVersion> element if present (preserve XML structure) assembly_version_pattern = %r{(<AssemblyVersion>)\s*([^<]+?)\s*(</AssemblyVersion>)} if updated_content.match(assembly_version_pattern) updated_content = updated_content.gsub(assembly_version_pattern) do |match| "#{$1}#{four_part_version}#{$3}" end end # Note: If AssemblyVersion doesn't exist, we don't add it (per FR-006) # Update <FileVersion> element if present (preserve XML structure) file_version_pattern = %r{(<FileVersion>)\s*([^<]+?)\s*(</FileVersion>)} if updated_content.match(file_version_pattern) updated_content = updated_content.gsub(file_version_pattern) do |match| "#{$1}#{four_part_version}#{$3}" end end # Note: If FileVersion doesn't exist, we don't add it (per FR-006) # Atomic write: only write if content changed if updated_content != content File.write(filename, updated_content) end end |
.set_version_in_file(filename, version) ⇒ nil
Update version number in a file based on its extension
Supports updating versions in multiple file formats:
-
.yml files
-
.gemspec files
-
.csproj files
-
.nuspec files
-
.wxs files
-
.toml files
-
Directory.Build.props files (.NET projects)
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/makit/version.rb', line 246 def self.set_version_in_file(filename, version) # Handle Directory.Build.props files with special logic if filename.include?("Directory.Build.props") set_version_in_directory_build_props(filename, version) return end text = File.read(filename) # VERSION = "0.0.138rake" (.rb file) new_text = text new_text = new_text.gsub(/VERSION:\s?['|"]([.\d]+)['|"]/, "VERSION: \"#{version}\"") if filename.include?(".yml") new_text = new_text.gsub(/spec\.version\s*=\s*['"]([^'"]+)['"]/, "spec.version = '#{version}'") if filename.include?(".gemspec") # Handle Directory.Build.props and .csproj files (both use <Version> tag) if filename.include?("Directory.Build.props") || filename.include?(".csproj") new_text = new_text.gsub(/<Version>([-\w\d.]+)</, "<Version>#{version}<") end new_text = new_text.gsub(/<version>([-\w\d.]+)</, "<version>#{version}<") if filename.include?(".nuspec") new_text = new_text.gsub(/ Version="([\d.]+)"/, " Version=\"#{version}\"") if filename.include?(".wxs") new_text = new_text.gsub(/VERSION = "([\d.]+)"/, "VERSION = \"#{version}\"") if filename.include?(".rb") # Handle Cargo.toml, pyproject.toml, and other .toml files if filename.include?(".toml") new_text = new_text.gsub(/version\s+=\s+['"]([\w.]+)['"]/, "version=\"#{version}\"") end # Handle package.json if filename.include?("package.json") require "json" json = JSON.parse(new_text) json["version"] = version new_text = JSON.pretty_generate(json) end # Handle pom.xml if filename.include?("pom.xml") new_text = new_text.gsub(%r{<version>([^<]+)</version>}, "<version>#{version}</version>") end File.write(filename, new_text) if new_text != text end |
.set_version_in_files(glob_pattern, version) ⇒ nil
Update version number in multiple files matching a glob pattern
288 289 290 291 292 |
# File 'lib/makit/version.rb', line 288 def self.set_version_in_files(glob_pattern, version) Dir.glob(glob_pattern).each do |filename| set_version_in_file(filename, version) end end |
.version ⇒ String
Attempt to detect the version from the SSOT (Single Source of Truth) version file Uses the same logic as Makit::Version.info to find version files in priority order: *.gemspec, Directory.Build.props, Cargo.toml, package.json, pyproject.toml, pom.xml Falls back to makit.gemspec only if we’re in the makit gem directory
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/makit/version.rb', line 71 def self.version # Try to find version file in project root using SSOT logic (same as Makit::Version.info) project_root = begin require_relative "directories" unless defined?(Makit::Directories) Makit::Directories::PROJECT_ROOT rescue NameError, LoadError find_project_root(Dir.pwd) end # Use SSOT version file detection (supports multiple file types) if project_root && Dir.exist?(project_root) version_file = find_ssot_version_file(project_root) if version_file && File.exist?(version_file) version = extract_version_from_ssot_file(version_file) return version if version end end # Fallback to makit.gemspec (for the makit gem itself) # Check if makit.gemspec exists relative to this file makit_gemspec = File.join(File.dirname(__FILE__), "..", "..", "makit.gemspec") makit_gemspec = File.(makit_gemspec) # Use makit.gemspec if: # 1. It exists, AND # 2. Either we're in the makit gem directory (project_root matches), OR # project_root is nil (running outside a project context, likely from installed gem) if File.exist?(makit_gemspec) makit_gem_dir = File.dirname(makit_gemspec) use_makit_gemspec = if project_root # If we have a project root, only use makit.gemspec if we're in the makit gem directory File.(project_root) == File.(makit_gem_dir) else # If no project root, check if current directory is makit gem or if makit.gemspec is nearby # This handles the case when running from installed gem or outside project context Dir.pwd == makit_gem_dir || File.(Dir.pwd).start_with?(File.(makit_gem_dir)) end if use_makit_gemspec gemspec_content = File.read(makit_gemspec) match = gemspec_content.match(/spec\.version\s*=\s*["']([^"']+)["']/) raise "Version not found in gemspec file" if match.nil? return match[1] end end # If no version file found, return default version (for non-gem projects) "0.0.0" end |