kettle-family 1.2.20 → 1.2.21

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: 971f2fe5b4d5a64435ee332ba8d083f494f8ba8b5a6f1a31a81885e504b3a585
4
- data.tar.gz: 9ea743325bbf23369827a7220361a765db24a9c2a6d1b893b7233135946d253f
3
+ metadata.gz: 351c882553083a30e5c52c10fb650195f5e00d5a77e452e19058da6e32c69c94
4
+ data.tar.gz: c3cf82b1ec51d6f9d043e579e3de5d2b09fd16f0d187d71b3abdfa9626db4576
5
5
  SHA512:
6
- metadata.gz: 2efbf5d7424ae7f3d758a73d3d5e9390a55e6fe09f5207eec73b932024cf700910e89001f5f116daa2aa567daa61aacdc5e66b9849ccbb87420e545de72c0d6d
7
- data.tar.gz: dbaf11ae29801f3f0f4f784dd06117e0b6e5cc361494e6c0633197ea754af21505e399f6b8cc47bcf4521d34765e6672d76822d09f9d83aa49d8ec27fd2ab7f1
6
+ metadata.gz: 0b2ff66410811aa28af2f578fd354cb9f9d7174a648f3863c7ad68e3e3098b129cb4a2bc9678a24a0fe8d478bba7a81bf61bae35013c48fae124ed0e113c56a2
7
+ data.tar.gz: e25a8ee6c06843662c0e2da1d9fa68d15b1aa6af30c5f5bbcdafe1690c0912e0d04e7f1b8bc1e814d4e5fef79850f1f4c430a69191d647679b393fa001547632
checksums.yaml.gz.sig CHANGED
@@ -1,4 +1 @@
1
- �ۥ.؇!�R����Bj,��]
2
- �'���z�� �ʊ��?��ޭ��۟�-��ȿ'g
3
- �w*$
4
- �}*'�ݠ��z��RU6�2Y�Zc���Q�ĸ�ԭY��]�zF�2>�f�&���i���g T�FO� ���,T�}���d@��q�r*���ʙ�3g5P����%���a/`[mR���W?���zw:�-�"{�����O����<?a�)�*��&
1
+ ��OA73�M����*h�C���6��&u;d��^D\.��:��t��E���'E�~����0{���pqmɸ hDg�U�u�c]��޻1��lDq4q�@�ҋ%�
data/CHANGELOG.md CHANGED
@@ -30,6 +30,35 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.21] - 2026-08-01
34
+
35
+ - TAG: [v1.2.21][1.2.21t]
36
+ - COVERAGE: 93.95% -- 4453/4740 lines in 30 files
37
+ - BRANCH COVERAGE: 76.28% -- 1733/2272 branches in 30 files
38
+ - 29.12% documented
39
+
40
+ ### Added
41
+
42
+ - Add version and mise-trust commands and run template members in dependency waves.
43
+
44
+ - Add reconcile-releases for verified RubyGems to GitHub Release backfills.
45
+
46
+ ### Fixed
47
+
48
+ - Template lockfile recovery preserves compound shell commands when appending lockfile bootstrap gems.
49
+
50
+ - Preserve the original template bootstrap failure when lockfile recovery finds no release lockfiles.
51
+
52
+ - Replace obsolete byebug debugger dependency stacks with debug before templating legacy members.
53
+
54
+ - Skip nomono-specific lockfile normalization until a legacy member declares nomono.
55
+
56
+ - Report actionable GitHub release reconciliation checks in family summaries.
57
+
58
+ - Honor KETTLE_DEV_DEV when release reconciliation invokes kettle-gh-release.
59
+
60
+ - Retry CI dependency installation while newly published family runtime floors propagate through gem.coop.
61
+
33
62
  ## [1.2.20] - 2026-07-31
34
63
 
35
64
  - TAG: [v1.2.20][1.2.20t]
@@ -1579,7 +1608,9 @@ Please file a bug if you notice a violation of semantic versioning.
1579
1608
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
1580
1609
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
1581
1610
 
1582
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.20...HEAD
1611
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.21...HEAD
1612
+ [1.2.21]: https://github.com/kettle-dev/kettle-family/compare/v1.2.20...v1.2.21
1613
+ [1.2.21t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.21
1583
1614
  [1.2.20]: https://github.com/kettle-dev/kettle-family/compare/v1.2.19...v1.2.20
1584
1615
  [1.2.20t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.20
1585
1616
  [1.2.19]: https://github.com/kettle-dev/kettle-family/compare/v1.2.18...v1.2.19
data/README.md CHANGED
@@ -819,7 +819,7 @@ Thanks for RTFM. ☺️
819
819
  [📌gitmoji]: https://gitmoji.dev
820
820
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
821
821
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
822
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.547-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
822
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.741-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
823
823
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
824
824
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
825
825
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -10,7 +10,7 @@ module Kettle
10
10
  class CLI < CommandKit::Command
11
11
  include CommandKit::Commands
12
12
 
13
- COMMANDS = %w[discover plan report metadata check clean-unreleased reset test lint docs template gha-sha-pins bup bupb bex install bump bump-version add-changelog release push pull sync up branch-lanes release-state state].freeze
13
+ COMMANDS = %w[version mise-trust discover plan report metadata check clean-unreleased reconcile-releases reset test lint docs template gha-sha-pins bup bupb bex install bump bump-version add-changelog release push pull sync up branch-lanes release-state state].freeze
14
14
  WORKFLOW_COMMANDS = %w[check reset test lint docs template gha-sha-pins bup bupb bex release push pull sync up].freeze
15
15
 
16
16
  command_name "kettle-family"
@@ -204,6 +204,30 @@ module Kettle
204
204
  end
205
205
  end
206
206
 
207
+ class Version < CommandKit::Command
208
+ command_name "version"
209
+ usage ""
210
+ description "Print the kettle-family version."
211
+
212
+ def run(*args)
213
+ raise OptionParser::InvalidArgument, "unexpected argument(s): #{args.join(" ")}" unless args.empty?
214
+
215
+ stdout.puts(Kettle::Family::Version::VERSION)
216
+ 0
217
+ end
218
+ end
219
+
220
+ class MiseTrust < BaseCommand
221
+ command_name "mise-trust"
222
+ usage "[options]"
223
+ description "Trust mise.toml at the family root and selected member roots."
224
+
225
+ def run(*args)
226
+ unexpected_arguments!(args)
227
+ run_family("mise-trust", execute: true)
228
+ end
229
+ end
230
+
207
231
  class Plan < Discover
208
232
  command_name "plan"
209
233
  description "Alias for discover while execution workflows are built."
@@ -290,6 +314,19 @@ module Kettle
290
314
  description "Uninstall locally installed family gem versions newer than the latest released version."
291
315
  end
292
316
 
317
+ class ReconcileReleases < BaseCommand
318
+ include ExecutionOptions
319
+
320
+ command_name "reconcile-releases"
321
+ usage "[options]"
322
+ description "Check RubyGems releases for missing GitHub Releases; --execute creates only verified backfills."
323
+
324
+ def run(*args)
325
+ unexpected_arguments!(args)
326
+ run_family("reconcile-releases")
327
+ end
328
+ end
329
+
293
330
  class Reset < WorkflowCommand
294
331
  command_name "reset"
295
332
  usage "[options] TARGET"
@@ -524,11 +561,14 @@ module Kettle
524
561
  end
525
562
 
526
563
  command Discover
564
+ command Version
565
+ command MiseTrust
527
566
  command Plan
528
567
  command "report", ReportCommand
529
568
  command Metadata
530
569
  command Check
531
570
  command CleanUnreleased
571
+ command ReconcileReleases
532
572
  command Reset
533
573
  command Test
534
574
  command Lint
@@ -639,9 +679,11 @@ module Kettle
639
679
  end
640
680
 
641
681
  def command_results_for_current_branch(command:, config:, members:, options:, start_at: StartAt.new(nil, nil))
682
+ return mise_trust_results(config: config, members: members) if command == "mise-trust"
642
683
  return bump_version_results(members: members, options: options, phase: command) if %w[bump bump-version].include?(command)
643
684
  return add_changelog_results(members: members, options: options) if command == "add-changelog"
644
685
  return clean_unreleased_results(config: config, members: members, options: options) if command == "clean-unreleased"
686
+ return reconcile_release_results(config: config, members: members, options: options) if command == "reconcile-releases"
645
687
  return branch_lane_results(config: config, members: members) if command == "branch-lanes"
646
688
  return release_state_results(config: config, members: members) if command == "release-state"
647
689
  return install_results(config: config, members: members, options: options) if command == "install"
@@ -683,6 +725,15 @@ module Kettle
683
725
  ).results
684
726
  end
685
727
 
728
+ def mise_trust_results(config:, members:)
729
+ roots = [config.root, *members.map(&:root)].uniq
730
+ runner = CommandRunner.new(execute: true, accept: true)
731
+ roots.map.with_index do |root, index|
732
+ member = members.find { |candidate| candidate.root == root } || Member.new("family", root, nil, nil, nil, [], [], nil, [], [])
733
+ runner.call(member: member, phase: "mise_trust", command: ["mise", "trust", "-C", root], env: {})
734
+ end
735
+ end
736
+
686
737
  def progress_io(command, options)
687
738
  return nil unless %w[release template].include?(command)
688
739
  return nil unless options[:execute]
@@ -928,6 +979,10 @@ module Kettle
928
979
  UnreleasedGemCleanup.new(config: config, members: members, execute: options[:execute]).results
929
980
  end
930
981
 
982
+ def reconcile_release_results(config:, members:, options:)
983
+ ReleaseReconciler.new(config: config, members: members, execute: options[:execute]).results
984
+ end
985
+
931
986
  def install_results(config:, members:, options:)
932
987
  LocalInstall.new(config: config, members: members, execute: options[:execute], jobs: options[:jobs]).results
933
988
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kettle/dev"
4
+
5
+ module Kettle
6
+ module Family
7
+ # Removes the obsolete byebug debugger stack before Bundler evaluates a
8
+ # legacy member. This must happen before kettle-jem can template Gemfile.
9
+ class DebuggerBootstrap
10
+ DEBUG_GEM = "debug"
11
+ OBSOLETE_NAME_FRAGMENT = "byebug"
12
+ DECLARATION_METHODS = %i[gem add_dependency add_development_dependency].freeze
13
+
14
+ def initialize(mode: :dry_run)
15
+ @mode = mode
16
+ end
17
+
18
+ attr_reader :mode
19
+
20
+ def member_needs_bootstrap?(member)
21
+ edits_for(member).any?
22
+ end
23
+
24
+ def bootstrap_member(member)
25
+ edits = edits_for(member)
26
+ write_edits(edits)
27
+ CommandResult.new(
28
+ member.name,
29
+ "template_bootstrap_debugger",
30
+ ["internal", "debugger-bootstrap", DEBUG_GEM],
31
+ member.root,
32
+ 0,
33
+ true,
34
+ "replaced obsolete byebug debugger dependencies with #{DEBUG_GEM} in #{edits.map { |edit| File.basename(edit.fetch(:path)) }.uniq.join(", ")}\n",
35
+ "",
36
+ 0.0,
37
+ mode != :execute,
38
+ (mode == :execute) ? nil : "dry run"
39
+ )
40
+ end
41
+
42
+ private
43
+
44
+ def write_edits(edits)
45
+ return if mode != :execute || edits.empty?
46
+
47
+ Kettle::Dev::VersionBump.write_edits(edits)
48
+ end
49
+
50
+ def edits_for(member)
51
+ [File.join(member.root, "Gemfile"), member.gemspec_path].filter_map { |path| file_edits(path) }.flatten
52
+ end
53
+
54
+ def file_edits(path)
55
+ return [] unless File.file?(path)
56
+
57
+ source = File.read(path)
58
+ declarations = debugger_declarations(source, path)
59
+ return [] if declarations.empty?
60
+
61
+ declarations.filter_map.with_index do |declaration, index|
62
+ debugger_edit(path, source, declaration, replace: index.zero?)
63
+ end
64
+ end
65
+
66
+ def debugger_declarations(source, path)
67
+ parse_result = Kettle::Dev::VersionBump.parse_source(source, path)
68
+ Kettle::Dev::VersionBump.each_node(parse_result.value).filter_map do |node|
69
+ next unless node.is_a?(Prism::CallNode)
70
+ next unless DECLARATION_METHODS.include?(node.name)
71
+
72
+ name_node = node.arguments&.arguments&.first
73
+ next unless name_node.is_a?(Prism::StringNode)
74
+ next unless name_node.unescaped.include?(OBSOLETE_NAME_FRAGMENT)
75
+
76
+ {node: node, name_node: name_node}
77
+ end
78
+ end
79
+
80
+ def debugger_edit(path, source, declaration, replace:)
81
+ node = declaration.fetch(:node)
82
+ if replace
83
+ replacement = debugger_replacement(node, declaration.fetch(:name_node))
84
+ return Kettle::Dev::VersionBump.file_edit(
85
+ path, source, node.location.start_offset, node.location.end_offset, replacement
86
+ )
87
+ end
88
+
89
+ line_start = source.rindex("\n", node.location.start_offset - 1).to_i + 1
90
+ line_end = source.index("\n", node.location.end_offset) || source.length
91
+ line_end += 1 if line_end < source.length
92
+ Kettle::Dev::VersionBump.file_edit(path, source, line_start, line_end, "")
93
+ end
94
+
95
+ def debugger_replacement(node, name_node)
96
+ receiver = node.receiver ? "#{node.receiver.location.slice}." : ""
97
+ quote = Kettle::Dev::VersionBump.quote_like(name_node.location.slice, DEBUG_GEM)
98
+ suffix = (node.name == :gem) ? ", require: false" : ""
99
+ "#{receiver}#{node.name} #{quote}#{suffix}"
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+ require "json"
5
+
6
+ module Kettle
7
+ module Family
8
+ # Reconciles RubyGems publications with GitHub Release metadata without
9
+ # creating tags or publishing gems. The child executable performs the
10
+ # authoritative prerequisite checks before an optional GitHub API call.
11
+ class ReleaseReconciler
12
+ CHECK_PHASE = "reconcile_github_release_check"
13
+ CREATE_PHASE = "reconcile_github_release"
14
+
15
+ def initialize(config:, members:, execute: false, runner: nil)
16
+ @config = config
17
+ @members = members
18
+ @execute = execute
19
+ @runner = runner || CommandRunner.new(execute: true)
20
+ end
21
+
22
+ def results
23
+ release_states = release_state_by_member
24
+ members.flat_map do |member|
25
+ reconcile_member(member: member, release_state: release_states[member.name])
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :config, :members, :execute, :runner
32
+
33
+ def release_state_by_member
34
+ ReleaseStateCheck.new(config: config, members: members).results.each_with_object({}) do |result, memo|
35
+ memo[result.member_name] = result
36
+ end
37
+ end
38
+
39
+ def reconcile_member(member:, release_state:)
40
+ return [state_failure(member: member, release_state: release_state)] unless release_state&.ok?
41
+
42
+ state = release_state.state || {}
43
+ version = published_version(state)
44
+ return [informational_result(member: member, message: "RubyGems release version is unknown; no reconciliation attempted")] unless version
45
+ return [informational_result(member: member, message: "GitHub release already matches v#{version}")] if github_release_matches?(state, version)
46
+
47
+ check = runner.call(member: member, phase: CHECK_PHASE, command: gh_release_command("--check", "--release-version", version, "--events"))
48
+ return [check] unless check.ok?
49
+ check = summarize_check(check: check, member: member, version: version)
50
+ return [check] unless execute
51
+
52
+ [check, runner.call(member: member, phase: CREATE_PHASE, command: gh_release_command("--release-version", version, "--events"))]
53
+ end
54
+
55
+ def published_version(state)
56
+ version = state.fetch("latest_released", nil).to_s.delete_prefix("v")
57
+ return nil if version.empty? || version == "unknown"
58
+
59
+ version
60
+ end
61
+
62
+ def github_release_matches?(state, version)
63
+ state.fetch("github_latest_release", nil).to_s.delete_prefix("v") == version
64
+ end
65
+
66
+ def gh_release_command(*args)
67
+ [RbConfig.ruby, kettle_gh_release_path, *args]
68
+ end
69
+
70
+ def kettle_gh_release_path
71
+ local_path = local_kettle_dev_executable
72
+ return local_path if local_path
73
+
74
+ specification = Gem.loaded_specs["kettle-dev"] || Gem::Specification.find_by_name("kettle-dev")
75
+ File.join(specification.full_gem_path, "exe", "kettle-gh-release")
76
+ end
77
+
78
+ def local_kettle_dev_executable
79
+ root = ENV.fetch("KETTLE_DEV_DEV", "").strip
80
+ return if root.empty? || %w[false 0 no off].include?(root.downcase)
81
+
82
+ candidates = [
83
+ File.join(root, "exe", "kettle-gh-release"),
84
+ File.join(root, "kettle-dev", "exe", "kettle-gh-release")
85
+ ]
86
+ candidates.find { |path| File.file?(path) }
87
+ end
88
+
89
+ def informational_result(member:, message:)
90
+ CommandResult.new(
91
+ member_name: member.name,
92
+ phase: CHECK_PHASE,
93
+ command: ["internal", CHECK_PHASE],
94
+ workdir: member.root,
95
+ status: 0,
96
+ success: true,
97
+ stdout: message,
98
+ stderr: "",
99
+ elapsed_seconds: 0.0,
100
+ skipped: false,
101
+ reason: nil
102
+ )
103
+ end
104
+
105
+ def summarize_check(check:, member:, version:)
106
+ payload = check.stdout.to_s.each_line.filter_map do |line|
107
+ candidate = JSON.parse(line)
108
+ candidate if candidate["type"] == "github_release"
109
+ rescue JSON::ParserError
110
+ nil
111
+ end.last
112
+ message = payload&.fetch("message", nil) || "GitHub release v#{version} is eligible to be created"
113
+
114
+ CommandResult.new(
115
+ member_name: member.name,
116
+ phase: check.phase,
117
+ command: check.command,
118
+ workdir: check.workdir,
119
+ status: check.status,
120
+ success: check.success,
121
+ stdout: message,
122
+ stderr: check.stderr,
123
+ elapsed_seconds: check.elapsed_seconds,
124
+ skipped: check.skipped,
125
+ reason: check.reason,
126
+ branch: check.branch,
127
+ output_streamed: check.output_streamed,
128
+ log_path: check.log_path
129
+ )
130
+ end
131
+
132
+ def state_failure(member:, release_state:)
133
+ CommandResult.new(
134
+ member_name: member.name,
135
+ phase: CHECK_PHASE,
136
+ command: ["internal", CHECK_PHASE],
137
+ workdir: member.root,
138
+ status: release_state&.status || 1,
139
+ success: false,
140
+ stdout: "",
141
+ stderr: release_state&.stderr.to_s,
142
+ elapsed_seconds: 0.0,
143
+ skipped: false,
144
+ reason: "release state unavailable"
145
+ )
146
+ end
147
+ end
148
+ end
149
+ end
@@ -20,6 +20,7 @@ module Kettle
20
20
  lint
21
21
  pull
22
22
  push
23
+ reconcile-releases
23
24
  release
24
25
  release-state
25
26
  reset
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "1.2.20"
8
+ VERSION = "1.2.21"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -228,21 +228,43 @@ module Kettle
228
228
  end
229
229
 
230
230
  def template_member_workflow_results(workflow_members)
231
+ debugger_results = template_debugger_bootstrap_results(workflow_members)
232
+ return debugger_results unless debugger_results.all?(&:ok?)
233
+
231
234
  bootstrap_results = template_bootstrap_dependency_results(workflow_members)
232
- return bootstrap_results unless bootstrap_results.all?(&:ok?)
235
+ return debugger_results + bootstrap_results unless bootstrap_results.all?(&:ok?)
236
+
237
+ template_progress = start_template_progress(workflow_members)
238
+ results = template_dependency_waves(workflow_members).each_with_object([]) do |wave, memo|
239
+ wave_results = template_results_for_wave(wave, progress: template_progress)
240
+ memo.concat(wave_results)
241
+ break memo unless wave_results.all?(&:ok?)
242
+ end
243
+ emit_template_progress_summary(results, progress: template_progress)
244
+ debugger_results + bootstrap_results + results
245
+ end
246
+
247
+ def template_debugger_bootstrap_results(workflow_members)
248
+ bootstrap = DebuggerBootstrap.new(mode: :execute)
249
+ workflow_members.filter_map do |member|
250
+ bootstrap.bootstrap_member(member) if bootstrap.member_needs_bootstrap?(member)
251
+ end
252
+ rescue => error
253
+ [template_bootstrap_failure_result(workflow_members.first, error.message)]
254
+ end
233
255
 
256
+ def template_results_for_wave(wave, progress:)
234
257
  queue = Queue.new
235
- workflow_members.each_with_index { |member, index| queue << [index, member] }
236
- ordered_results = Array.new(workflow_members.length)
258
+ wave.each_with_index { |member, index| queue << [index, member] }
259
+ ordered_results = Array.new(wave.length)
237
260
  mutex = Mutex.new
238
261
  stop = false
239
- template_progress = start_template_progress(workflow_members)
240
- Array.new(template_jobs(workflow_members)) do
262
+ Array.new([template_jobs(wave), wave.length].min) do
241
263
  Thread.new do # rubocop:disable ThreadSafety/NewThread -- family templating intentionally runs independent members concurrently.
242
264
  loop do
243
265
  break if mutex.synchronize { stop }
244
266
  index, member = queue.pop(true)
245
- member_results = template_results_for_member(member, progress: template_progress)
267
+ member_results = template_results_for_member(member, progress: progress)
246
268
  mutex.synchronize do
247
269
  ordered_results[index] = member_results
248
270
  stop = true unless member_results.all?(&:ok?)
@@ -252,9 +274,23 @@ module Kettle
252
274
  end
253
275
  end
254
276
  end.each(&:join)
255
- flattened = ordered_results.compact.flatten
256
- emit_template_progress_summary(flattened, progress: template_progress)
257
- bootstrap_results + flattened
277
+ ordered_results.compact.flatten
278
+ end
279
+
280
+ def template_dependency_waves(workflow_members)
281
+ by_name = workflow_members.to_h { |member| [member.name, member] }
282
+ remaining = workflow_members.dup
283
+ completed = Set.new
284
+ [].tap do |waves|
285
+ until remaining.empty?
286
+ wave = remaining.select { |member| (member.dependencies & by_name.keys).all? { |name| completed.include?(name) } }
287
+ raise Error, "template dependency wave could not resolve: #{remaining.map(&:name).join(", ")}" if wave.empty?
288
+
289
+ waves << wave
290
+ completed.merge(wave.map(&:name))
291
+ remaining -= wave
292
+ end
293
+ end
258
294
  end
259
295
 
260
296
  def template_bootstrap_dependency_results(workflow_members)
@@ -2558,9 +2594,10 @@ module Kettle
2558
2594
  return configured unless template_prepare_lockfiles_phase?(phase)
2559
2595
  return %w[bundle install] if bundle_update_command?(configured) && !File.file?(File.join(member.root, "Gemfile.lock"))
2560
2596
 
2561
- command = PRE_TEMPLATE_BOOTSTRAP_GEMS.select { |gem_name| member_lockfile_contains_gem?(member, gem_name) }.reduce(configured) do |command_text, gem_name|
2597
+ command = PRE_TEMPLATE_BOOTSTRAP_GEMS.select { |gem_name| member_declares_or_locks_gem?(member, gem_name) }.reduce(configured) do |command_text, gem_name|
2562
2598
  append_command_arg(command_text, gem_name)
2563
2599
  end
2600
+ command = remove_command_arg(command, "nomono") unless member_declares_or_locks_gem?(member, "nomono")
2564
2601
  skip_checksum_option ? remove_command_arg(command, "--add-checksums") : command
2565
2602
  end
2566
2603
 
@@ -2575,7 +2612,19 @@ module Kettle
2575
2612
  def append_command_arg(command_text, arg)
2576
2613
  return command_text if normalize_command_includes_arg?(command_text, arg)
2577
2614
 
2578
- argv = command_text.is_a?(Array) ? command_text.map(&:to_s) : Shellwords.split(command_text.to_s)
2615
+ return append_command_arg_to_argv(command_text.map(&:to_s), arg) if command_text.is_a?(Array)
2616
+
2617
+ # CommandRunner executes String commands through `sh -lc`. Ruby has no
2618
+ # shell AST, so only parse the leading command segment and retain a
2619
+ # configured compound-command operator and its remaining segments.
2620
+ command, separator, remainder = command_text.to_s.partition(/\s(?:&&|\|\||;)\s/)
2621
+ argv = append_command_arg_to_argv(Shellwords.split(command), arg)
2622
+ [argv.shelljoin, separator, remainder].join
2623
+ rescue ArgumentError
2624
+ "#{command_text} #{Shellwords.escape(arg)}"
2625
+ end
2626
+
2627
+ def append_command_arg_to_argv(argv, arg)
2579
2628
  update_index = argv.index("update")
2580
2629
  if update_index
2581
2630
  option_index = argv[(update_index + 1)..]&.index { |token| token.start_with?("-") }
@@ -2584,11 +2633,7 @@ module Kettle
2584
2633
  else
2585
2634
  argv << arg
2586
2635
  end
2587
- return argv if command_text.is_a?(Array)
2588
-
2589
- argv.shelljoin
2590
- rescue ArgumentError
2591
- "#{command_text} #{Shellwords.escape(arg)}"
2636
+ argv
2592
2637
  end
2593
2638
 
2594
2639
  def remove_command_arg(command_text, arg)
@@ -2618,6 +2663,20 @@ module Kettle
2618
2663
  File.readlines(lockfile).any? { |line| line.match?(/\A #{Regexp.escape(gem_name)} \(/) }
2619
2664
  end
2620
2665
 
2666
+ def member_declares_or_locks_gem?(member, gem_name)
2667
+ return true if member_lockfile_contains_gem?(member, gem_name)
2668
+
2669
+ gemfile = File.join(member.root, "Gemfile")
2670
+ return false unless File.file?(gemfile)
2671
+
2672
+ parsed = Kettle::Dev::VersionBump.parse_source(File.read(gemfile), gemfile)
2673
+ Kettle::Dev::VersionBump.each_node(parsed.value).any? do |node|
2674
+ node.is_a?(Prism::CallNode) && node.name == :gem &&
2675
+ node.arguments&.arguments&.first.is_a?(Prism::StringNode) &&
2676
+ node.arguments.arguments.first.unescaped == gem_name
2677
+ end
2678
+ end
2679
+
2621
2680
  def template_prepare_lockfiles_phase?(phase)
2622
2681
  command == "template" && phase == "prepare_lockfiles"
2623
2682
  end
@@ -2666,6 +2725,11 @@ module Kettle
2666
2725
  memo: memo,
2667
2726
  phase: "prepare_template_dependencies_recovery"
2668
2727
  )
2728
+ if no_release_lockfiles_failure?(memo.last)
2729
+ memo.pop
2730
+ memo << result
2731
+ return false
2732
+ end
2669
2733
  return false unless memo.last&.ok?
2670
2734
 
2671
2735
  result = runner.call(
@@ -2679,6 +2743,10 @@ module Kettle
2679
2743
  result.ok?
2680
2744
  end
2681
2745
 
2746
+ def no_release_lockfiles_failure?(result)
2747
+ result && !result.ok? && [result.stdout, result.stderr].join("\n").include?("kettle-reset: no release lockfiles found")
2748
+ end
2749
+
2682
2750
  def commit_template_changes(member:, runner:, memo:)
2683
2751
  return unless deferred_monorepo_template_commit?(member)
2684
2752
  return unless memo.all?(&:ok?)
data/lib/kettle/family.rb CHANGED
@@ -13,9 +13,11 @@ require_relative "family/config"
13
13
  require_relative "family/secrets"
14
14
  require_relative "family/branch_target_config"
15
15
  require_relative "family/release_state_check"
16
+ require_relative "family/release_reconciler"
16
17
  require_relative "family/unreleased_gem_cleanup"
17
18
  require_relative "family/local_install"
18
19
  require_relative "family/nomono_bootstrap"
20
+ require_relative "family/debugger_bootstrap"
19
21
  require_relative "family/release_waves"
20
22
  require_relative "family/workflow"
21
23
  require_relative "family/discovery"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-family
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.20
4
+ version: 1.2.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -74,7 +74,7 @@ dependencies:
74
74
  version: '2.3'
75
75
  - - ">="
76
76
  - !ruby/object:Gem::Version
77
- version: 2.5.13
77
+ version: 2.5.15
78
78
  type: :runtime
79
79
  prerelease: false
80
80
  version_requirements: !ruby/object:Gem::Requirement
@@ -84,7 +84,7 @@ dependencies:
84
84
  version: '2.3'
85
85
  - - ">="
86
86
  - !ruby/object:Gem::Version
87
- version: 2.5.13
87
+ version: 2.5.15
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: kettle-test
90
90
  requirement: !ruby/object:Gem::Requirement
@@ -316,6 +316,7 @@ files:
316
316
  - lib/kettle/family/command_result.rb
317
317
  - lib/kettle/family/command_runner.rb
318
318
  - lib/kettle/family/config.rb
319
+ - lib/kettle/family/debugger_bootstrap.rb
319
320
  - lib/kettle/family/dependency_floor.rb
320
321
  - lib/kettle/family/discovery.rb
321
322
  - lib/kettle/family/git_status.rb
@@ -324,6 +325,7 @@ files:
324
325
  - lib/kettle/family/nomono_bootstrap.rb
325
326
  - lib/kettle/family/orderer.rb
326
327
  - lib/kettle/family/readiness_check.rb
328
+ - lib/kettle/family/release_reconciler.rb
327
329
  - lib/kettle/family/release_state_check.rb
328
330
  - lib/kettle/family/release_state_result.rb
329
331
  - lib/kettle/family/release_waves.rb
@@ -342,10 +344,10 @@ licenses:
342
344
  - AGPL-3.0-only
343
345
  metadata:
344
346
  homepage_uri: https://kettle-family.galtzo.com
345
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.20
346
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.20/CHANGELOG.md
347
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.21
348
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.21/CHANGELOG.md
347
349
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
348
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.20
350
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.21
349
351
  funding_uri: https://github.com/sponsors/pboling
350
352
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
351
353
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file