kettle-dev 2.4.5 → 2.5.0

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.
@@ -17,6 +17,10 @@ require "kettle/ndjson"
17
17
  require "kettle/rb/compat_matrix"
18
18
  require "ruby-progressbar"
19
19
 
20
+ require_relative "interactive_release_command"
21
+ require_relative "lockfile_reset"
22
+ require_relative "release_secrets"
23
+
20
24
  module Kettle
21
25
  module Dev
22
26
  class ReleaseCLI
@@ -112,9 +116,9 @@ module Kettle
112
116
 
113
117
  public
114
118
 
115
- 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, **options)
119
+ 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)
116
120
  @root = Kettle::Dev::CIHelpers.project_root
117
- @git = Kettle::Dev::GitAdapter.new
121
+ @git = Kettle::Dev::GitAdapter.new(@root)
118
122
  @start_step = (start_step || 0).to_i
119
123
  @start_step = 0 if @start_step < 0
120
124
  @skip_steps = normalize_skip_steps(skip_steps)
@@ -122,11 +126,13 @@ module Kettle
122
126
  @skip_remotes = normalize_skip_remotes(skip_remotes || ENV["K_RELEASE_SKIP_REMOTES"])
123
127
  @local_ci = !!local_ci
124
128
  @skip_bundle_audit = truthy_value?(skip_bundle_audit) || truthy_value?(ENV["KETTLE_DEV_SKIP_BUNDLE_AUDIT"])
129
+ @yes = !!yes
125
130
  @version_override = Kettle::Dev::Versioning.normalize_explicit_version(version)
126
131
  @appraisal_task = normalize_appraisal_task(appraisal_task || ENV["KETTLE_RELEASE_APPRAISAL_TASK"])
127
132
  @release_candidate = nil
128
133
  @event_stream = options[:event_stream]
129
134
  @event_recorder = Kettle::Ndjson.event_recorder(@event_stream, phase_timings: [])
135
+ @secrets_provider = options[:secrets_provider] || Kettle::Dev::ReleaseSecrets::Factory.build(provider_name: secrets_provider_name)
130
136
  @report_path = options[:report_path]
131
137
  @json_output = !!options[:json_output]
132
138
  @json_io = options[:json_io] || $stdout
@@ -134,6 +140,7 @@ module Kettle
134
140
  @diagnostics = []
135
141
  @started_at = nil
136
142
  @finished_report = nil
143
+ @changelog_generated_coverage = false
137
144
  end
138
145
 
139
146
  def run
@@ -185,9 +192,9 @@ module Kettle
185
192
  gem_name = detect_gem_name
186
193
  latest_overall, latest_for_series = latest_released_versions(gem_name, version)
187
194
  rescue => e
188
- warn("[kettle-release] gem.coop release check failed: #{e.class}: #{e.message}")
195
+ warn("[kettle-release] RubyGems.org release check failed: #{e.class}: #{e.message}")
189
196
  warn(e.backtrace.first(3).map { |l| " " + l }.join("\n")) if ENV["KETTLE_DEV_DEBUG"]
190
- warn("Proceeding without gem.coop latest version info.")
197
+ warn("Proceeding without RubyGems.org latest version info.")
191
198
  end
192
199
 
193
200
  if latest_overall
@@ -209,7 +216,7 @@ module Kettle
209
216
  latest_for_series = nil unless lfs_series == cur_series
210
217
  end
211
218
  # Determine the sanity-check target correctly for the current series.
212
- # If gem.coop has a newer overall series than our current series, only compare
219
+ # If RubyGems.org has a newer overall series than our current series, only compare
213
220
  # against the latest published in our current series. If that cannot be determined
214
221
  # (e.g., offline), skip the sanity check rather than treating the overall as target.
215
222
  target = if (cur_series <=> overall_series) == -1
@@ -218,10 +225,10 @@ module Kettle
218
225
  latest_overall
219
226
  end
220
227
  # IMPORTANT: Never treat a higher different-series "latest_overall" as a downgrade target.
221
- # If our current series is behind overall and gem.coop does not report a latest_for_series,
228
+ # If our current series is behind overall and RubyGems.org does not report a latest_for_series,
222
229
  # then we cannot determine the correct target for this series and should skip the check.
223
230
  if (cur_series <=> overall_series) == -1 && target.nil?
224
- puts "Could not determine latest released version from gem.coop (offline?). Proceeding without sanity check."
231
+ puts "Could not determine latest released version from RubyGems.org (offline?). Proceeding without sanity check."
225
232
  elsif target
226
233
  bump = Kettle::Dev::Versioning.classify_bump(target, version)
227
234
  case bump
@@ -238,16 +245,13 @@ module Kettle
238
245
  puts "Proposed bump type: #{label} (from #{target} -> #{version})"
239
246
  end
240
247
  else
241
- puts "Could not determine latest released version from gem.coop (offline?). Proceeding without sanity check."
248
+ puts "Could not determine latest released version from RubyGems.org (offline?). Proceeding without sanity check."
242
249
  end
243
250
  else
244
- puts "Could not determine latest released version from gem.coop (offline?). Proceeding without sanity check."
251
+ puts "Could not determine latest released version from RubyGems.org (offline?). Proceeding without sanity check."
245
252
  end
246
253
 
247
- puts "Have you updated lib/**/version.rb and CHANGELOG.md for v#{version}? [y/N]"
248
- print("> ")
249
- ans = Kettle::Dev::InputAdapter.gets&.strip
250
- abort("Aborted: please update version.rb and CHANGELOG.md, then re-run.") unless ans&.downcase&.start_with?("y")
254
+ confirm_yes!("Have you updated lib/**/version.rb and CHANGELOG.md for v#{version}? [y/N]", "> ", "Aborted: please update version.rb and CHANGELOG.md, then re-run.")
251
255
 
252
256
  # Initial validation: Ensure README.md and LICENSE.txt have identical sets of copyright years; also ensure current year present when matched
253
257
  validate_copyright_years!
@@ -272,7 +276,7 @@ module Kettle
272
276
  # 3. bin/setup
273
277
  run_cmd!("bin/setup") if run_step?(3)
274
278
  # 4. bin/rake
275
- run_cmd!("bin/rake") if run_step?(4)
279
+ run_cmd!(release_default_task_command) if run_step?(4)
276
280
 
277
281
  # 5. appraisal:generate (optional) + canonical docs build
278
282
  if run_step?(5)
@@ -352,9 +356,11 @@ module Kettle
352
356
 
353
357
  # 13. signing guidance and checks
354
358
  if run_step?(13)
355
- if ENV.fetch("SKIP_GEM_SIGNING", "false").casecmp("false").zero?
359
+ if signing_enabled?
356
360
  puts "TIP: For local dry-runs or testing the release workflow, set SKIP_GEM_SIGNING=true to avoid PEM password prompts."
357
- if Kettle::Dev::InputAdapter.tty?
361
+ if @yes
362
+ puts "Proceeding with signing enabled because --yes was provided."
363
+ elsif Kettle::Dev::InputAdapter.tty?
358
364
  # In CI, avoid interactive prompts when no TTY is present (e.g., act or GitHub Actions "CI validation").
359
365
  # Non-interactive CI runs should not abort here; later signing checks are either stubbed in tests
360
366
  # or will be handled explicitly by ensure_signing_setup_or_skip!.
@@ -369,11 +375,17 @@ module Kettle
369
375
  end
370
376
 
371
377
  ensure_signing_setup_or_skip!
378
+ ensure_release_secrets_ready_for_signing! if signing_enabled? && release_secrets_configured?
372
379
  end
373
380
 
374
381
  # 14. build
375
382
  if run_step?(14)
376
- puts "Running build (you may be prompted for the signing key password)..."
383
+ ensure_release_secrets_ready_for_signing! if signing_enabled? && release_secrets_configured?
384
+ if signing_enabled? && release_secrets_configured?
385
+ puts "Running build with gem signing passphrase from configured secrets provider (#{release_secrets_provider_label})..."
386
+ else
387
+ puts "Running build (you may be prompted for the signing key password)..."
388
+ end
377
389
  run_cmd!("bundle exec rake build")
378
390
  end
379
391
 
@@ -385,13 +397,18 @@ module Kettle
385
397
  if local_ci?
386
398
  with_unpublished_candidate_cleanup { release_gem_and_tag_locally!(version) }
387
399
  else
388
- puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
400
+ ensure_release_secrets_ready_for_signing! if signing_enabled? && release_secrets_configured?
401
+ if release_secrets_configured?
402
+ puts "Running release with configured secrets provider (#{release_secrets_provider_label}) for signing and RubyGems MFA prompts..."
403
+ else
404
+ puts "Running release (you may be prompted for signing key password and RubyGems MFA OTP)..."
405
+ end
389
406
  with_unpublished_candidate_cleanup do
390
407
  run_cmd!("bundle exec rake release")
391
408
  @release_candidate.published = true
392
409
  confirm_release_candidate_available!(@release_candidate)
393
410
  end
394
- mark_gem_coop_release_cache_bust(version)
411
+ mark_rubygems_release_cache_bust(version)
395
412
  end
396
413
  end
397
414
 
@@ -553,18 +570,31 @@ module Kettle
553
570
  end
554
571
 
555
572
  def prepare_release_lockfiles_for_commit!
556
- dirty_lockfiles = release_lockfile_paths.select { |path| release_lockfile_normalization_needed?(path) }
557
- unless dirty_lockfiles.empty?
558
- puts "Normalizing release lockfiles with local path dependencies disabled..."
559
- dirty_lockfiles.each { |path| normalize_release_lockfile!(path) }
573
+ puts "Resetting release lockfiles with local path dependencies disabled..."
574
+ begin
575
+ lockfile_reset.reset(Kettle::Dev::LockfileReset::RELEASE_LOCKFILES_TARGET)
576
+ rescue Kettle::Dev::Error => error
577
+ raise unless error.message.start_with?("Reset #{Kettle::Dev::LockfileReset::RELEASE_LOCKFILES_TARGET} failed validation:")
578
+
579
+ # Keep release-facing failures shaped like the lockfile validation
580
+ # guard, even when the shared reset helper is the component that
581
+ # detects the unrepaired lockfile.
560
582
  end
561
583
 
584
+ diagnostics = release_lockfile_paths.flat_map { |path| release_lockfile_diagnostics(path) }
585
+ if diagnostics.empty?
586
+ puts "Release lockfile reset complete: #{release_lockfile_paths.length} lockfile(s) checked, no diagnostics remain."
587
+ end
562
588
  validate_release_lockfiles!(stage: "before release prep commit")
563
589
  end
564
590
 
565
591
  def validate_release_lockfiles!(stage:)
566
592
  diagnostics = release_lockfile_paths.flat_map { |path| release_lockfile_diagnostics(path) }
567
593
  return if diagnostics.empty?
594
+ if stage == "before push"
595
+ diagnostics = reset_release_lockfiles_before_push(diagnostics)
596
+ return if diagnostics.empty?
597
+ end
568
598
 
569
599
  abort(<<~MSG)
570
600
  Release lockfile validation failed #{stage}:
@@ -574,169 +604,140 @@ module Kettle
574
604
  end
575
605
 
576
606
  def release_lockfile_paths
577
- candidates = [
578
- File.join(@root, "Gemfile.lock"),
579
- File.join(@root, "Appraisal.root.gemfile.lock")
580
- ]
581
- candidates.select { |path| File.file?(path) }.sort
607
+ lockfile_reset.lockfile_paths
582
608
  end
583
609
 
584
610
  def release_lockfile_normalization_needed?(path)
585
- release_lockfile_has_local_path_remote?(path) || !release_lockfile_empty_registry_checksums(path).empty?
611
+ lockfile_reset.normalization_needed?(path)
586
612
  end
587
613
 
588
614
  def normalize_release_lockfile!(path)
589
- gemfile = release_gemfile_for_lockfile(path)
590
- unless gemfile && File.file?(gemfile)
591
- warn("Cannot normalize #{Kettle::Dev.display_path(path)} because its Gemfile was not found.")
592
- return
593
- end
594
-
595
- update_gems = release_lockfile_empty_registry_checksums(path).map(&:first).uniq.sort
596
- env = release_lockfile_normalization_env.merge("BUNDLE_GEMFILE" => gemfile)
597
- command = +"env"
598
- env.each do |key, value|
599
- command << " #{key}=#{Shellwords.escape(value)}"
600
- end
601
- command << " bundle lock"
602
- command << " --update #{update_gems.map { |gem_name| Shellwords.escape(gem_name) }.join(" ")}" unless update_gems.empty?
603
- run_cmd!(command)
615
+ lockfile_reset.reset_lockfile!(path)
604
616
  end
605
617
 
606
618
  def release_gemfile_for_lockfile(path)
607
- basename = File.basename(path)
608
- return File.join(@root, "Gemfile") if basename == "Gemfile.lock"
609
-
610
- path.delete_suffix(".lock")
619
+ lockfile_reset.gemfile_for_lockfile(path)
611
620
  end
612
621
 
613
622
  def release_lockfile_normalization_env
614
- {
615
- "KETTLE_DEV_DEV" => "false",
616
- "K_JEM_TEMPLATING" => "false",
617
- "STRUCTUREDMERGE_DEV" => "false",
618
- "TREE_SITTER_LANGUAGE_PACK_DEV" => "false",
619
- "RUBOCOP_LTS_LOCAL" => "false",
620
- "GALTZO_FLOSS_DEV" => "false",
621
- "UR_BRAIN_DEV" => "false"
622
- }
623
+ lockfile_reset.normalization_env
623
624
  end
624
625
 
625
626
  def release_lockfile_diagnostics(path)
626
- diagnostics = []
627
- release_lockfile_local_path_remote_lines(path).each do |line_number|
628
- diagnostics << "#{release_lockfile_label(path)} has local path remote at line #{line_number}"
629
- end
630
- release_lockfile_empty_registry_checksums(path).each do |name, version, line_number|
631
- diagnostics << "#{release_lockfile_label(path)} CHECKSUMS has no sha256 for #{name} #{version} at line #{line_number}"
632
- end
633
- diagnostics
627
+ lockfile_reset.diagnostics(path)
634
628
  end
635
629
 
636
630
  def release_lockfile_has_local_path_remote?(path)
637
- !release_lockfile_local_path_remote_lines(path).empty?
631
+ lockfile_reset.has_local_path_remote?(path)
638
632
  end
639
633
 
640
634
  def release_lockfile_local_path_remote_lines(path)
641
- File.readlines(path).filter_map.with_index(1) do |line, index|
642
- next unless line.start_with?(" remote: /", " remote: ./", " remote: ../")
643
-
644
- index
645
- end
635
+ lockfile_reset.local_path_remote_lines(path)
646
636
  end
647
637
 
648
638
  def release_lockfile_empty_registry_checksums(path)
649
- path_gems = release_lockfile_path_source_gems(path) | release_lockfile_path_dependency_gems(path)
650
- in_checksums = false
651
- entries = File.readlines(path).filter_map.with_index(1) do |line, index|
652
- stripped = line.strip
653
- if stripped == "CHECKSUMS"
654
- in_checksums = true
655
- next
656
- end
657
- next unless in_checksums
658
- next if stripped.empty?
659
- next if stripped == "BUNDLED WITH"
639
+ lockfile_reset.empty_registry_checksums(path)
640
+ end
660
641
 
661
- match = stripped.match(/\A([A-Za-z0-9_.-]+) \(([^)]+)\)(?:\s+(sha256=.*))?\z/)
662
- next unless match
642
+ def release_lockfile_has_any_sha_checksum?(path)
643
+ lockfile_reset.has_any_sha_checksum?(path)
644
+ end
663
645
 
664
- name = match[1]
665
- checksum = match[3].to_s
666
- next if !checksum.empty?
667
- next if path_gems.include?(name)
646
+ def release_lockfile_path_source_gems(path)
647
+ lockfile_reset.path_source_gems(path)
648
+ end
668
649
 
669
- [name, match[2], index]
670
- end
671
- return [] unless release_lockfile_has_any_sha_checksum?(path)
650
+ def release_lockfile_path_dependency_gems(path)
651
+ lockfile_reset.path_dependency_gems(path)
652
+ end
672
653
 
673
- entries
654
+ def release_lockfile_label(path)
655
+ Kettle::Dev.display_path(path)
674
656
  end
675
657
 
676
- def release_lockfile_has_any_sha_checksum?(path)
677
- in_checksums = false
678
- File.readlines(path).any? do |line|
679
- stripped = line.strip
680
- if stripped == "CHECKSUMS"
681
- in_checksums = true
682
- next false
683
- end
684
- next false unless in_checksums
658
+ def reset_release_lockfiles_before_push(diagnostics)
659
+ puts "Release lockfile validation found #{diagnostics.length} issue(s) before push:"
660
+ diagnostics.each { |diagnostic| puts " - #{diagnostic}" }
661
+ puts "Running one fallback release lockfile reset before push; tracked lockfile changes will amend the release prep commit."
662
+ changed_before_reset = changed_release_lockfile_paths
663
+ begin
664
+ lockfile_reset.reset(Kettle::Dev::LockfileReset::RELEASE_LOCKFILES_TARGET)
665
+ rescue Kettle::Dev::Error => error
666
+ puts error.message
667
+ end
685
668
 
686
- stripped.include?("sha256=")
669
+ remaining = release_lockfile_paths.flat_map { |path| release_lockfile_diagnostics(path) }
670
+ if remaining.empty?
671
+ amend_release_lockfile_reset_commit(changed_before_reset)
672
+ return []
687
673
  end
688
- end
689
674
 
690
- def release_lockfile_path_source_gems(path)
691
- gems = Set.new
692
- in_path = false
693
- in_specs = false
694
- File.readlines(path).each do |line|
695
- header = line.strip
696
- if header.match?(/\A[A-Z][A-Z ]*\z/)
697
- in_path = header == "PATH"
698
- in_specs = false
699
- next
700
- end
701
- next unless in_path
675
+ puts "Release lockfile reset did not repair all before-push issues:"
676
+ remaining.each { |diagnostic| puts " - #{diagnostic}" }
677
+ remaining
678
+ end
702
679
 
703
- if header == "specs:"
704
- in_specs = true
705
- next
680
+ def amend_release_lockfile_reset_commit(changed_before_reset = [])
681
+ paths = changed_release_lockfile_paths
682
+ if paths.empty?
683
+ if changed_before_reset.empty?
684
+ puts "Fallback release lockfile reset result: diagnostics cleared, but tracked lockfiles matched the release prep commit before and after reset; no amend is possible."
685
+ else
686
+ puts "Fallback release lockfile reset result: diagnostics cleared by restoring tracked lockfiles to the release prep commit; no amend is needed."
706
687
  end
707
- next unless in_specs
708
-
709
- match = line.match(/\A ([A-Za-z0-9_.-]+) \([^)]+\)/)
710
- gems << match[1] if match
688
+ return
711
689
  end
712
- gems
690
+
691
+ puts "Fallback release lockfile reset result: diagnostics cleared; amending release prep commit with #{paths.length} lockfile(s)."
692
+ abort("Failed to stage reset release lockfiles.") unless @git.add_paths(paths)
693
+ abort("Failed to amend release prep commit with reset lockfiles.") unless @git.commit_amend_no_edit
713
694
  end
714
695
 
715
- def release_lockfile_path_dependency_gems(path)
716
- gems = Set.new
717
- in_dependencies = false
718
- File.readlines(path).each do |line|
719
- header = line.strip
720
- if header.match?(/\A[A-Z][A-Z ]*\z/)
721
- in_dependencies = header == "DEPENDENCIES"
722
- next
723
- end
724
- next unless in_dependencies
696
+ def changed_release_lockfile_paths
697
+ release_lockfile_paths.select { |path| git_path_changed?(path) }
698
+ end
725
699
 
726
- match = line.match(/\A ([A-Za-z0-9_.-]+)!\z/)
727
- gems << match[1] if match
700
+ def git_path_changed?(path)
701
+ if @git.respond_to?(:diff_head_quiet?)
702
+ !@git.diff_head_quiet?(path)
703
+ else
704
+ !@git.diff_quiet?(path)
728
705
  end
729
- gems
730
706
  end
731
707
 
732
- def release_lockfile_label(path)
733
- Kettle::Dev.display_path(path)
708
+ def lockfile_reset
709
+ @lockfile_reset ||= Kettle::Dev::LockfileReset.new(root: @root, command_runner: method(:run_cmd!))
734
710
  end
735
711
 
736
712
  def run_changelog!
737
713
  cmd = "bundle exec kettle-changelog"
738
714
  cmd = "#{cmd} --version #{Shellwords.escape(@version_override)}" if @version_override
715
+ cmd = "#{cmd} --yes" if @yes
739
716
  run_cmd!(cmd)
717
+ @changelog_generated_coverage = true
718
+ end
719
+
720
+ def release_default_task_command
721
+ # `kettle-changelog` generates strict coverage by running `bundle exec kettle-test`.
722
+ # When that happened during this same release invocation, the default task can skip
723
+ # its test/coverage prerequisites and still run lint, audit, documentation, and any
724
+ # other non-test release checks. Resumed releases do not set this flag, so they keep
725
+ # the full default task behavior.
726
+ return "KETTLE_DEV_SKIP_TESTS=true bin/rake" if @changelog_generated_coverage
727
+
728
+ "bin/rake"
729
+ end
730
+
731
+ def confirm_yes!(message, prompt, abort_message)
732
+ puts(message)
733
+ if @yes
734
+ puts("#{prompt}y")
735
+ return
736
+ end
737
+
738
+ print(prompt)
739
+ ans = Kettle::Dev::InputAdapter.gets&.strip
740
+ abort(abort_message) unless ans&.downcase&.start_with?("y")
740
741
  end
741
742
 
742
743
  def changelog_strict?
@@ -1041,7 +1042,7 @@ module Kettle
1041
1042
  emit_command_event(cmd, "started")
1042
1043
  with_bundle_audit_skip_env do
1043
1044
  with_machine_stdout_redirect do
1044
- self.class.run_cmd!(cmd)
1045
+ run_command_with_release_secrets!(cmd)
1045
1046
  end
1046
1047
  end
1047
1048
  emit_command_event(cmd, "ok")
@@ -1055,12 +1056,65 @@ module Kettle
1055
1056
 
1056
1057
  def bundle_audit_skip_command(cmd)
1057
1058
  return cmd unless skip_bundle_audit?
1058
- return cmd unless cmd.start_with?("bin/rake")
1059
+ return cmd unless /(?:\A|\s)bin\/rake\b/.match?(cmd)
1059
1060
  return cmd if cmd.start_with?("KETTLE_DEV_SKIP_BUNDLE_AUDIT=")
1060
1061
 
1061
1062
  "KETTLE_DEV_SKIP_BUNDLE_AUDIT=true #{cmd}"
1062
1063
  end
1063
1064
 
1065
+ def run_command_with_release_secrets!(cmd)
1066
+ return self.class.run_cmd!(cmd) unless release_secret_command?(cmd) && release_secrets_configured?
1067
+
1068
+ puts "$ #{cmd}"
1069
+ _stdout_str, stderr_str, status = Kettle::Dev::InteractiveReleaseCommand.new(secrets_provider: @secrets_provider).call(
1070
+ self.class.send(:command_env),
1071
+ cmd
1072
+ )
1073
+ return if status.success?
1074
+
1075
+ exit_code = status.respond_to?(:exitstatus) ? status.exitstatus : 1
1076
+ diag = stderr_str.to_s.empty? ? "" : "\n--- STDERR (last 20 lines) ---\n#{stderr_str.lines.last(20).join}".rstrip
1077
+ abort("Command failed: #{cmd} (exit #{exit_code})#{diag}")
1078
+ rescue Kettle::Dev::Error => error
1079
+ abort(error.message)
1080
+ end
1081
+
1082
+ def release_secrets_configured?
1083
+ !@secrets_provider.instance_of?(Kettle::Dev::ReleaseSecrets::Provider)
1084
+ end
1085
+
1086
+ def release_secrets_provider_label
1087
+ release_secrets_configured? ? @secrets_provider.class.name.to_s.split("::").last : "interactive"
1088
+ end
1089
+
1090
+ def signing_enabled?
1091
+ ENV.fetch("SKIP_GEM_SIGNING", "false").casecmp("false").zero?
1092
+ end
1093
+
1094
+ def ensure_release_secrets_ready_for_signing!
1095
+ value = @secrets_provider.gem_signing_passphrase.to_s
1096
+ abort(release_secrets_configuration_message("gem signing passphrase was empty")) if value.empty?
1097
+ rescue Kettle::Dev::Error => error
1098
+ abort(release_secrets_configuration_message(error.message))
1099
+ end
1100
+
1101
+ def release_secrets_configuration_message(reason)
1102
+ <<~MSG.strip
1103
+ Release secrets provider was requested, but #{reason}.
1104
+ Configure the 1Password CLI before release:
1105
+ KETTLE_RELEASE_1PASSWORD_ITEM=Rubygems
1106
+ KETTLE_RELEASE_1PASSWORD_GEM_SIGNING_PASSPHRASE_FIELD=GEM-SIGN-PASSPHRASE
1107
+ Or provide an explicit reference:
1108
+ KETTLE_RELEASE_1PASSWORD_GEM_SIGNING_PASSPHRASE_REFERENCE=op://<vault>/<item>/<field>
1109
+ Ensure `op` is installed and signed in. Secret prompts are not allowed when --secrets-provider is set.
1110
+ MSG
1111
+ end
1112
+
1113
+ def release_secret_command?(cmd)
1114
+ /\Abundle(\s+exec)?\s+rake\s+(build|release)\b/.match?(cmd) ||
1115
+ /\Agem\s+push\b/.match?(cmd)
1116
+ end
1117
+
1064
1118
  def with_bundle_audit_skip_env
1065
1119
  return yield unless skip_bundle_audit?
1066
1120
 
@@ -1170,7 +1224,7 @@ module Kettle
1170
1224
  puts "Local CI failed for #{chosen}."
1171
1225
  if committed
1172
1226
  puts "Rolling back release prep commit (soft reset)..."
1173
- system("git", "reset", "--soft", "HEAD^")
1227
+ @git.reset_soft("HEAD^")
1174
1228
  end
1175
1229
  abort("Aborting due to local CI failure.")
1176
1230
  end
@@ -1188,7 +1242,7 @@ module Kettle
1188
1242
  puts "Local tag #{tag} already exists."
1189
1243
  else
1190
1244
  puts "Creating local git tag #{tag} without pushing it."
1191
- run_cmd!("git tag -a #{Shellwords.escape(tag)} -m #{Shellwords.escape(tag)}")
1245
+ abort("Failed to create local tag #{tag}.") unless @git.tag_annotated(tag, tag)
1192
1246
  end
1193
1247
 
1194
1248
  puts "Publishing #{File.basename(gem_path)} to RubyGems without pushing git refs..."
@@ -1197,7 +1251,7 @@ module Kettle
1197
1251
  @release_candidate ||= build_release_candidate(gem_name, version)
1198
1252
  @release_candidate.published = true
1199
1253
  confirm_release_candidate_available!(@release_candidate)
1200
- mark_gem_coop_release_cache_bust(version, gem_name: gem_name)
1254
+ mark_rubygems_release_cache_bust(version, gem_name: gem_name)
1201
1255
  end
1202
1256
 
1203
1257
  def build_release_candidate(gem_name, version)
@@ -1368,7 +1422,7 @@ module Kettle
1368
1422
  end
1369
1423
 
1370
1424
  def latest_released_versions(gem_name, current_version)
1371
- data = Kettle::Dev::GemCoopVersions.fetch(gem_name, version_hint: current_version)
1425
+ data = Kettle::Dev::RubyGemsVersions.fetch(gem_name, version_hint: current_version)
1372
1426
  return [nil, nil] unless data.is_a?(Array)
1373
1427
 
1374
1428
  versions = data.map { |h| h["number"] }.compact
@@ -1386,11 +1440,11 @@ module Kettle
1386
1440
  [nil, nil]
1387
1441
  end
1388
1442
 
1389
- def mark_gem_coop_release_cache_bust(version, gem_name: nil)
1443
+ def mark_rubygems_release_cache_bust(version, gem_name: nil)
1390
1444
  gem_name ||= detect_gem_name
1391
- Kettle::Dev::GemCoopVersions.mark_released(gem_name, version)
1445
+ Kettle::Dev::RubyGemsVersions.mark_released(gem_name, version)
1392
1446
  rescue => error
1393
- warn("[kettle-release] could not mark gem.coop cache-bust for release-state: #{error.class}: #{error.message}") if Kettle::Dev::DEBUGGING
1447
+ warn("[kettle-release] could not mark RubyGems.org cache-bust for release-state: #{error.class}: #{error.message}") if Kettle::Dev::DEBUGGING
1394
1448
  end
1395
1449
 
1396
1450
  def gem_name_from_gem_path(gem_path, version)
@@ -1401,13 +1455,13 @@ module Kettle
1401
1455
  def commit_release_prep!(version)
1402
1456
  msg = "🔖 Prepare release v#{version}"
1403
1457
  # Stage all changes (including new/untracked files) prior to committing
1404
- run_cmd!("git add -A")
1458
+ abort("Failed to stage release prep changes.") unless @git.add_all
1405
1459
  out, _ = git_output(["status", "--porcelain"])
1406
1460
  if out.empty?
1407
1461
  puts "No changes to commit for release prep (continuing)."
1408
1462
  false
1409
1463
  else
1410
- run_cmd!(%(git commit -am #{Shellwords.escape(msg)}))
1464
+ abort("Failed to commit release prep changes.") unless @git.commit_all(msg)
1411
1465
  true
1412
1466
  end
1413
1467
  end