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 +4 -4
- data/CHANGELOG.md +21 -0
- data/lib/gemchain/cascade.rb +6 -4
- data/lib/gemchain/cli.rb +11 -4
- data/lib/gemchain/executor.rb +46 -8
- data/lib/gemchain/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28efcb002b8ff78a3dad0554ff43dd7082221c42a927d0f61fd31d892fff87c1
|
|
4
|
+
data.tar.gz: 8e2f0892d8018723ce6f90c27abdd877249889b1f2bc357de0c9811083bf5fe5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/gemchain/cascade.rb
CHANGED
|
@@ -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.
|
|
113
|
-
# is unreachable, assume not published so
|
|
114
|
-
# release) is preferred — bumping an
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
data/lib/gemchain/executor.rb
CHANGED
|
@@ -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
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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.
|
data/lib/gemchain/version.rb
CHANGED