Class: Gemchain::Cascade

Inherits:
Object
  • Object
show all
Defined in:
lib/gemchain/cascade.rb

Defined Under Namespace

Classes: Result

Instance Method Summary collapse

Constructor Details

#initialize(workspace, released_check: nil) ⇒ Cascade

Returns a new instance of Cascade.



7
8
9
10
# File 'lib/gemchain/cascade.rb', line 7

def initialize(workspace, released_check: nil)
  @workspace = workspace
  @released_check = released_check || method(:released_on_rubygems?)
end

Instance Method Details

#checkObject

Returns a full ecosystem check.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/gemchain/cascade.rb', line 31

def check
  gems_info = @workspace.gems.map do |gem|
    deps = @workspace.dependents_of(gem.name)
    {
      name: gem.name,
      version: gem.version.to_s,
      path: gem.path,
      dependencies: gem.dependencies,
      dependents: deps.map(&:name),
      dependent_count: deps.size
    }
  end

  {gems: gems_info}
end

#guard(gem_name) ⇒ Object

Pre-release guard: warns about gems that depend on gem_name. Returns a hash with dependency info, or nil if gem not found.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/gemchain/cascade.rb', line 14

def guard(gem_name)
  gem = @workspace.find(gem_name)
  return nil unless gem

  direct = @workspace.dependents_of(gem_name)
  transitive = @workspace.transitive_dependents(gem_name)

  {
    gem: gem,
    direct_dependents: direct.size,
    dependents: direct,
    transitive_dependents: transitive,
    all_downstream: transitive.map { |n| @workspace.find(n) }.compact
  }
end

Generates Gemfile content with local path references for all workspace gems.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/gemchain/cascade.rb', line 93

def link_content(workspace: nil)
  workspace ||= Dir.pwd
  lines = []
  lines << "# Auto-generated by gemchain — local development path references"
  lines << "# Replace in your Gemfile to test against local copies"
  lines << ""
  lines << "# Add this block to your Gemfile:"
  lines << "ask_rb_root = File.expand_path(\"..\", __dir__)" if workspace
  lines << ""

  gem_names = @workspace.gems.map(&:name).sort
  lines << "%w[#{gem_names.join(' ')}].each do |gem_name|"
  lines << "  gem gem_name, path: File.join(ask_rb_root, gem_name)"
  lines << "end"
  lines << ""

  lines.join("\n")
end

#released_on_rubygems?(name, version) ⇒ Boolean

True when name is published at version on RubyGems. Bounded by a short timeout; when rubygems is unreachable, assume not published so the full mode (bump → test → release) is preferred — bumping an already-published version is a no-op, and the executor's release step additionally skips re-releases of published versions.

Returns:

  • (Boolean)


117
118
119
120
121
122
123
124
# File 'lib/gemchain/cascade.rb', line 117

def released_on_rubygems?(name, version)
  require "json"
  require "net/http"
  body = Timeout.timeout(5) { Net::HTTP.get(URI("https://rubygems.org/api/v1/gems/#{name}.json")) }
  JSON.parse(body)["version"] == version
rescue StandardError
  false
end

#update(gem_name, new_version, dry_run: false, test_only: false) ⇒ Object

Performs the full cascade update:

  1. If the source gem isn't already at new_version AND published on RubyGems: bump, test, release it (a version.rb match alone is not enough — a failed run can leave it bumped but unpublished)
  2. For each dependent (topological order): update constraint, test, bump, release

With test_only: true, only the dependents' :test steps are emitted — verify compatibility without editing or releasing anything.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/gemchain/cascade.rb', line 55

def update(gem_name, new_version, dry_run: false, test_only: false)
  gem = @workspace.find(gem_name)
  return Result.new(success: false, message: "Gem '#{gem_name}' not found in workspace") unless gem

  unless new_version.match?(/\A\d+\.\d+\.\d+\z/)
    return Result.new(success: false, message: "Invalid version: #{new_version.inspect} — expected X.Y.Z")
  end

  steps = []
  order = @workspace.cascade_order(gem_name)

  if !test_only && (gem.version.to_s != new_version || !@released_check.call(gem_name, new_version))
    # The source must be released before dependents' `bundle update`
    # can resolve the new version from RubyGems.
    steps << {action: :bump, gem: gem_name, version: new_version}
    steps << {action: :test, gem: gem_name}
    steps << {action: :release, gem: gem_name, version: new_version}
  end

  order.each do |dep_name|
    next if dep_name == gem_name
    dep_gem = @workspace.find(dep_name)
    next unless dep_gem
    next unless dep_gem.dependencies.key?(gem_name)

    if test_only
      steps << {action: :test, gem: dep_name, dependency: gem_name}
    else
      steps << {action: :update_dependency, gem: dep_name, dependency: gem_name, constraint: ">= #{new_version}"}
      steps << {action: :test, gem: dep_name, dependency: gem_name}
      steps << {action: :release, gem: dep_name}
    end
  end

  {steps: steps, dry_run: dry_run, test_only: test_only}
end