gemchain 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f57089a36952e2778e0ca90654ee97a6484ba1f6f9c002a91c1a30089c6973a
4
- data.tar.gz: cd1f24b7232ba48c8f1135d8e80374eb656e49e3322d3773b1f3680020761160
3
+ metadata.gz: 28efcb002b8ff78a3dad0554ff43dd7082221c42a927d0f61fd31d892fff87c1
4
+ data.tar.gz: 8e2f0892d8018723ce6f90c27abdd877249889b1f2bc357de0c9811083bf5fe5
5
5
  SHA512:
6
- metadata.gz: 989b65d9e09fc06ea8445c7137b5e641fd001f7ab332023132a075c91e3a1d7b9649d34470fe78650d2c258507e5f2729d253824d85ec38b082b2efd7121e829
7
- data.tar.gz: 65efa9a75881d8880d950c2b880816e10d575c9cbf728f427a0679f5fc6fc77c37f41d836a44d5359dbd412b0d8e6bf18db25772c2cae317d37b53dc58dcef1b
6
+ metadata.gz: 606408575bd7f7e3fd77d5b7185c37435617d8f8d728caeb42f4a42ddccd15c7570e1999ead8390703b31890720c23f886258493c79e3f066350c5a785fe98e5
7
+ data.tar.gz: 1afe1853d547bcf461a517b0f1216c206c84566ac1d3116aa7eefe8f72221e4d8b5e86bb82ed48bd5aba403d9652ea4204715dac90b493ccabdab3265b61e9b1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## [0.2.4] — 2026-09-17
2
+
3
+ ### Fixed
4
+
5
+ - **Cascade continues past dependent failures** — when a dependent fails
6
+ (test, build, push, etc.), its remaining steps are skipped but the
7
+ cascade continues to the next gem. Healthy dependents are still updated
8
+ and released. Previously a single failure (e.g. missing git remote)
9
+ halted the entire cascade, leaving perfectly good dependents untouched.
10
+ The final report now lists which gems failed and which succeeded.
11
+
12
+ ## [0.2.3] — 2026-08-13
13
+
14
+ ### Fixed
15
+
16
+ - **Release step is idempotent for already-published versions** — a re-run
17
+ (or a flaky RubyGems API check during mode detection) reached `:release`
18
+ with the version already published and collided on `git tag`. The release
19
+ now checks RubyGems first (with a 5s timeout) and skips build/push/tag
20
+ when the version is already published.
21
+
1
22
  ## [0.2.2] — 2026-08-13
2
23
 
3
24
  ### Fixed
@@ -109,13 +109,15 @@ module Gemchain
109
109
  lines.join("\n")
110
110
  end
111
111
 
112
- # True when +name+ is published at +version+ on RubyGems. When rubygems
113
- # is unreachable, assume not published so the full mode (bump → test →
114
- # release) is preferred — bumping an already-published version is a no-op.
112
+ # True when +name+ is published at +version+ on RubyGems. Bounded by a
113
+ # short timeout; when rubygems is unreachable, assume not published so
114
+ # the full mode (bump → test → release) is preferred — bumping an
115
+ # already-published version is a no-op, and the executor's release step
116
+ # additionally skips re-releases of published versions.
115
117
  def released_on_rubygems?(name, version)
116
118
  require "json"
117
119
  require "net/http"
118
- body = Net::HTTP.get(URI("https://rubygems.org/api/v1/gems/#{name}.json"))
120
+ body = Timeout.timeout(5) { Net::HTTP.get(URI("https://rubygems.org/api/v1/gems/#{name}.json")) }
119
121
  JSON.parse(body)["version"] == version
120
122
  rescue StandardError
121
123
  false
data/lib/gemchain/cli.rb CHANGED
@@ -120,10 +120,17 @@ module Gemchain
120
120
  puts "\n Cascade complete."
121
121
  end
122
122
  else
123
- gem, message = result.failed.first
124
- puts "\n ✖ Cascade stopped at #{gem}:"
125
- puts " #{message}"
126
- puts " Released so far: #{result.released.join(', ')}" unless result.released.empty?
123
+ puts "\n ⚠️ Cascade completed with failures:"
124
+ result.failed.each do |gem, message|
125
+ puts "#{gem}: #{message.split("\n").first}"
126
+ end
127
+ puts "\n ✅ Released: #{result.released.join(', ')}" unless result.released.empty?
128
+ puts " ⏭ Skipped: #{result.skipped.join(', ')}" unless result.skipped.empty?
129
+ if result.released.empty?
130
+ puts "\n No gems were released."
131
+ else
132
+ puts "\n Good dependents were updated successfully."
133
+ end
127
134
  end
128
135
  end
129
136
  puts
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require "set"
5
+ require "timeout"
4
6
 
5
7
  module Gemchain
6
8
  class ExecutionError < Error; end
7
9
 
8
10
  # Executes a cascade plan: bumps versions, rewrites gemspec constraints,
9
- # runs test suites, and releases gems to RubyGems stopping at the first
10
- # failure. All shell interaction goes through the +runner+ so the whole
11
- # pipeline is testable without touching bundler, git, or RubyGems.
11
+ # runs test suites, and releases gems to RubyGems. On failure the
12
+ # remaining steps for the failing gem are skipped but the cascade
13
+ # continues to the next gem, so healthy dependents are still updated.
14
+ # All shell interaction goes through the +runner+ so the whole pipeline
15
+ # is testable without touching bundler, git, or RubyGems.
12
16
  class Executor
13
17
  Result = Struct.new(:success, :released, :skipped, :failed, keyword_init: true)
14
18
 
@@ -36,14 +40,17 @@ module Gemchain
36
40
 
37
41
  # @param workspace [Workspace] the gem ecosystem
38
42
  # @param steps [Array<Hash>] the plan from Cascade#update
39
- # @param runner [#run] command runner (defaults to the real one)
43
+ # @param runner [#run, #success?] command runner (defaults to the real one)
40
44
  # @param confirm [Proc] per-release confirmation, called with a message;
41
45
  # defaults to a [y/N] prompt on stdin
42
46
  # @param yes [Boolean] skip confirmations entirely
43
47
  # @param bump [Symbol] dependent version bump level (:patch, :minor, :major)
44
48
  # @param output [#puts] progress output stream
49
+ # @param released_check [Proc] (name, version) → published on RubyGems?
50
+ # Used to skip releases whose version is already published; defaults
51
+ # to a RubyGems API lookup with a short timeout.
45
52
  def initialize(workspace:, steps:, runner: nil, confirm: nil, yes: false,
46
- bump: :patch, output: $stdout)
53
+ bump: :patch, output: $stdout, released_check: nil)
47
54
  @workspace = workspace
48
55
  @steps = steps
49
56
  @runner = runner || Runner.new
@@ -51,14 +58,23 @@ module Gemchain
51
58
  @yes = yes
52
59
  @bump = bump.to_sym
53
60
  @output = output
61
+ @released_check = released_check || method(:released_on_rubygems?)
54
62
  raise ArgumentError, "bump must be one of #{BUMP_LEVELS.inspect}" unless BUMP_LEVELS.include?(@bump)
55
63
  end
56
64
 
57
65
  def run
58
66
  released = []
59
67
  skipped = []
68
+ failed = {}
69
+ failed_gems = Set.new
60
70
 
61
71
  @steps.each do |step|
72
+ # Skip remaining steps for a gem that already failed
73
+ if failed_gems.include?(step[:gem])
74
+ @output.puts " ⏭ Skipping remaining steps for #{step[:gem]} (previous step failed)"
75
+ next
76
+ end
77
+
62
78
  begin
63
79
  case step[:action]
64
80
  when :bump
@@ -77,12 +93,14 @@ module Gemchain
77
93
  end
78
94
  end
79
95
  rescue ExecutionError => e
80
- return Result.new(success: false, released: released, skipped: skipped,
81
- failed: {step[:gem] => e.message})
96
+ @output.puts " ✖ #{step[:gem]}: #{e.message.split("\n").first}"
97
+ failed[step[:gem]] = e.message
98
+ failed_gems << step[:gem]
82
99
  end
83
100
  end
84
101
 
85
- Result.new(success: true, released: released, skipped: skipped, failed: nil)
102
+ success = failed.empty?
103
+ Result.new(success: success, released: released, skipped: skipped, failed: failed.empty? ? nil : failed)
86
104
  end
87
105
 
88
106
  private
@@ -169,6 +187,14 @@ module Gemchain
169
187
  name = step[:gem]
170
188
  version = step[:version] || bump_dependent(dir, name)
171
189
 
190
+ # Idempotency guard: a re-run (or flaky mode detection) can reach
191
+ # :release with the version already published — re-pushing/tagging
192
+ # would fail. Skip the release entirely in that case.
193
+ if @released_check.call(name, version)
194
+ @output.puts " ⏭ #{name} #{version} already on RubyGems — skipping release"
195
+ return
196
+ end
197
+
172
198
  files = release_files(dir)
173
199
  @runner.run(["git", "add", *files], chdir: dir)
174
200
 
@@ -205,6 +231,18 @@ module Gemchain
205
231
  status.success?
206
232
  end
207
233
 
234
+ # True when +name+ is published at +version+ on RubyGems. Bounded by a
235
+ # short timeout; on any failure assume NOT published so the release
236
+ # proceeds (a no-op re-push is safer than silently skipping a real one).
237
+ def released_on_rubygems?(name, version)
238
+ require "json"
239
+ require "net/http"
240
+ body = Timeout.timeout(5) { Net::HTTP.get(URI("https://rubygems.org/api/v1/gems/#{name}.json")) }
241
+ JSON.parse(body)["version"] == version
242
+ rescue StandardError
243
+ false
244
+ end
245
+
208
246
  # Dependents get an automatic version bump (they're being re-released
209
247
  # with a tightened constraint); the source gem's version comes from the
210
248
  # :bump step.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gemchain
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemchain
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto