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



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

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



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.

Examples:

extract_semver_from_version("0.1.1.0")      # => "0.1.1"
extract_semver_from_version("0.1.1")        # => "0.1.1"
extract_semver_from_version("0.1.1-alpha") # => "0.1.1"

Parameters:

  • version_string (String)

    Version string (3-part or 4-part)

Returns:

  • (String)

    3-part SemVer version string



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.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



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.expand_path(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

Parameters:

  • versions (Array<String>)

    array of version strings

Returns:

  • (String, nil)

    highest version string or nil if empty



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

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



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

.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



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.

Examples:

normalize_version_for_parsing("0.1.1+abc123") # => "0.1.1"
normalize_version_for_parsing("0.1.1.0")      # => "0.1.1.0"
normalize_version_for_parsing(" 0.1.1 ")      # => "0.1.1"

Parameters:

  • version_string (String)

    Version string to normalize

Returns:

  • (String)

    Normalized version string



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

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



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.

Examples:

semver_to_four_part("0.1.2")        # => "0.1.2.0"
semver_to_four_part("0.1.2-alpha")  # => "0.1.2.0"

Parameters:

  • version_string (String)

    SemVer version string (x.y.z)

Returns:

  • (String)

    4-part version string (x.y.z.0)



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.

Parameters:

  • filename (String)

    Path to Directory.Build.props file

  • new_version (String)

    New SemVer version string (x.y.z)

Returns:

  • (nil)

Raises:

  • (RuntimeError)

    If <Version> element is missing or update fails



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)

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

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)


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

.versionString

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

Returns:

  • (String)

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

Raises:

  • (RuntimeError)

    If a version file is found but version cannot be detected



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.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 version file found, return default version (for non-gem projects)
  "0.0.0"
end