kettle-dev 2.5.12 → 2.5.14

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.
@@ -221,7 +221,7 @@ module Kettle
221
221
  end
222
222
 
223
223
  def empty_registry_checksums(path)
224
- path_gems = path_source_gems(path) | path_dependency_gems(path) | self_path_source_gems(path)
224
+ non_registry_gems = non_registry_source_gems(path) | path_dependency_gems(path)
225
225
  in_checksums = false
226
226
  entries = File.readlines(path).filter_map.with_index(1) do |line, index|
227
227
  stripped = line.strip
@@ -239,7 +239,7 @@ module Kettle
239
239
  name = match[1]
240
240
  checksum = match[3].to_s
241
241
  next unless checksum.empty?
242
- next if path_gems.include?(name)
242
+ next if non_registry_gems.include?(name)
243
243
 
244
244
  [name, match[2], index]
245
245
  end
@@ -282,6 +282,12 @@ module Kettle
282
282
  end
283
283
  end
284
284
 
285
+ def non_registry_source_gems(path)
286
+ lockfile_parser(path).specs.each_with_object(Set.new) do |spec, gems|
287
+ gems << spec.name unless registry_source?(spec.source)
288
+ end
289
+ end
290
+
285
291
  def path_dependency_gems(path)
286
292
  lockfile_parser(path).dependencies.each_value.each_with_object(Set.new) do |dependency, gems|
287
293
  source = dependency.source
@@ -289,12 +295,6 @@ module Kettle
289
295
  end
290
296
  end
291
297
 
292
- def self_path_source_gems(path)
293
- lockfile_parser(path).specs.each_with_object(Set.new) do |spec, gems|
294
- gems << spec.name if self_path_source?(spec.source)
295
- end
296
- end
297
-
298
298
  def display_path(path)
299
299
  Kettle::Dev.display_path(path)
300
300
  end
@@ -332,6 +332,10 @@ module Kettle
332
332
  source.instance_of?(::Bundler::Source::Path)
333
333
  end
334
334
 
335
+ def registry_source?(source)
336
+ source.instance_of?(::Bundler::Source::Rubygems)
337
+ end
338
+
335
339
  def self_path_source?(source)
336
340
  path_source?(source) && source.path.to_s == "."
337
341
  end
@@ -10,6 +10,7 @@ require "net/http"
10
10
  require "openssl"
11
11
  require "time"
12
12
  require_relative "cache_progress"
13
+ require "kettle/ndjson"
13
14
  begin
14
15
  require "addressable/uri"
15
16
  rescue LoadError
@@ -271,9 +272,10 @@ module Kettle
271
272
  end
272
273
 
273
274
  # @param check_num [Integer] 1-based index to resume from
274
- def initialize(check_num: 1)
275
+ def initialize(check_num: 1, event_recorder: nil)
275
276
  @check_num = (check_num || 1).to_i
276
277
  @check_num = 1 if @check_num < 1
278
+ @event_recorder = event_recorder
277
279
  @image_url_cache_path = configured_image_url_cache_path
278
280
  @refresh_image_url_cache = env_truthy?(ENV["KETTLE_IMAGE_URL_CACHE_REFRESH"])
279
281
  @image_url_skip_patterns = configured_image_url_skip_patterns
@@ -294,7 +296,14 @@ module Kettle
294
296
  checks[begin_idx..-1].each_with_index do |check, i|
295
297
  idx = begin_idx + i + 1
296
298
  puts "[kettle-pre-release] Running check ##{idx} of #{checks.size}"
297
- check.call
299
+ emit_pre_release_event(action: "check", status: "started", check: check_name(check), index: idx, total: checks.size)
300
+ begin
301
+ check.call
302
+ emit_pre_release_event(action: "check", status: "ok", check: check_name(check), index: idx, total: checks.size)
303
+ rescue => error
304
+ emit_pre_release_event(action: "check", status: "failed", check: check_name(check), index: idx, total: checks.size, reason: "#{error.class}: #{error.message}")
305
+ raise
306
+ end
298
307
  end
299
308
  nil
300
309
  end
@@ -355,6 +364,7 @@ module Kettle
355
364
  end
356
365
 
357
366
  puts "[kettle-pre-release] Normalization candidates: #{total_candidates}. Files changed: #{changed.uniq.size}."
367
+ emit_pre_release_event(action: "markdown_urls", status: "ok", candidates: total_candidates, changed_files: changed.uniq.size)
358
368
  nil
359
369
  end
360
370
 
@@ -408,10 +418,12 @@ module Kettle
408
418
  puts "[kettle-pre-release] Image URL checks: #{progress.cached_count} cached, #{progress.live_count} live."
409
419
  puts "[kettle-pre-release] Skipped #{progress.skipped_count} image URL check(s)." if skipped.any?
410
420
  if failures.any?
421
+ emit_pre_release_event(action: "image_links", status: "failed", urls: urls.size, cached: progress.cached_count, live: progress.live_count, skipped: progress.skipped_count, failures: failures.size)
411
422
  warn("[kettle-pre-release] #{failures.size} image URL(s) failed validation:")
412
423
  failures.each { |u| warn(" - #{u}") }
413
424
  Kettle::Dev::ExitAdapter.abort("Image link validation failed")
414
425
  else
426
+ emit_pre_release_event(action: "image_links", status: "ok", urls: urls.size, cached: progress.cached_count, live: progress.live_count, skipped: progress.skipped_count, failures: 0)
415
427
  puts "[kettle-pre-release] All image links validated."
416
428
  end
417
429
  nil
@@ -419,6 +431,33 @@ module Kettle
419
431
 
420
432
  private
421
433
 
434
+ def check_name(check)
435
+ check.name.to_s.sub(/\Acheck_/, "").sub(/!\z/, "")
436
+ end
437
+
438
+ def emit_pre_release_event(action:, status:, **payload)
439
+ mark = case status.to_s
440
+ when "started"
441
+ ">"
442
+ when "ok", "skipped"
443
+ "."
444
+ when "failed"
445
+ "!"
446
+ else
447
+ "?"
448
+ end
449
+ Kettle::Ndjson.emit_event(
450
+ @event_recorder,
451
+ "pre_release",
452
+ payload.merge(
453
+ action: action,
454
+ phase: "release",
455
+ status: status,
456
+ mark: mark
457
+ )
458
+ )
459
+ end
460
+
422
461
  def image_url_cache
423
462
  return nil if @image_url_cache_path.to_s.empty?
424
463
 
@@ -117,14 +117,15 @@ module Kettle
117
117
 
118
118
  public
119
119
 
120
- def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil, skip_bundle_audit: nil, ci_workflows: nil, skip_remotes: nil, secrets_provider_name: nil, yes: false, **options)
120
+ def initialize(start_step: 0, local_ci: false, version: nil, appraisal_task: nil, skip_steps: nil, skip_bundle_audit: nil, ci_workflows: nil, skip_remotes: nil, required_remotes: nil, secrets_provider_name: nil, yes: false, **options)
121
121
  @root = Kettle::Dev::CIHelpers.project_root
122
122
  @git = Kettle::Dev::GitAdapter.new(@root)
123
123
  @start_step = (start_step || 0).to_i
124
124
  @start_step = 0 if @start_step < 0
125
125
  @skip_steps = normalize_skip_steps(skip_steps)
126
126
  @ci_workflows = normalize_ci_workflows(ci_workflows || ENV["K_RELEASE_CI_WORKFLOWS"])
127
- @skip_remotes = normalize_skip_remotes(skip_remotes || ENV["K_RELEASE_SKIP_REMOTES"])
127
+ @skip_remotes = normalize_remote_names(skip_remotes || ENV["K_RELEASE_SKIP_REMOTES"], "skip remotes")
128
+ @required_remotes = normalize_required_remotes(required_remotes)
128
129
  @local_ci = !!local_ci
129
130
  @skip_bundle_audit = truthy_value?(skip_bundle_audit) || truthy_value?(ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"])
130
131
  @yes = !!yes
@@ -479,14 +480,19 @@ module Kettle
479
480
  workflows.map { |workflow| workflow.match?(/\.ya?ml\z/) ? workflow : "#{workflow}.yml" }.uniq
480
481
  end
481
482
 
482
- def normalize_skip_remotes(value)
483
+ def normalize_remote_names(value, label)
483
484
  remotes = Array(value).flat_map { |part| part.to_s.split(",") }.map(&:strip).reject(&:empty?)
484
485
  invalid = remotes.find { |remote| !remote.match?(/\A[A-Za-z0-9_.-]+\z/) }
485
- abort("Invalid skip remotes value #{invalid.inspect}; use comma-separated git remote names.") if invalid
486
+ abort("Invalid #{label} value #{invalid.inspect}; use comma-separated git remote names.") if invalid
486
487
 
487
488
  remotes.uniq
488
489
  end
489
490
 
491
+ def normalize_required_remotes(value)
492
+ remotes = normalize_remote_names(value.nil? ? ENV["K_RELEASE_REQUIRED_REMOTES"] : value, "required remotes")
493
+ remotes.empty? ? ["origin"] : remotes
494
+ end
495
+
490
496
  private
491
497
 
492
498
  def local_ci?
@@ -571,7 +577,7 @@ module Kettle
571
577
 
572
578
  def run_pre_release_checks!
573
579
  puts "Running pre-release checks via kettle-pre-release..."
574
- Kettle::Dev::PreReleaseCLI.new(check_num: 1).run
580
+ Kettle::Dev::PreReleaseCLI.new(check_num: 1, event_recorder: @event_recorder).run
575
581
  run_changelog!
576
582
  end
577
583
 
@@ -590,9 +596,11 @@ module Kettle
590
596
  attempts = release_lockfile_reset_attempts(stage)
591
597
  attempts.times do |index|
592
598
  attempt = index + 1
599
+ emit_release_lockfile_event(action: "reset", status: "started", stage: stage, attempt: attempt, attempts: attempts)
593
600
  puts "Resetting release lockfiles with local path dependencies disabled #{stage} (attempt #{attempt}/#{attempts})..."
594
601
  begin
595
602
  lockfile_reset.reset(Kettle::Dev::LockfileReset::RELEASE_LOCKFILES_TARGET)
603
+ emit_release_lockfile_event(action: "reset", status: "ok", stage: stage, attempt: attempt, attempts: attempts)
596
604
  break
597
605
  rescue Kettle::Dev::Error => error
598
606
  raise unless error.message.start_with?("Reset #{Kettle::Dev::LockfileReset::RELEASE_LOCKFILES_TARGET} failed validation:")
@@ -600,9 +608,26 @@ module Kettle
600
608
  # Keep release-facing failures shaped like the lockfile validation
601
609
  # guard, even when the shared reset helper is the component that
602
610
  # detects the unrepaired lockfile.
611
+ emit_release_lockfile_event(
612
+ action: "reset",
613
+ status: "blocked",
614
+ stage: stage,
615
+ attempt: attempt,
616
+ attempts: attempts,
617
+ reason: error.message
618
+ )
603
619
  break
604
620
  rescue => error
605
- raise unless retryable_release_lockfile_reset_error?(error) && attempt < attempts
621
+ retryable = retryable_release_lockfile_reset_error?(error) && attempt < attempts
622
+ emit_release_lockfile_event(
623
+ action: "reset",
624
+ status: retryable ? "retrying" : "failed",
625
+ stage: stage,
626
+ attempt: attempt,
627
+ attempts: attempts,
628
+ reason: error.message
629
+ )
630
+ raise unless retryable
606
631
 
607
632
  puts "Release lockfile reset could not resolve a gem from #{RELEASE_VALIDATION_SOURCE}; waiting before retry #{attempt + 1}/#{attempts}."
608
633
  sleep(release_availability_probe_interval)
@@ -611,7 +636,16 @@ module Kettle
611
636
 
612
637
  diagnostics = release_lockfile_paths.flat_map { |path| release_lockfile_diagnostics(path) }
613
638
  if diagnostics.empty?
639
+ emit_release_lockfile_event(action: "validate", status: "ok", stage: stage, count: release_lockfile_paths.length)
614
640
  puts "Release lockfile reset complete: #{release_lockfile_paths.length} lockfile(s) checked, no diagnostics remain."
641
+ else
642
+ emit_release_lockfile_event(
643
+ action: "validate",
644
+ status: "failed",
645
+ stage: stage,
646
+ count: diagnostics.length,
647
+ reason: "#{diagnostics.length} diagnostic(s)"
648
+ )
615
649
  end
616
650
  @release_lockfiles_reset_for_release_tasks = true if stage == "before release task bundle installs"
617
651
  end
@@ -758,6 +792,7 @@ module Kettle
758
792
  cmd = "bundle exec kettle-changelog"
759
793
  cmd = "#{cmd} --version #{Shellwords.escape(@version_override)}" if @version_override
760
794
  cmd = "#{cmd} --yes" if @yes
795
+ cmd = "#{cmd} --events=changelog" if @event_stream
761
796
  run_cmd!(cmd)
762
797
  @changelog_generated_coverage = true
763
798
  end
@@ -1010,12 +1045,24 @@ module Kettle
1010
1045
  def monitor_workflows_after_push!
1011
1046
  ensure_github_pull_request_for_ci!
1012
1047
  keep_release_secrets_alive!("CI monitoring")
1048
+ restart_hint = "bundle exec kettle-release start_step=10"
1049
+ emit_ci_monitor_event(action: "start", status: "started", workflows: @ci_workflows, restart_hint: restart_hint)
1013
1050
  # Use abort-on-failure CI monitor to match historical behavior and specs
1014
- Kettle::Dev::CIMonitor.monitor_all!(
1015
- restart_hint: "bundle exec kettle-release start_step=10",
1016
- workflows: @ci_workflows,
1017
- keepalive: release_secrets_configured? ? -> { keep_release_secrets_alive!("CI monitoring") } : nil
1018
- )
1051
+ begin
1052
+ Kettle::Dev::CIMonitor.monitor_all!(
1053
+ restart_hint: restart_hint,
1054
+ workflows: @ci_workflows,
1055
+ keepalive: release_secrets_configured? ? -> { keep_release_secrets_alive!("CI monitoring") } : nil,
1056
+ event_recorder: @event_recorder
1057
+ )
1058
+ emit_ci_monitor_event(action: "finish", status: "ok", workflows: @ci_workflows, restart_hint: restart_hint)
1059
+ rescue SystemExit => error
1060
+ emit_ci_monitor_event(action: "finish", status: "failed", workflows: @ci_workflows, restart_hint: restart_hint, reason: error.message)
1061
+ raise
1062
+ rescue => error
1063
+ emit_ci_monitor_event(action: "finish", status: "failed", workflows: @ci_workflows, restart_hint: restart_hint, reason: "#{error.class}: #{error.message}")
1064
+ raise
1065
+ end
1019
1066
  end
1020
1067
 
1021
1068
  def ensure_github_pull_request_for_ci!
@@ -1412,6 +1459,7 @@ module Kettle
1412
1459
  sleep(release_availability_probe_initial_delay)
1413
1460
  attempts.times do |index|
1414
1461
  attempt = index + 1
1462
+ emit_release_probe_event(action: "availability", status: "started", candidate: candidate, attempt: attempt, attempts: attempts)
1415
1463
  puts("Validating #{candidate.gem_name} #{candidate.version} from #{RELEASE_VALIDATION_SOURCE} (attempt #{attempt}/#{attempts})")
1416
1464
  stdout_str = nil
1417
1465
  stderr_str = nil
@@ -1420,10 +1468,21 @@ module Kettle
1420
1468
  stdout_str, stderr_str, status = Open3.capture3(self.class.send(:command_env), Gem.ruby, script_path)
1421
1469
  end
1422
1470
  $stdout.print(stdout_str) unless stdout_str.to_s.empty?
1423
- return true if status.success?
1471
+ if status.success?
1472
+ emit_release_probe_event(action: "availability", status: "ok", candidate: candidate, attempt: attempt, attempts: attempts)
1473
+ return true
1474
+ end
1424
1475
 
1425
1476
  last_stderr = stderr_str
1426
1477
  last_status = status
1478
+ emit_release_probe_event(
1479
+ action: "availability",
1480
+ status: (attempt < attempts) ? "retrying" : "failed",
1481
+ candidate: candidate,
1482
+ attempt: attempt,
1483
+ attempts: attempts,
1484
+ reason: "exit #{status.exitstatus}"
1485
+ )
1427
1486
  sleep(release_availability_probe_interval) if attempt < attempts
1428
1487
  end
1429
1488
 
@@ -1590,6 +1649,7 @@ module Kettle
1590
1649
  name: command_event_name(command),
1591
1650
  status: status,
1592
1651
  reason: reason,
1652
+ summary: command_event_summary(command),
1593
1653
  command: command,
1594
1654
  changed_files: []
1595
1655
  }
@@ -1598,7 +1658,80 @@ module Kettle
1598
1658
  end
1599
1659
 
1600
1660
  def command_event_name(command)
1601
- command.to_s.split(/\s+/, 3).first(2).join("_").gsub(/[^A-Za-z0-9_]+/, "_").sub(/_+\z/, "")
1661
+ words = Shellwords.split(command.to_s)
1662
+ effective_words = command_event_effective_words(words)
1663
+ joined = effective_words.join(" ")
1664
+
1665
+ return "kettle_changelog" if joined.start_with?("bundle exec kettle-changelog")
1666
+ return "gem_build" if joined == "bundle exec rake build"
1667
+ return "gem_release" if joined == "bundle exec rake release"
1668
+ return "gem_push" if effective_words.first(2) == ["gem", "push"]
1669
+ return "gem_checksums" if effective_words.first == "bin/gem_checksums"
1670
+ return "bundle_lock" if effective_words.first(2) == ["bundle", "lock"]
1671
+ return "bin_setup" if effective_words == ["bin/setup"]
1672
+ return "default_task" if effective_words == ["bin/rake"]
1673
+ return "appraisal_generate" if effective_words == ["bin/rake", "appraisal:generate"]
1674
+ return "appraisal_update" if effective_words == ["bin/rake", "appraisal:update"]
1675
+ return "yard" if effective_words == ["bin/rake", "yard"]
1676
+ return "git_fetch" if effective_words.first(2) == ["git", "fetch"]
1677
+ return "git_pull" if effective_words.first(2) == ["git", "pull"]
1678
+ return "git_rebase" if effective_words.first(2) == ["git", "rebase"]
1679
+ return "git_merge" if effective_words.first(2) == ["git", "merge"]
1680
+ return "git_push" if effective_words.first(2) == ["git", "push"]
1681
+
1682
+ effective_words.first(2).join("_").gsub(/[^A-Za-z0-9_]+/, "_").sub(/_+\z/, "")
1683
+ end
1684
+
1685
+ def command_event_effective_words(words)
1686
+ words = words.dup
1687
+ words.shift while words.first&.match?(/\A[A-Za-z_][A-Za-z0-9_]*=/)
1688
+
1689
+ if words.first == "env"
1690
+ words.shift
1691
+ while words.any?
1692
+ if words.first == "-u"
1693
+ words.shift(2)
1694
+ elsif words.first&.include?("=")
1695
+ words.shift
1696
+ else
1697
+ break
1698
+ end
1699
+ end
1700
+ end
1701
+
1702
+ words
1703
+ end
1704
+
1705
+ def command_event_summary(command)
1706
+ words = Shellwords.split(command.to_s)
1707
+ effective_words = command_event_effective_words(words)
1708
+ joined = effective_words.join(" ")
1709
+
1710
+ return "changelog" if joined.start_with?("bundle exec kettle-changelog")
1711
+ return command_event_bundle_lock_summary(words) if effective_words.first(2) == ["bundle", "lock"]
1712
+ return "setup" if effective_words == ["bin/setup"]
1713
+ return "default task" if effective_words == ["bin/rake"]
1714
+ return "appraisals" if effective_words == ["bin/rake", "appraisal:generate"]
1715
+ return "appraisals" if effective_words == ["bin/rake", "appraisal:update"]
1716
+ return "documentation" if effective_words == ["bin/rake", "yard"]
1717
+ return "build gem" if joined == "bundle exec rake build"
1718
+ return "publish gem" if joined == "bundle exec rake release"
1719
+ return "push gem" if effective_words.first(2) == ["gem", "push"]
1720
+ return "checksums" if effective_words.first == "bin/gem_checksums"
1721
+ return effective_words[2] if effective_words.first(2) == ["git", "fetch"] && effective_words[2]
1722
+ return effective_words[2] if effective_words.first(2) == ["git", "push"] && effective_words[2]
1723
+ return effective_words[2] if effective_words.first(2) == ["git", "pull"] && effective_words[2]
1724
+ return effective_words[2] if effective_words.first(2) == ["git", "rebase"] && effective_words[2]
1725
+ return effective_words[2] if effective_words.first(2) == ["git", "merge"] && effective_words[2]
1726
+
1727
+ nil
1728
+ end
1729
+
1730
+ def command_event_bundle_lock_summary(words)
1731
+ gemfile = words.find { |word| word.start_with?("BUNDLE_GEMFILE=") }&.split("=", 2)&.last
1732
+ return "lockfile" unless gemfile
1733
+
1734
+ File.basename(gemfile)
1602
1735
  end
1603
1736
 
1604
1737
  def record_diagnostic(kind, message, severity:, blocking:)
@@ -1635,6 +1768,97 @@ module Kettle
1635
1768
  )
1636
1769
  end
1637
1770
 
1771
+ def emit_remote_parity_event(action:, remote: nil, status: nil, trunk: nil, attempt: nil, attempts: nil, required: nil, reason: nil, remotes: nil)
1772
+ mark = release_progress_mark(status)
1773
+ Kettle::Ndjson.emit_event(
1774
+ @event_recorder,
1775
+ "remote_parity",
1776
+ action: action,
1777
+ phase: "release",
1778
+ remote: remote,
1779
+ status: status,
1780
+ trunk: trunk,
1781
+ attempt: attempt,
1782
+ attempts: attempts,
1783
+ required: required,
1784
+ reason: reason,
1785
+ remotes: remotes,
1786
+ mark: mark
1787
+ )
1788
+ end
1789
+
1790
+ def emit_release_lockfile_event(action:, status:, stage:, attempt: nil, attempts: nil, count: nil, reason: nil)
1791
+ Kettle::Ndjson.emit_event(
1792
+ @event_recorder,
1793
+ "release_lockfile",
1794
+ action: action,
1795
+ phase: "release",
1796
+ status: status,
1797
+ stage: stage,
1798
+ attempt: attempt,
1799
+ attempts: attempts,
1800
+ count: count,
1801
+ reason: reason,
1802
+ mark: release_progress_mark(status)
1803
+ )
1804
+ end
1805
+
1806
+ def emit_release_probe_event(action:, status:, candidate:, attempt:, attempts:, reason: nil)
1807
+ Kettle::Ndjson.emit_event(
1808
+ @event_recorder,
1809
+ "release_probe",
1810
+ action: action,
1811
+ phase: "release",
1812
+ status: status,
1813
+ gem: candidate.gem_name,
1814
+ version: candidate.version,
1815
+ source: RELEASE_VALIDATION_SOURCE,
1816
+ attempt: attempt,
1817
+ attempts: attempts,
1818
+ reason: reason,
1819
+ mark: release_progress_mark(status)
1820
+ )
1821
+ end
1822
+
1823
+ def release_progress_mark(status)
1824
+ case status.to_s
1825
+ when "started"
1826
+ ">"
1827
+ when "ok", "skipped"
1828
+ "."
1829
+ when "failed", "blocked"
1830
+ "!"
1831
+ when "retrying"
1832
+ ">"
1833
+ else
1834
+ ">"
1835
+ end
1836
+ end
1837
+
1838
+ def emit_ci_monitor_event(action:, status:, workflows:, restart_hint:, reason: nil)
1839
+ mark = case status.to_s
1840
+ when "started"
1841
+ ">"
1842
+ when "ok"
1843
+ "."
1844
+ when "failed"
1845
+ "!"
1846
+ else
1847
+ "?"
1848
+ end
1849
+ Kettle::Ndjson.emit_event(
1850
+ @event_recorder,
1851
+ "ci_monitor",
1852
+ action: action,
1853
+ phase: "release",
1854
+ status: status,
1855
+ workflows: workflows,
1856
+ restart_hint: restart_hint,
1857
+ reason: reason,
1858
+ mark: mark
1859
+ )
1860
+ end
1861
+
1638
1862
  def finish_release_report(status:, error:)
1639
1863
  return if @finished_report
1640
1864
 
@@ -1798,6 +2022,10 @@ module Kettle
1798
2022
  @skip_remotes.include?(name)
1799
2023
  end
1800
2024
 
2025
+ def required_remote?(name)
2026
+ @required_remotes.include?(name)
2027
+ end
2028
+
1801
2029
  def active_remotes
1802
2030
  list_remotes.reject { |remote| skipped_remote?(remote) }
1803
2031
  end
@@ -1836,26 +2064,30 @@ module Kettle
1836
2064
  if has_remote?("all")
1837
2065
  remotes = active_remotes
1838
2066
  skipped = list_remotes.select { |remote| skipped_remote?(remote) }
2067
+ required = remotes.select { |remote| required_remote?(remote) }
1839
2068
  puts "Remote 'all' detected. Fetching from active remotes and enforcing strict trunk parity..."
1840
2069
  puts "Skipping configured remotes: #{skipped.join(", ")}" unless skipped.empty?
2070
+ puts "Required release parity remotes: #{required.join(", ")}" unless required.empty?
2071
+ fetched_remotes = []
2072
+ emit_remote_parity_event(action: "start", status: "started", trunk: trunk, remotes: remotes - ["all"])
1841
2073
  remotes.each do |remote|
1842
2074
  next if remote == "all"
1843
2075
 
1844
- fetch_remote_for_parity!(remote)
2076
+ fetched_remotes << remote if fetch_remote_for_parity!(remote)
1845
2077
  end
1846
2078
  missing_from = []
1847
- remotes.each do |r|
1848
- next if r == "all"
1849
-
2079
+ fetched_remotes.each do |r|
1850
2080
  if remote_branch_exists?(r, trunk)
1851
2081
  _ahead, behind = ahead_behind_counts(trunk, "#{r}/#{trunk}")
1852
2082
  missing_from << r if behind.positive?
1853
2083
  end
1854
2084
  end
1855
2085
  unless missing_from.empty?
2086
+ emit_remote_parity_event(action: "missing", status: "failed", trunk: trunk, remotes: missing_from, reason: "missing commits")
1856
2087
  abort("Local #{trunk} is missing commits present on: #{missing_from.join(", ")}. Please sync trunk first.")
1857
2088
  end
1858
- puts "Local #{trunk} has all commits from remotes: #{(remotes - ["all"]).join(", ")}"
2089
+ puts "Local #{trunk} has all commits from remotes: #{fetched_remotes.join(", ")}"
2090
+ emit_remote_parity_event(action: "ok", status: "ok", trunk: trunk, remotes: fetched_remotes)
1859
2091
  return
1860
2092
  end
1861
2093
 
@@ -1924,8 +2156,10 @@ module Kettle
1924
2156
  attempts.times do |index|
1925
2157
  attempt = index + 1
1926
2158
  puts "Fetching remote '#{remote}' for release parity (attempt #{attempt}/#{attempts})"
2159
+ emit_remote_parity_event(action: "fetch", remote: remote, status: "started", attempt: attempt, attempts: attempts, required: required_remote?(remote))
1927
2160
  begin
1928
2161
  run_cmd!(command)
2162
+ emit_remote_parity_event(action: "fetch", remote: remote, status: "ok", attempt: attempt, attempts: attempts, required: required_remote?(remote))
1929
2163
  return true
1930
2164
  rescue SystemExit => error
1931
2165
  last_error = error
@@ -1935,7 +2169,17 @@ module Kettle
1935
2169
  sleep(remote_fetch_parity_interval) if attempt < attempts
1936
2170
  end
1937
2171
 
1938
- abort(remote_fetch_failure_message(remote, last_error&.message.to_s))
2172
+ detail = last_error&.message.to_s
2173
+ if required_remote?(remote)
2174
+ emit_remote_parity_event(action: "fetch", remote: remote, status: "failed", attempts: attempts, required: true, reason: detail)
2175
+ abort(remote_fetch_failure_message(remote, detail))
2176
+ end
2177
+
2178
+ message = optional_remote_fetch_failure_message(remote, detail)
2179
+ warn(message)
2180
+ emit_remote_parity_event(action: "skip", remote: remote, status: "skipped", attempts: attempts, required: false, reason: detail)
2181
+ record_diagnostic("release_remote_skipped", message, severity: "warning", blocking: false)
2182
+ false
1939
2183
  end
1940
2184
 
1941
2185
  def remote_fetch_parity_attempts
@@ -1948,7 +2192,7 @@ module Kettle
1948
2192
 
1949
2193
  def remote_fetch_failure_message(remote, detail)
1950
2194
  <<~MSG
1951
- Unable to fetch git remote '#{remote}' during release parity checks.
2195
+ Unable to fetch required git remote '#{remote}' during release parity checks.
1952
2196
  kettle-release enforces trunk parity across active remotes before publishing.
1953
2197
 
1954
2198
  If this remote is temporarily unavailable or intentionally not part of this release,
@@ -1961,6 +2205,20 @@ module Kettle
1961
2205
  MSG
1962
2206
  end
1963
2207
 
2208
+ def optional_remote_fetch_failure_message(remote, detail)
2209
+ <<~MSG
2210
+ Unable to fetch optional git remote '#{remote}' during release parity checks; skipping this remote for this release.
2211
+ Required remotes still block release parity checks.
2212
+
2213
+ To make this remote block releases, configure:
2214
+ kettle-release --required-remotes #{remote}
2215
+ K_RELEASE_REQUIRED_REMOTES=#{remote} kettle-release
2216
+
2217
+ Original failure:
2218
+ #{detail}
2219
+ MSG
2220
+ end
2221
+
1964
2222
  def merge_feature_into_trunk_and_push!(trunk, feature)
1965
2223
  return if feature.nil? || feature == trunk
1966
2224
 
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "2.5.12"
8
+ VERSION = "2.5.14"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data.tar.gz.sig CHANGED
@@ -1,3 +1,3 @@
1
- w2��YSs��E�xx���阻Qc��X���_�%�%X.��C⹎���f�,~�d
2
- O�OE�_m�Ф�P���s �!j���=�4� ��q�{��0Ӕ�Ʈ}���:��� ܪ}ȿ����7yO?�����q׊y��z�';�%��j5ˠ�����%F��}��*���0fVk2Cp{
3
- ��#H�٩��� 嫕�d KgHV� ��'Ꮍ����.�!�s�U=���� @�����|pq����&P���Dqd߹� ?�3j2�s�}�� $�ҳ�?R|�2���T
1
+ ��''Qˁ ��(�NO��3��m������Ę��>-�U?40��ə����Qe�K��* �/#�Fc�+�MkI'��4��`rj4�V���j�)� �
2
+ A48~,�\���k���Bz1�@���D \=�N/�G�?�/��U��o�'L��srbȻ�����V�е�t1K1��O,'�}O��d!�ȹ�u$8m�`ݩːS"c��47#H^u�żx�����-�R@b!��[�����������;$
3
+ ��_��t���֮�W��'/��h����
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.5.12
4
+ version: 2.5.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -80,7 +80,7 @@ dependencies:
80
80
  version: '0.1'
81
81
  - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: 0.1.4
83
+ version: 0.1.9
84
84
  type: :runtime
85
85
  prerelease: false
86
86
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  version: '0.1'
91
91
  - - ">="
92
92
  - !ruby/object:Gem::Version
93
- version: 0.1.4
93
+ version: 0.1.9
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: kettle-rb
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -401,10 +401,10 @@ licenses:
401
401
  - AGPL-3.0-only
402
402
  metadata:
403
403
  homepage_uri: https://kettle-dev.galtzo.com
404
- source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.5.12
405
- changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.5.12/CHANGELOG.md
404
+ source_code_uri: https://github.com/kettle-dev/kettle-dev/tree/v2.5.14
405
+ changelog_uri: https://github.com/kettle-dev/kettle-dev/blob/v2.5.14/CHANGELOG.md
406
406
  bug_tracker_uri: https://github.com/kettle-dev/kettle-dev/issues
407
- documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.5.12
407
+ documentation_uri: https://www.rubydoc.info/gems/kettle-dev/2.5.14
408
408
  funding_uri: https://github.com/sponsors/pboling
409
409
  wiki_uri: https://github.com/kettle-dev/kettle-dev/wiki
410
410
  news_uri: https://www.railsbling.com/tags/kettle-dev
metadata.gz.sig CHANGED
Binary file