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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +146 -1
- data/README.md +115 -1
- data/exe/kettle-changelog +4 -2
- data/exe/kettle-release +40 -2
- data/exe/kettle-reset +35 -0
- data/lib/kettle/dev/changelog_cli.rb +73 -34
- data/lib/kettle/dev/ci_monitor.rb +0 -2
- data/lib/kettle/dev/dvcs_cli.rb +3 -2
- data/lib/kettle/dev/executable_version.rb +15 -1
- data/lib/kettle/dev/git_adapter.rb +126 -18
- data/lib/kettle/dev/interactive_release_command.rb +125 -0
- data/lib/kettle/dev/lockfile_reset.rb +442 -0
- data/lib/kettle/dev/rakelib/spec_test.rake +8 -2
- data/lib/kettle/dev/rakelib/yard.rake +11 -0
- data/lib/kettle/dev/release_cli.rb +203 -149
- data/lib/kettle/dev/release_secrets.rb +153 -0
- data/lib/kettle/dev/reset_cli.rb +81 -0
- data/lib/kettle/dev/{gem_coop_versions.rb → ruby_gems_versions.rb} +6 -6
- data/lib/kettle/dev/version.rb +4 -1
- data/lib/kettle/dev.rb +11 -1
- data.tar.gz.sig +0 -0
- metadata +37 -11
- metadata.gz.sig +0 -0
|
@@ -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]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
251
|
+
puts "Could not determine latest released version from RubyGems.org (offline?). Proceeding without sanity check."
|
|
245
252
|
end
|
|
246
253
|
|
|
247
|
-
|
|
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!(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
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
|
-
|
|
611
|
+
lockfile_reset.normalization_needed?(path)
|
|
586
612
|
end
|
|
587
613
|
|
|
588
614
|
def normalize_release_lockfile!(path)
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
631
|
+
lockfile_reset.has_local_path_remote?(path)
|
|
638
632
|
end
|
|
639
633
|
|
|
640
634
|
def release_lockfile_local_path_remote_lines(path)
|
|
641
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
|
|
662
|
-
|
|
642
|
+
def release_lockfile_has_any_sha_checksum?(path)
|
|
643
|
+
lockfile_reset.has_any_sha_checksum?(path)
|
|
644
|
+
end
|
|
663
645
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
650
|
+
def release_lockfile_path_dependency_gems(path)
|
|
651
|
+
lockfile_reset.path_dependency_gems(path)
|
|
652
|
+
end
|
|
672
653
|
|
|
673
|
-
|
|
654
|
+
def release_lockfile_label(path)
|
|
655
|
+
Kettle::Dev.display_path(path)
|
|
674
656
|
end
|
|
675
657
|
|
|
676
|
-
def
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
match = line.match(/\A ([A-Za-z0-9_.-]+) \([^)]+\)/)
|
|
710
|
-
gems << match[1] if match
|
|
688
|
+
return
|
|
711
689
|
end
|
|
712
|
-
|
|
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
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
727
|
-
|
|
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
|
|
733
|
-
Kettle::Dev.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
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
|
|
1443
|
+
def mark_rubygems_release_cache_bust(version, gem_name: nil)
|
|
1390
1444
|
gem_name ||= detect_gem_name
|
|
1391
|
-
Kettle::Dev::
|
|
1445
|
+
Kettle::Dev::RubyGemsVersions.mark_released(gem_name, version)
|
|
1392
1446
|
rescue => error
|
|
1393
|
-
warn("[kettle-release] could not mark
|
|
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
|
-
|
|
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
|
-
|
|
1464
|
+
abort("Failed to commit release prep changes.") unless @git.commit_all(msg)
|
|
1411
1465
|
true
|
|
1412
1466
|
end
|
|
1413
1467
|
end
|