Class: Makit::Version

Inherits:
Object
  • Object
show all
Defined in:
lib/makit/version.rb,
lib/makit/version_util.rb

Overview

Version utility class for handling version comparisons

Class Method Summary collapse

Class Method Details

.bumpString

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.

Examples:

# Current version: 1.2.3
Makit::Version.bump
# => "1.2.4"

With pre-release suffix

# Current version: 1.2.3-alpha
Makit::Version.bump
# => "1.2.4" (removes pre-release suffix when bumping)

Returns:

  • (String)

    The new version string after bumping

Raises:

  • (RuntimeError)

    If project root cannot be determined, no version file is found, or version cannot be parsed/updated



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/makit/version.rb', line 349

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

Parameters:

  • filename (String)

    Path to the file to search

  • regex (Regexp)

    Regular expression pattern to match version

Returns:

  • (String, nil)

    The extracted version or nil if no match found

Raises:

  • (RuntimeError)

    If file doesn’t exist



226
227
228
229
230
231
# File 'lib/makit/version.rb', line 226

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_version_from_ssot_file(file_path) ⇒ Object

Extract version from SSOT file based on file type



440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'lib/makit/version.rb', line 440

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>})
    match ? match[1] : nil
  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



426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/makit/version.rb', line 426

def self.find_project_root(start_dir)
  current = File.expand_path(start_dir)
  root = File.expand_path("/")

  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.

Parameters:

  • project_root (String)

    Path to the project root directory

Returns:

  • (String, nil)

    Path to the version file, or nil if not found



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/makit/version.rb', line 395

def self.find_ssot_version_file(project_root)
  # Check for manually defined VERSION_FILE constant first

  if defined?(VERSION_FILE) && !VERSION_FILE.nil?
    version_file = File.expand_path(VERSION_FILE, project_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(project_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

Parameters:

  • versions (Array<String>)

    array of version strings

Returns:

  • (String, nil)

    highest version string or nil if empty



188
189
190
# File 'lib/makit/version.rb', line 188

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”`

Parameters:

  • path (String)

    Path to the file containing version information

Returns:

  • (String)

    The extracted version string

Raises:

  • (RuntimeError)

    If file doesn’t exist or has unrecognized extension



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/makit/version.rb', line 202

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

.infonil

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.

Examples:

Output format

Version File: makit.gemspec
Version: 0.0.147

With VERSION_FILE constant defined

VERSION_FILE = "custom/version.txt"
Makit::Version.info
# Uses the file specified by VERSION_FILE constant

Returns:

  • (nil)

    Outputs version information to stdout, or returns early with warning if no file found

Raises:

  • (RuntimeError)

    If project root cannot be determined or version cannot be extracted from file



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/makit/version.rb', line 299

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

  relative_path = version_file.sub(project_root + "/", "")
  puts "  Version File: #{relative_path}"
  puts "  Version: #{version}"
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

Examples:

Basic version

Makit::Version.parse("1.2.3")
# => { major: 1, minor: 2, patch: 3, suffix: "" }

Version with pre-release suffix

Makit::Version.parse("1.2.3-alpha")
# => { major: 1, minor: 2, patch: 3, suffix: "-alpha" }

Invalid format

Makit::Version.parse("1.2")
# => RuntimeError: Invalid version format: 1.2. Expected format: Major.Minor.Patch

Parameters:

  • version_string (String)

    Version string in SemVer format (e.g., “1.2.3” or “1.2.3-alpha.1”)

Returns:

  • (Hash)

    Hash with keys: :major, :minor, :patch, :suffix

    • :major [Integer] Major version number

    • :minor [Integer] Minor version number

    • :patch [Integer] Patch version number

    • :suffix [String] Pre-release suffix (e.g., “-alpha.1”) or empty string

Raises:

  • (RuntimeError)

    If version string doesn’t match MAJOR.MINOR.PATCH format



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/makit/version.rb', line 147

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

.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

Parameters:

  • filename (String)

    Path to the file to update

  • version (String)

    New version string to set

Returns:

  • (nil)


246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/makit/version.rb', line 246

def self.set_version_in_file(filename, version)
  text = File.read(filename)
  #   VERSION = "0.0.138rake" (.rb file)

  new_text = text
  new_text = text.gsub(/VERSION:\s?['|"]([.\d]+)['|"]/, "VERSION: \"#{version}\"") if filename.include?(".yml")
  new_text = text.gsub(/version\s?=\s?['|"]([.\d]+)['|"]/, "version='#{version}'") if filename.include?(".gemspec")
  new_text = text.gsub(/<Version>([-\w\d.]+)</, "<Version>#{version}<") if filename.include?(".csproj")
  new_text = text.gsub(/<version>([-\w\d.]+)</, "<version>#{version}<") if filename.include?(".nuspec")
  new_text = text.gsub(/ Version="([\d.]+)"/, " Version=\"#{version}\"") if filename.include?(".wxs")
  new_text = text.gsub(/VERSION = "([\d.]+)"/, "VERSION = \"#{version}\"") if filename.include?(".rb")
  new_text = text.gsub(/version\s+=\s+['"]([\w.]+)['"]/, "version=\"#{version}\"") if filename.include?(".toml")
  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

Parameters:

  • glob_pattern (String)

    Glob pattern to match files (e.g., ‘*/.csproj’)

  • version (String)

    New version string to set in all matching files

Returns:

  • (nil)


265
266
267
268
269
# File 'lib/makit/version.rb', line 265

def self.set_version_in_files(glob_pattern, version)
  Dir.glob(glob_pattern).each do |filename|
    set_version_in_file(filename, version)
  end
end

.versionString

Attempt to detect the version from the gemspec file First tries to find a gemspec in the current project root Falls back to makit.gemspec only if we’re in the makit gem directory

Returns:

  • (String)

    The version string, or “0.0.0” if no gemspec is found

Raises:

  • (RuntimeError)

    If a gemspec is found but version cannot be detected



70
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
120
# File 'lib/makit/version.rb', line 70

def self.version
  # Try to find gemspec in project root first (for projects using makit)

  project_root = begin
    require_relative "directories" unless defined?(Makit::Directories)
    Makit::Directories::PROJECT_ROOT
  rescue NameError, LoadError
    find_project_root(Dir.pwd)
  end

  # Look for any .gemspec file in project root

  if project_root && Dir.exist?(project_root)
    gemspec_files = Dir.glob(File.join(project_root, "*.gemspec"))
    if gemspec_files.any?
      gemspec = gemspec_files.first
      gemspec_content = File.read(gemspec)
      match = gemspec_content.match(/spec\.version\s*=\s*["']([^"']+)["']/)
      return match[1] if match
    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.expand_path(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.expand_path(project_root) == File.expand_path(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.expand_path(Dir.pwd).start_with?(File.expand_path(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 gemspec found, return default version (for non-gem projects)

  "0.0.0"
end