Module: Rakit::Gem

Defined in:
lib/rakit/gem.rb

Class Method Summary collapse

Class Method Details

.bump(gemspec_path) ⇒ Object

Bump the last digit of the version in the gemspec file (e.g. “0.1.0” -> “0.1.1”). Writes the file in place. Returns the new version string.



31
32
33
34
35
36
37
38
39
40
# File 'lib/rakit/gem.rb', line 31

def self.bump(gemspec_path)
  content = ::File.read(gemspec_path)
  content.sub!(/^(\s*s\.version\s*=\s*["'])([\d.]+)(["'])/) do
    segs = Regexp.last_match(2).split(".")
    segs[-1] = (segs[-1].to_i + 1).to_s
    "#{Regexp.last_match(1)}#{segs.join(".")}#{Regexp.last_match(3)}"
  end or raise "No s.version line found in #{gemspec_path}"
  ::File.write(gemspec_path, content)
  content[/s\.version\s*=\s*["']([^"']+)["']/, 1]
end

.http_get_following_redirects(uri, limit: 5) ⇒ Object

Raises:

  • (ArgumentError)


144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/rakit/gem.rb', line 144

def self.http_get_following_redirects(uri, limit: 5)
  raise ArgumentError, "redirect limit exceeded" if limit <= 0
  require "net/http"
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 10) do |http|
    request = Net::HTTP::Get.new(uri)
    request["User-Agent"] = "rakit (https://rubygems.org/gems/rakit)"
    http.request(request)
  end
  case response
  when Net::HTTPRedirection
    location = response["location"]
    next_uri = location.match?(/\Ahttps?:\/\//) ? URI(location) : URI.join(uri, location)
    http_get_following_redirects(next_uri, limit: limit - 1)
  else
    response
  end
end

.latest_remote_version(name) ⇒ Object

Return the latest version string for name on the remote, or nil on error.



93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/rakit/gem.rb', line 93

def self.latest_remote_version(name)
  out, _err, status = Open3.capture3("gem", "list", name, "--remote")
  return nil unless status.success?
  # Format: "name (1.0.0)" for latest only, or "name (1.0.0, 0.9.0)" with --all
  out.each_line do |line|
    next unless line.include?(name)
    if line =~ /\s*#{Regexp.escape(name)}\s*\(([^)]+)\)/
      versions = Regexp.last_match(1).split(",").map(&:strip)
      return versions.max { |a, b| ::Gem::Version.new(a) <=> ::Gem::Version.new(b) }
    end
  end
  nil
end

.package(spec, out_dir) ⇒ Object

Build the gem for the given spec and move it into out_dir. Returns the path to the built .gem file.



12
13
14
15
16
17
# File 'lib/rakit/gem.rb', line 12

def self.package(spec, out_dir)
  FileUtils.mkdir_p(out_dir)
  gem_file = ::Gem::Package.build(spec)
  FileUtils.mv(gem_file, out_dir)
  ::File.join(out_dir, gem_file)
end

.parse_gem_basename(base) ⇒ Object

Parse “name-version” basename (no .gem) into [name, version]. Version is the last hyphen-separated segment that looks like a version (e.g. 0.1.5).



63
64
65
66
67
# File 'lib/rakit/gem.rb', line 63

def self.parse_gem_basename(base)
  # Match name (may contain hyphens) and version (digits and dots, optional pre-release suffix).
  m = base.match(/\A(.+)-(\d+(?:\.\d+)*(?:\.\w+)?)\z/)
  m ? [m[1], m[2]] : nil
end

.publish(gemspec_path) ⇒ Object

Publish the gem to rubygems.org. Loads the gemspec from gemspec_path and expects the .gem file in dirname(gemspec_path)/artifacts/. Run package first.



21
22
23
24
25
26
27
# File 'lib/rakit/gem.rb', line 21

def self.publish(gemspec_path)
  path = ::File.expand_path(gemspec_path)
  spec = ::Gem::Specification.load(path)
  out_dir = ::File.join(::File.dirname(path), "artifacts")
  gem_path = ::File.join(out_dir, "#{spec.full_name}.gem")
  push(gem_path)
end

.push(gem_path) ⇒ Object

Push the .gem at gem_path to rubygems.org. If that version is already published, warns and returns without pushing. Raises if the file is missing or if gem push fails.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/rakit/gem.rb', line 45

def self.push(gem_path)
  raise "Gem not found: #{gem_path}. Run rake package first." unless ::File.file?(gem_path)

  base = ::File.basename(gem_path, ".gem")
  name, version = parse_gem_basename(base)
  raise "Could not parse name/version from #{base}.gem" unless name && version

  if version_published?(name, version)
    warn "publish: Version #{version} of #{name} is already published on rubygems.org. Skipping push. Bump the version in the gemspec to publish again."
    return
  end

  success = system("gem", "push", gem_path)
  raise "gem push failed" unless success
end

.version_published?(name, version) ⇒ Boolean

Returns:

  • (Boolean)


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rakit/gem.rb', line 69

def self.version_published?(name, version)
  # If a version >= this one is already published (e.g. latest is higher), treat as published.
  latest = latest_remote_version(name)
  return true if latest && ::Gem::Version.new(latest) >= ::Gem::Version.new(version)

  begin
    return true if version_published_gem_list?(name, version)
  rescue StandardError
    # try API fallbacks
  end
  begin
    return true if version_published_v2?(name, version)
  rescue StandardError
    # try v1 fallback
  end
  begin
    return true if version_published_v1?(name, version)
  rescue StandardError
    nil
  end
  false
end

.version_published_gem_list?(name, version) ⇒ Boolean

Run ‘gem list NAME –remote` and check if version appears in the output.

Returns:

  • (Boolean)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/rakit/gem.rb', line 108

def self.version_published_gem_list?(name, version)
  out, err, status = Open3.capture3("gem", "list", name, "--remote")
  return false unless status.success?
  # Output format: "name (1.0.0, 0.9.0)" or "name (1.0.0)"
  combined = "#{out}#{err}"
  combined.each_line do |line|
    next unless line.include?(name)
    if line =~ /\s*#{Regexp.escape(name)}\s*\((.*)\)/
      versions = Regexp.last_match(1).split(",").map(&:strip)
      return true if versions.include?(version)
    end
  end
  false
end

.version_published_v1?(name, version) ⇒ Boolean

GET /api/v1/versions/name.json and check if version is in the list

Returns:

  • (Boolean)


133
134
135
136
137
138
139
140
141
142
# File 'lib/rakit/gem.rb', line 133

def self.version_published_v1?(name, version)
  require "net/http"
  require "json"
  require "uri"
  uri = URI("https://rubygems.org/api/v1/versions/#{URI::DEFAULT_PARSER.escape(name)}.json")
  response = http_get_following_redirects(uri)
  return false unless response.is_a?(Net::HTTPSuccess)
  list = JSON.parse(response.body)
  list.is_a?(Array) && list.any? { |h| h["number"] == version }
end

.version_published_v2?(name, version) ⇒ Boolean

GET /api/v2/rubygems/name/versions/version.json (follows redirects)

Returns:

  • (Boolean)


124
125
126
127
128
129
130
# File 'lib/rakit/gem.rb', line 124

def self.version_published_v2?(name, version)
  require "net/http"
  require "uri"
  uri = URI("https://rubygems.org/api/v2/rubygems/#{URI::DEFAULT_PARSER.escape(name)}/versions/#{URI::DEFAULT_PARSER.escape(version)}.json")
  response = http_get_following_redirects(uri)
  response.is_a?(Net::HTTPSuccess)
end