kettle-dev 2.4.2 → 2.4.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: a251345bb56780a5c5b9efcfb53d394ca469581b5798f10839ac8ed5b4cae682
4
- data.tar.gz: 4a81578792afe11fd09299ea7d1836d66f0cd8c76af00235491b8234fcec1553
3
+ metadata.gz: a4bf3f7184786c2e3511e39e034241b7818591eaf45b86b56cd40a281b582b6a
4
+ data.tar.gz: 305319beff2fe1cab480ae0570e1658afc625282e47a865f8579bc4f880a9846
5
5
  SHA512:
6
- metadata.gz: 0cfa39888af23e14856d9874d965248c8d68a83b88c42bd3cc722b1a84fe8564b3c1381aa714aaebe0df531822365b70e4f2e7e2eff913f5e781e201e5f49b13
7
- data.tar.gz: aefab75e861bc0c2132385b4e7411904b01cb6562c82d3485cdbdbb27d407004450b8e0a7030f33abf869bc73df4074f2cbdd1c3ef7abcfb21e9222b5c1c7f9e
6
+ metadata.gz: c7046d8d77386cebc3f9379735621d8cdb29b7ca9d02cf88933ac2583bc7d55dfdf013835373c1c0997ffb7cbb383d8fcbfb761023805e41b0b2b0fb4e9f2eaf
7
+ data.tar.gz: 751fa7efe9475e49fd0748e9f90875120385bbc98c5ca8364d680c4d549a544b7e1cfc4ca7bf2fa03eefdb00e16151e7769cae3f3c8216f5f8cb19d60d3f63fb
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,58 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [2.4.4] - 2026-07-25
34
+
35
+ - TAG: [v2.4.4][2.4.4t]
36
+ - COVERAGE: 92.01% -- 4079/4433 lines in 38 files
37
+ - BRANCH COVERAGE: 76.04% -- 1587/2087 branches in 38 files
38
+ - 59.16% documented
39
+
40
+ ### Fixed
41
+
42
+ - `kettle-release` availability validation now resolves the exact published gem
43
+ from gem.coop without requiring it, so framework-dependent gems are not
44
+ mistaken for unavailable releases after RubyGems accepts the push.
45
+ - `kettle-release` now records a candidate as published immediately after the
46
+ push command succeeds, preventing post-publish validation failures from
47
+ triggering unpublished-gem local cleanup.
48
+
49
+ - Legacy-engine GitHub Actions workflows now configure Bundler to mirror
50
+ gem.coop to RubyGems.org during explicit CI installs, avoiding unsupported
51
+ gem.coop full-index requests from old Bundler/RubyGems combinations.
52
+
53
+ ## [2.4.3] - 2026-07-25
54
+
55
+ - TAG: [v2.4.3][2.4.3t]
56
+ - COVERAGE: 92.17% -- 4082/4429 lines in 38 files
57
+ - BRANCH COVERAGE: 76.38% -- 1594/2087 branches in 38 files
58
+ - 59.16% documented
59
+
60
+ ### Added
61
+
62
+ - `kettle-release` now supports `--events[=TYPES]`, `--json`, and
63
+ `--report PATH` machine-readable release reporting backed by the shared
64
+ `kettle-ndjson` event toolkit.
65
+
66
+ ### Changed
67
+
68
+ - kettle-jem-template-20260725-001 - Generated JRuby and TruffleRuby workflow
69
+ files now run when pull request head branches start with `feature/release`,
70
+ so release CI monitoring does not report intentionally skipped engine
71
+ workflows as failures.
72
+
73
+ ### Fixed
74
+
75
+ - `kettle-release` now retries transient git remote fetch failures during
76
+ strict pre-push trunk parity checks, and failures now include an explicit
77
+ `--skip-remotes` recovery hint for intentionally unavailable remotes.
78
+ - `kettle-release` now waits briefly and retries post-publish gem.coop
79
+ availability validation before failing, so normal registry mirror lag does
80
+ not abort immediately after a successful RubyGems push.
81
+
82
+ - Raised the `kettle-ndjson` runtime dependency floor to v0.1.1 so the
83
+ dependency remains installable on kettle-dev's Ruby 2.4+ support floor.
84
+
33
85
  ## [2.4.2] - 2026-07-24
34
86
 
35
87
  - TAG: [v2.4.2][2.4.2t]
@@ -2674,7 +2726,11 @@ Please file a bug if you notice a violation of semantic versioning.
2674
2726
  - Selecting will run the selected workflow via `act`
2675
2727
  - This may move to its own gem in the future.
2676
2728
 
2677
- [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.2...HEAD
2729
+ [Unreleased]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.4...HEAD
2730
+ [2.4.4]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.3...v2.4.4
2731
+ [2.4.4t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.4.4
2732
+ [2.4.3]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.2...v2.4.3
2733
+ [2.4.3t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.4.3
2678
2734
  [2.4.2]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.1...v2.4.2
2679
2735
  [2.4.2t]: https://github.com/kettle-dev/kettle-dev/releases/tag/v2.4.2
2680
2736
  [2.4.1]: https://github.com/kettle-dev/kettle-dev/compare/v2.4.0...v2.4.1
data/README.md CHANGED
@@ -891,7 +891,7 @@ Thanks for RTFM. ☺️
891
891
  [📌gitmoji]: https://gitmoji.dev
892
892
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
893
893
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
894
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.321-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
894
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.433-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
895
895
  [🔐security]: https://github.com/kettle-dev/kettle-dev/blob/main/SECURITY.md
896
896
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
897
897
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/exe/kettle-release CHANGED
@@ -35,8 +35,10 @@ $LOAD_PATH.unshift(repo_lib) if File.directory?(repo_lib) && !$LOAD_PATH.include
35
35
  begin
36
36
  require "kettle/dev"
37
37
  require "kettle/dev/executable_version"
38
+ require "kettle/dev/release_cli"
38
39
  Kettle::Dev::ExecutableVersion.print_and_exit!(script_basename, ARGV, value_option: true)
39
- Kettle::Dev::ExecutableVersion.print_header(script_basename)
40
+ machine_output = ARGV.any? { |arg| arg == "--json" || arg.start_with?("--events") }
41
+ Kettle::Dev::ExecutableVersion.print_header(script_basename) unless machine_output
40
42
  rescue LoadError => e
41
43
  warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
42
44
  warn("Hint: Ensure the host project has kettle-dev as a dependency and run bundle install.")
@@ -47,7 +49,7 @@ end
47
49
  # Do not guard with __FILE__ == $PROGRAM_NAME because binstubs use Kernel.load.
48
50
  if ARGV.include?("-h") || ARGV.include?("--help")
49
51
  puts <<~USAGE
50
- Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [--ci-workflows WORKFLOWS] [--skip-remotes REMOTES] [start_step=<0-19>]
52
+ Usage: kettle-release [--version VERSION] [--local-ci] [--appraisal-update] [--skip-bundle-audit] [--skip-steps STEPS] [--ci-workflows WORKFLOWS] [--skip-remotes REMOTES] [--json|--events[=TYPES]] [--report PATH] [start_step=<0-19>]
51
53
 
52
54
  Automates the release flow for a Ruby gem in the host project.
53
55
 
@@ -87,6 +89,9 @@ if ARGV.include?("-h") || ARGV.include?("--help")
87
89
  --skip-bundle-audit # Skip bundle:audit/update during release rake checks
88
90
  --ci-workflows WORKFLOWS # Comma-separated workflow files or stems to monitor at CI step 10
89
91
  --skip-remotes REMOTES # Comma-separated git remote names to exclude from release fetch/push parity
92
+ --json # Print final machine-readable release report as JSON
93
+ --events[=TYPES] # Print newline-delimited JSON release events, optionally filtered by type
94
+ --report PATH # Write final machine-readable release report as JSON
90
95
  --local-ci # Sensitive release mode: run act locally, publish before any git push,
91
96
  # create the git tag locally, then push commits and tags after publish
92
97
 
@@ -195,18 +200,60 @@ def extract_skip_remotes_arg!(argv)
195
200
  remotes
196
201
  end
197
202
 
203
+ def extract_report_arg!(argv)
204
+ report_path = nil
205
+ if (idx = argv.index("--report"))
206
+ report_path = argv[idx + 1]
207
+ if report_path.to_s.empty? || report_path.to_s.start_with?("-")
208
+ Kettle::Dev::ExitAdapter.abort("--report requires a path")
209
+ end
210
+ argv.slice!(idx, 2)
211
+ end
212
+ argv.delete_if do |arg|
213
+ if arg.start_with?("--report=", "report=")
214
+ report_path = arg.split("=", 2)[1]
215
+ Kettle::Dev::ExitAdapter.abort("--report requires a path") if report_path.to_s.empty?
216
+ true
217
+ else
218
+ false
219
+ end
220
+ end
221
+ report_path
222
+ end
223
+
224
+ def extract_events_arg!(argv)
225
+ events = false
226
+ event_types = nil
227
+ argv.delete_if do |arg|
228
+ if arg == "--events"
229
+ events = true
230
+ elsif arg.start_with?("--events=")
231
+ events = true
232
+ event_types = arg.split("=", 2)[1]
233
+ else
234
+ false
235
+ end
236
+ end
237
+ [events, event_types]
238
+ end
239
+
198
240
  local_ci = ARGV.include?("--local-ci")
199
241
  appraisal_task = ARGV.delete("--appraisal-update") ? "appraisal:update" : nil
200
242
  skip_bundle_audit = !!ARGV.delete("--skip-bundle-audit")
243
+ json_output = !!ARGV.delete("--json")
244
+ events_output, event_types = extract_events_arg!(ARGV)
245
+ Kettle::Dev::ExitAdapter.abort("--events cannot be combined with --json") if events_output && json_output
201
246
  start_step_arg = ARGV.find { |a| a.start_with?("start_step=") }
202
247
  start_step = start_step_arg ? start_step_arg.split("=", 2)[1].to_i : 0
203
248
  version_override = extract_version_arg!(ARGV)
204
249
  skip_steps = extract_skip_steps_arg!(ARGV)
205
250
  ci_workflows = extract_ci_workflows_arg!(ARGV)
206
251
  skip_remotes = extract_skip_remotes_arg!(ARGV)
252
+ report_path = extract_report_arg!(ARGV)
207
253
 
208
254
  begin
209
- Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task, skip_steps: skip_steps, skip_bundle_audit: skip_bundle_audit, ci_workflows: ci_workflows, skip_remotes: skip_remotes).run
255
+ event_stream = events_output ? Kettle::Ndjson.event_stream($stdout, types: event_types) : nil
256
+ Kettle::Dev::ReleaseCLI.new(start_step: start_step, local_ci: local_ci, version: version_override, appraisal_task: appraisal_task, skip_steps: skip_steps, skip_bundle_audit: skip_bundle_audit, ci_workflows: ci_workflows, skip_remotes: skip_remotes, event_stream: event_stream, json_output: json_output, json_io: $stdout, report_path: report_path).run
210
257
  rescue LoadError => e
211
258
  warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
212
259
  warn(Array(e.backtrace).join("\n")) if ENV["DEBUG"]
@@ -13,6 +13,7 @@ require "yaml"
13
13
  require "set"
14
14
 
15
15
  # External gems
16
+ require "kettle/ndjson"
16
17
  require "kettle/rb/compat_matrix"
17
18
  require "ruby-progressbar"
18
19
 
@@ -124,14 +125,43 @@ module Kettle
124
125
  @version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
125
126
  @appraisal_task = normalize_appraisal_task(appraisal_task || ENV["KETTLE_RELEASE_APPRAISAL_TASK"])
126
127
  @release_candidate = nil
128
+ @event_stream = options[:event_stream]
129
+ @event_recorder = Kettle::Ndjson.event_recorder(@event_stream, phase_timings: [])
130
+ @report_path = options[:report_path]
131
+ @json_output = !!options[:json_output]
132
+ @json_io = options[:json_io] || $stdout
133
+ @command_events = []
134
+ @diagnostics = []
135
+ @started_at = nil
136
+ @finished_report = nil
127
137
  end
128
138
 
129
139
  def run
140
+ @started_at = monotonic_time
141
+ emit_run_start
142
+ status = "ok"
143
+ error = nil
130
144
  with_bundle_audit_skip_env do
131
- run_with_release_environment
145
+ with_machine_stdout_redirect do
146
+ run_with_release_environment
147
+ end
132
148
  end
149
+ rescue SystemExit => e
150
+ status = e.status.to_i.zero? ? "ok" : "failed"
151
+ error = e
152
+ record_diagnostic("release_exit", e.message, severity: (status == "ok") ? "info" : "error", blocking: status != "ok")
153
+ raise
154
+ rescue => e
155
+ status = "failed"
156
+ error = e
157
+ record_diagnostic("release_error", "#{e.class}: #{e.message}", severity: "error", blocking: true)
158
+ raise
159
+ ensure
160
+ finish_release_report(status: status, error: error)
133
161
  end
134
162
 
163
+ attr_reader :finished_report
164
+
135
165
  def run_with_release_environment
136
166
  run_pre_release_checks! if run_step?(0)
137
167
 
@@ -358,6 +388,7 @@ module Kettle
358
388
  puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
359
389
  with_unpublished_candidate_cleanup do
360
390
  run_cmd!("bundle exec rake release")
391
+ @release_candidate.published = true
361
392
  confirm_release_candidate_available!(@release_candidate)
362
393
  end
363
394
  mark_gem_coop_release_cache_bust(version)
@@ -395,11 +426,11 @@ module Kettle
395
426
  begin
396
427
  version ||= detect_version
397
428
  gem_name = detect_gem_name
398
- puts "\n🚀 Release #{gem_name} v#{version} Complete 🚀"
429
+ human_output.puts "\n🚀 Release #{gem_name} v#{version} Complete 🚀"
399
430
  rescue => e
400
431
  Kettle::Dev.debug_error(e, __method__)
401
432
  # Fallback if detection fails for any reason
402
- puts "\n🚀 Release v#{version || "unknown"} Complete 🚀"
433
+ human_output.puts "\n🚀 Release v#{version || "unknown"} Complete 🚀"
403
434
  end
404
435
  end
405
436
 
@@ -1007,9 +1038,19 @@ module Kettle
1007
1038
 
1008
1039
  def run_cmd!(cmd)
1009
1040
  cmd = bundle_audit_skip_command(cmd)
1041
+ emit_command_event(cmd, "started")
1010
1042
  with_bundle_audit_skip_env do
1011
- self.class.run_cmd!(cmd)
1043
+ with_machine_stdout_redirect do
1044
+ self.class.run_cmd!(cmd)
1045
+ end
1012
1046
  end
1047
+ emit_command_event(cmd, "ok")
1048
+ rescue SystemExit => e
1049
+ emit_command_event(cmd, "failed", reason: e.message)
1050
+ raise
1051
+ rescue => e
1052
+ emit_command_event(cmd, "failed", reason: "#{e.class}: #{e.message}")
1053
+ raise
1013
1054
  end
1014
1055
 
1015
1056
  def bundle_audit_skip_command(cmd)
@@ -1152,8 +1193,11 @@ module Kettle
1152
1193
 
1153
1194
  puts "Publishing #{File.basename(gem_path)} to RubyGems without pushing git refs..."
1154
1195
  run_cmd!("gem push #{Shellwords.escape(gem_path)}")
1155
- confirm_release_candidate_available!(@release_candidate || build_release_candidate(gem_name_from_gem_path(gem_path, version), version))
1156
- mark_gem_coop_release_cache_bust(version, gem_name: gem_name_from_gem_path(gem_path, version))
1196
+ gem_name = gem_name_from_gem_path(gem_path, version)
1197
+ @release_candidate ||= build_release_candidate(gem_name, version)
1198
+ @release_candidate.published = true
1199
+ confirm_release_candidate_available!(@release_candidate)
1200
+ mark_gem_coop_release_cache_bust(version, gem_name: gem_name)
1157
1201
  end
1158
1202
 
1159
1203
  def build_release_candidate(gem_name, version)
@@ -1218,21 +1262,45 @@ module Kettle
1218
1262
 
1219
1263
  def run_release_availability_probe(candidate)
1220
1264
  script_path = write_release_availability_probe(candidate)
1221
- stdout_str = nil
1222
- stderr_str = nil
1223
- status = nil
1224
- with_unbundled_release_probe_env do
1225
- stdout_str, stderr_str, status = Open3.capture3(self.class.send(:command_env), Gem.ruby, script_path)
1265
+ attempts = release_availability_probe_attempts
1266
+ last_stderr = nil
1267
+ last_status = nil
1268
+ sleep(release_availability_probe_initial_delay)
1269
+ attempts.times do |index|
1270
+ attempt = index + 1
1271
+ puts("Validating #{candidate.gem_name} #{candidate.version} from #{RELEASE_VALIDATION_SOURCE} (attempt #{attempt}/#{attempts})")
1272
+ stdout_str = nil
1273
+ stderr_str = nil
1274
+ status = nil
1275
+ with_unbundled_release_probe_env do
1276
+ stdout_str, stderr_str, status = Open3.capture3(self.class.send(:command_env), Gem.ruby, script_path)
1277
+ end
1278
+ $stdout.print(stdout_str) unless stdout_str.to_s.empty?
1279
+ return true if status.success?
1280
+
1281
+ last_stderr = stderr_str
1282
+ last_status = status
1283
+ sleep(release_availability_probe_interval) if attempt < attempts
1226
1284
  end
1227
- $stdout.print(stdout_str) unless stdout_str.to_s.empty?
1228
- return true if status.success?
1229
1285
 
1230
- diag = stderr_str.to_s.empty? ? "" : "\n--- STDERR ---\n#{stderr_str}".rstrip
1231
- abort("Published gem availability probe failed for #{candidate.gem_name} #{candidate.version} (exit #{status.exitstatus})#{diag}")
1286
+ diag = last_stderr.to_s.empty? ? "" : "\n--- STDERR ---\n#{last_stderr}".rstrip
1287
+ abort("Published gem availability probe failed for #{candidate.gem_name} #{candidate.version} after #{attempts} attempt(s) (exit #{last_status.exitstatus})#{diag}")
1232
1288
  ensure
1233
1289
  FileUtils.rm_f(script_path) if script_path
1234
1290
  end
1235
1291
 
1292
+ def release_availability_probe_attempts
1293
+ 15
1294
+ end
1295
+
1296
+ def release_availability_probe_initial_delay
1297
+ 5
1298
+ end
1299
+
1300
+ def release_availability_probe_interval
1301
+ 10
1302
+ end
1303
+
1236
1304
  def with_unbundled_release_probe_env(&block)
1237
1305
  if defined?(Bundler)
1238
1306
  Bundler.with_unbundled_env(&block)
@@ -1260,11 +1328,12 @@ module Kettle
1260
1328
 
1261
1329
  gemfile(true) do
1262
1330
  source #{RELEASE_VALIDATION_SOURCE.dump}
1263
- gem gem_name, "= \#{version}"
1331
+ gem gem_name, "= \#{version}", require: false
1264
1332
  end
1265
1333
 
1266
- spec = Gem.loaded_specs.fetch(gem_name)
1267
- abort("loaded \#{gem_name} \#{spec.version}, expected \#{version}") unless spec.version.to_s == version
1334
+ spec = Bundler.load.specs.find { |candidate| candidate.name == gem_name }
1335
+ abort("resolved no \#{gem_name} spec") unless spec
1336
+ abort("resolved \#{gem_name} \#{spec.version}, expected \#{version}") unless spec.version.to_s == version
1268
1337
  puts "Validated \#{gem_name} \#{version} from #{RELEASE_VALIDATION_SOURCE}"
1269
1338
  RUBY
1270
1339
  end
@@ -1369,6 +1438,118 @@ module Kettle
1369
1438
  end
1370
1439
  end
1371
1440
 
1441
+ def emit_run_start
1442
+ Kettle::Ndjson.emit_event(
1443
+ @event_recorder,
1444
+ "run_start",
1445
+ command: "release",
1446
+ root: @root,
1447
+ start_step: @start_step,
1448
+ skipped_steps: @skip_steps,
1449
+ local_ci: @local_ci,
1450
+ ci_workflows: @ci_workflows,
1451
+ skipped_remotes: @skip_remotes
1452
+ )
1453
+ end
1454
+
1455
+ def emit_command_event(command, status, reason: nil)
1456
+ event = {
1457
+ phase: "release",
1458
+ index: @command_events.length + 1,
1459
+ name: command_event_name(command),
1460
+ status: status,
1461
+ reason: reason,
1462
+ command: command,
1463
+ changed_files: []
1464
+ }
1465
+ @command_events << event unless status == "started"
1466
+ Kettle::Ndjson.emit_step_event(@event_recorder, "command_step", event, phase: "release", index: event[:index])
1467
+ end
1468
+
1469
+ def command_event_name(command)
1470
+ command.to_s.split(/\s+/, 3).first(2).join("_").gsub(/[^A-Za-z0-9_]+/, "_").sub(/_+\z/, "")
1471
+ end
1472
+
1473
+ def record_diagnostic(kind, message, severity:, blocking:)
1474
+ diagnostic = {
1475
+ kind: kind,
1476
+ severity: severity,
1477
+ message: message.to_s,
1478
+ blocking: blocking
1479
+ }
1480
+ @diagnostics << diagnostic
1481
+ Kettle::Ndjson.emit_diagnostic_event(@event_recorder, diagnostic, index: @diagnostics.length)
1482
+ end
1483
+
1484
+ def finish_release_report(status:, error:)
1485
+ return if @finished_report
1486
+
1487
+ duration_ms = @started_at ? duration_ms_since(@started_at) : nil
1488
+ @finished_report = {
1489
+ command: "release",
1490
+ root: @root,
1491
+ version: safe_detect_version,
1492
+ gem_name: safe_detect_gem_name,
1493
+ start_step: @start_step,
1494
+ skipped_steps: @skip_steps,
1495
+ local_ci: @local_ci,
1496
+ ci_workflows: @ci_workflows,
1497
+ skipped_remotes: @skip_remotes,
1498
+ status: status,
1499
+ error_class: error&.class&.name,
1500
+ error_message: error&.message,
1501
+ duration_ms: duration_ms,
1502
+ command_events: @command_events,
1503
+ diagnostics: @diagnostics,
1504
+ diagnostics_count: @diagnostics.length,
1505
+ phase_timings: @event_recorder.phase_timings
1506
+ }.compact
1507
+ Kettle::Ndjson.emit_summary_event(@event_recorder, @finished_report)
1508
+ write_release_report(@finished_report)
1509
+ @json_io.puts(JSON.pretty_generate(@finished_report)) if @json_output
1510
+ end
1511
+
1512
+ def write_release_report(report)
1513
+ return unless @report_path
1514
+
1515
+ FileUtils.mkdir_p(File.dirname(@report_path))
1516
+ File.write(@report_path, "#{JSON.pretty_generate(report)}\n")
1517
+ end
1518
+
1519
+ def with_machine_stdout_redirect
1520
+ return yield unless @json_output || @event_stream
1521
+
1522
+ previous_stdout = $stdout
1523
+ $stdout = $stderr
1524
+ yield
1525
+ ensure
1526
+ $stdout = previous_stdout if previous_stdout
1527
+ end
1528
+
1529
+ def human_output
1530
+ (@json_output || @event_stream) ? $stderr : $stdout
1531
+ end
1532
+
1533
+ def safe_detect_version
1534
+ detect_version
1535
+ rescue
1536
+ nil
1537
+ end
1538
+
1539
+ def safe_detect_gem_name
1540
+ detect_gem_name
1541
+ rescue
1542
+ nil
1543
+ end
1544
+
1545
+ def monotonic_time
1546
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
1547
+ end
1548
+
1549
+ def duration_ms_since(started_at)
1550
+ ((monotonic_time - started_at) * 1000).round(3)
1551
+ end
1552
+
1372
1553
  def push_tags!
1373
1554
  # After release, push tags to remotes according to policy:
1374
1555
  # 1) If a remote named "all" exists, push tags only to it.
@@ -1506,7 +1687,7 @@ module Kettle
1506
1687
  remotes.each do |remote|
1507
1688
  next if remote == "all"
1508
1689
 
1509
- run_cmd!("git fetch #{Shellwords.escape(remote)}")
1690
+ fetch_remote_for_parity!(remote)
1510
1691
  end
1511
1692
  missing_from = []
1512
1693
  remotes.each do |r|
@@ -1582,6 +1763,50 @@ module Kettle
1582
1763
  end
1583
1764
  end
1584
1765
 
1766
+ def fetch_remote_for_parity!(remote)
1767
+ command = "git fetch #{Shellwords.escape(remote)}"
1768
+ attempts = remote_fetch_parity_attempts
1769
+ last_error = nil
1770
+ attempts.times do |index|
1771
+ attempt = index + 1
1772
+ puts "Fetching remote '#{remote}' for release parity (attempt #{attempt}/#{attempts})"
1773
+ begin
1774
+ run_cmd!(command)
1775
+ return true
1776
+ rescue SystemExit => error
1777
+ last_error = error
1778
+ rescue => error
1779
+ last_error = error
1780
+ end
1781
+ sleep(remote_fetch_parity_interval) if attempt < attempts
1782
+ end
1783
+
1784
+ abort(remote_fetch_failure_message(remote, last_error&.message.to_s))
1785
+ end
1786
+
1787
+ def remote_fetch_parity_attempts
1788
+ 3
1789
+ end
1790
+
1791
+ def remote_fetch_parity_interval
1792
+ 5
1793
+ end
1794
+
1795
+ def remote_fetch_failure_message(remote, detail)
1796
+ <<~MSG
1797
+ Unable to fetch git remote '#{remote}' during release parity checks.
1798
+ kettle-release enforces trunk parity across active remotes before publishing.
1799
+
1800
+ If this remote is temporarily unavailable or intentionally not part of this release,
1801
+ rerun with one of:
1802
+ kettle-release --skip-remotes #{remote}
1803
+ K_RELEASE_SKIP_REMOTES=#{remote} kettle-release
1804
+
1805
+ Original failure:
1806
+ #{detail}
1807
+ MSG
1808
+ end
1809
+
1585
1810
  def merge_feature_into_trunk_and_push!(trunk, feature)
1586
1811
  return if feature.nil? || feature == trunk
1587
1812
 
@@ -3,7 +3,7 @@
3
3
  module Kettle
4
4
  module Dev
5
5
  module Version
6
- VERSION = "2.4.2"
6
+ VERSION = "2.4.4"
7
7
  end
8
8
  VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-dev
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.2
4
+ version: 2.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -71,6 +71,26 @@ dependencies:
71
71
  - - ">="
72
72
  - !ruby/object:Gem::Version
73
73
  version: 0.3.1
74
+ - !ruby/object:Gem::Dependency
75
+ name: kettle-ndjson
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '0.1'
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 0.1.1
84
+ type: :runtime
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '0.1'
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 0.1.1
74
94
  - !ruby/object:Gem::Dependency
75
95
  name: kettle-rb
76
96
  requirement: !ruby/object:Gem::Requirement
@@ -216,7 +236,7 @@ dependencies:
216
236
  version: '3.2'
217
237
  - - ">="
218
238
  - !ruby/object:Gem::Version
219
- version: 3.2.0
239
+ version: 3.2.1
220
240
  type: :development
221
241
  prerelease: false
222
242
  version_requirements: !ruby/object:Gem::Requirement
@@ -226,7 +246,7 @@ dependencies:
226
246
  version: '3.2'
227
247
  - - ">="
228
248
  - !ruby/object:Gem::Version
229
- version: 3.2.0
249
+ version: 3.2.1
230
250
  - !ruby/object:Gem::Dependency
231
251
  name: stone_checksums
232
252
  requirement: !ruby/object:Gem::Requirement
@@ -256,7 +276,7 @@ dependencies:
256
276
  version: '2.0'
257
277
  - - ">="
258
278
  - !ruby/object:Gem::Version
259
- version: 2.0.4
279
+ version: 2.0.5
260
280
  type: :development
261
281
  prerelease: false
262
282
  version_requirements: !ruby/object:Gem::Requirement
@@ -266,7 +286,7 @@ dependencies:
266
286
  version: '2.0'
267
287
  - - ">="
268
288
  - !ruby/object:Gem::Version
269
- version: 2.0.4
289
+ version: 2.0.5
270
290
  description: "\U0001F372 Kettle::Dev is a meta tool from kettle-dev to streamline
271
291
  development and testing. Acts as a shim dependency, pulling in many other dependencies,
272
292
  to give you OOTB productivity with a RubyGem, or Ruby app project. Configures a
@@ -349,10 +369,10 @@ licenses:
349
369
  - AGPL-3.0-only
350
370
  metadata:
351
371
  homepage_uri: https://kettle-dev.galtzo.com
352
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.4.2
353
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.4.2/CHANGELOG.md
372
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.4.4
373
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.4.4/CHANGELOG.md
354
374
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
355
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.4.2
375
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.4.4
356
376
  funding_uri: https://github.com/sponsors/pboling
357
377
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
358
378
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file