kettle-family 1.2.12 → 1.2.13

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: 8ba0934fc9a3cb13405a2d6546e64f6542df7972983eb22b881981c71fde5af4
4
- data.tar.gz: dca77905dae036ee7d9ea4a71c6d52fd9ebc7689f4981005bc9b317f278c2d2a
3
+ metadata.gz: 14997d7ae5a4ba5f2e3ec46f9fe9925a15020a0549fc779d70c57171492edd57
4
+ data.tar.gz: e9e84f2d92c1b8e2434ddc01a2d275e4b9077b06644b854ec817893a7e4157f1
5
5
  SHA512:
6
- metadata.gz: a8652fa19d9f51210fd3eef7d206f7299db43e05688f8e1e09b0d5acc6b05969efa1303e4e47ae9fd98cfb31391b5ea1ab48ce506181ae577f4db88e534988f6
7
- data.tar.gz: 1f1bb5c5677b5d870114678d5ab569f238e2e00d4ab0c5d713801135bdfd40941596dcd356313ad974c05967b9fe47e47b34c88583b02e2631c36b37c31e60f6
6
+ metadata.gz: aac3fc439504f17be4685f87f7bd1d4c213f885941f5c7793f1bca2369e92c384c7ae3cc8919fbf8f1023e7d1d812abe29c76d0a7fbef0301a5a8125b42217f3
7
+ data.tar.gz: 9d4ad335a463968aad4643d36b616f37b5d99b1431c659f2f3ae329017f304e5115dff3be3be72829ca99744a8cdf4b81619949146ce1df6f6ae7516226a12e6
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,73 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.2.13] - 2026-07-31
34
+
35
+ - TAG: [v1.2.13][1.2.13t]
36
+ - COVERAGE: 94.09% -- 4043/4297 lines in 28 files
37
+ - BRANCH COVERAGE: 76.48% -- 1538/2011 branches in 28 files
38
+ - 28.84% documented
39
+
40
+ ### Fixed
41
+
42
+ - Family publish runs now keep release secrets provider interaction in
43
+ `kettle-family` by default, so `--secrets-provider` no longer gets passed down
44
+ to child `kettle-release` commands unless the configured publish command
45
+ explicitly includes it.
46
+
47
+ - Family release progress now renders child `kettle-release` `secret_provider`
48
+ events, keeping keepalive and prompt-response activity visible when a release
49
+ command emits NDJSON.
50
+
51
+ - `kettle-family state` now queries `kettle-jem` transfer changelog totals
52
+ outside the active bundle when the root bundle does not include `kettle-jem`,
53
+ so the transfer lag column header can show the real total instead of `T(?)`.
54
+
55
+ - `kettle-family` local development bundles now localize `kettle-dev`'s direct
56
+ sibling runtime dependencies when `KETTLE_DEV_DEV` is enabled, so unreleased
57
+ floors such as `kettle-ndjson >= 0.1.4` resolve during family development.
58
+
59
+ - Release dry-runs now stop after planned lockfile normalization when a member
60
+ has local path remotes, avoiding a false readiness failure on lockfiles that
61
+ would be normalized under `--execute`.
62
+
63
+ - Release state now reads kettle-jem transfer changelog replay cursors from
64
+ `.structuredmerge/kettle-jem.lock`, so `T(n)` reports actual lag instead of
65
+ treating every member as missing all transfer changelogs.
66
+
67
+ - `kettle-family state` now labels the transfer changelog column as `T(n)`,
68
+ where `n` is the total transfer changelog count and row values are the lag
69
+ after each member's stored replay cursor.
70
+
71
+ - `kettle-family state` now uses an ASCII `^ / v` ahead/behind column header so
72
+ terminal table alignment is stable in monospace output.
73
+
74
+ - Text reports now repeat the command context in the final summary footer so the
75
+ command, family, mode, config, order, and release target details remain visible
76
+ after long runs.
77
+
78
+ - Failed streamed release commands now report the last useful output line when
79
+ no explicit failure marker is present, avoiding empty `output omitted`
80
+ summaries for Bundler boot failures.
81
+
82
+ - Template execution now aligns stale managed `nomono` Gemfile floors and
83
+ lockfiles before member bundles run, avoiding already-activated `nomono`
84
+ conflicts during family templating.
85
+
86
+ - `kettle-family` now loads `kettle-dev` through its public entrypoint instead
87
+ of direct nested implementation files.
88
+
89
+ - Command summaries now include total elapsed wall time in the final footer.
90
+
91
+ - Parent-owned RubyGems MFA lookup failures during release now fall back to
92
+ manual OTP entry when possible, and otherwise fail the member cleanly instead
93
+ of escaping from a worker thread with a misleading release summary.
94
+
95
+ - Dependency-floor CI bundle validation now recognizes workflow `env:
96
+ BUNDLE_GEMFILE` entries, including `${{ github.workspace }}` paths, so
97
+ release waves wait for just-published floors before pushing dependent CI
98
+ workflows that install through setup-ruby-flash.
99
+
33
100
  ## [1.2.12] - 2026-07-30
34
101
 
35
102
  - TAG: [v1.2.12][1.2.12t]
@@ -1376,7 +1443,9 @@ Please file a bug if you notice a violation of semantic versioning.
1376
1443
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
1377
1444
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
1378
1445
 
1379
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.12...HEAD
1446
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.13...HEAD
1447
+ [1.2.13]: https://github.com/kettle-dev/kettle-family/compare/v1.2.12...v1.2.13
1448
+ [1.2.13t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.13
1380
1449
  [1.2.12]: https://github.com/kettle-dev/kettle-family/compare/v1.2.11...v1.2.12
1381
1450
  [1.2.12t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.12
1382
1451
  [1.2.11]: https://github.com/kettle-dev/kettle-family/compare/v1.2.10...v1.2.11
data/README.md CHANGED
@@ -309,9 +309,11 @@ has a provider configured. Family publish runs pass `--yes` to child
309
309
  `kettle-release` commands by default, making release confirmation approval
310
310
  explicit instead of relying on terminal prompt detection. Pass `--no-accept` to
311
311
  leave child release confirmation prompts interactive. When publishing through
312
- `kettle-release`, `kettle-family` loads the signing passphrase once and passes
313
- the cached value to each child process; `kettle-release` still fetches RubyGems
314
- OTP values directly from 1Password at prompt time.
312
+ `kettle-release`, `kettle-family` owns release secret prompts by default: it
313
+ loads the signing passphrase once, watches child release prompts, and fetches
314
+ RubyGems OTP values from 1Password when each MFA prompt arrives. A child
315
+ `kettle-release` command owns its own secrets provider only when the configured
316
+ publish command explicitly includes `--secrets-provider`.
315
317
 
316
318
  ## 🔧 Basic Usage
317
319
 
@@ -331,12 +333,14 @@ kettle-family state
331
333
 
332
334
  `state` is an alias for `release-state`. The release-state report lists each
333
335
  gem's current `version.rb`, latest versioned `CHANGELOG.md` section, latest
334
- published RubyGems release, latest GitHub release tag, transfer-changelog lag,
336
+ published RubyGems release, latest GitHub release tag, transfer-changelog count,
335
337
  whether pending changelog work exists in either `Unreleased` or an unpublished
336
338
  prepared release section, and how many commits the current branch is ahead of or
337
339
  behind the latest release tag. The version columns are ordered as `V.rb`,
338
- `V.ch.md`, `V.rel`, and `GH.rel`; `GH.rel` is prefixed with a red circle when it
339
- does not match `V.rel`.
340
+ `V.ch.md`, `V.rel`, `GH.rel`, and `T(n)`; `GH.rel` is prefixed with a red circle
341
+ when it does not match `V.rel`. The `T(n)` header shows the total number of
342
+ kettle-jem transfer changelog entries. Each row value in that column shows how
343
+ many entries are after that member's stored replay cursor.
340
344
 
341
345
  Use release-state tokens with `--only` to select gems by state instead of by
342
346
  name. Multiple tokens are combined with logical AND, so this selects only gems
@@ -405,10 +409,11 @@ prompts.
405
409
  Executed publish runs can opt in to the local 1Password CLI for unattended
406
410
  release credentials. The gem signing passphrase is loaded once and cached only
407
411
  in memory for the current `kettle-family` process. When the publish command is
408
- `kettle-release`, that cached value is passed through an internal environment
409
- integration so child releases do not query 1Password repeatedly for the same
410
- passphrase. RubyGems OTP values are loaded by `kettle-release` when each MFA
411
- prompt arrives, so long releases do not reuse a stale TOTP from release startup.
412
+ `kettle-release`, `kettle-family` watches the child release process and answers
413
+ signing and RubyGems MFA prompts itself, so long releases do not reuse a stale
414
+ TOTP from release startup. If a publish command explicitly includes
415
+ `--secrets-provider`, that child `kettle-release` owns its own provider
416
+ interaction instead.
412
417
 
413
418
  ```yaml
414
419
  release:
@@ -704,8 +709,8 @@ Thanks for RTFM. ☺️
704
709
  [✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord
705
710
  [✉️ruby-friends-img]: https://img.shields.io/badge/daily.dev-%F0%9F%92%8E_Ruby_Friends-0A0A0A?style=for-the-badge&logo=dailydotdev&logoColor=white
706
711
  [✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends
707
- [✉️ruby-forum-top-img]: https://img.shields.io/discourse/topics?server=https://www.rubyforum.org&style=flat&logo=discourse&label=Ruby%20Users%20Forum
708
- [✉️ruby-forum-img]: https://img.shields.io/discourse/topics?server=https://www.rubyforum.org&style=for-the-badge&logo=discourse&label=Ruby%20Users%20Forum
712
+ [✉️ruby-forum-top-img]: https://img.shields.io/discourse/topics?server=https%3A%2F%2Fwww.rubyforum.org&style=flat&logo=discourse&label=Ruby%20Users%20Forum
713
+ [✉️ruby-forum-img]: https://img.shields.io/discourse/topics?server=https%3A%2F%2Fwww.rubyforum.org&style=for-the-badge&logo=discourse&label=Ruby%20Users%20Forum
709
714
  [✉️ruby-forum]: https://www.rubyforum.org/tag/kettle-dev
710
715
 
711
716
  [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
@@ -814,7 +819,7 @@ Thanks for RTFM. ☺️
814
819
  [📌gitmoji]: https://gitmoji.dev
815
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
816
821
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
817
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.096-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
822
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.297-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
818
823
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
819
824
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
820
825
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -591,7 +591,9 @@ module Kettle
591
591
  selected = Selection.new(members: ordered, release_state_results: release_state_results).apply(only: effective_only, exclude: options[:exclude], start_at: start_at.member)
592
592
  result_members = selected
593
593
  print_execution_intent(command: command, config: config, members: result_members, options: options, start_at: start_at)
594
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
594
595
  results = command_results(command: command, config: config, members: result_members, options: options, start_at: start_at)
596
+ elapsed_seconds = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
595
597
  Report.new(
596
598
  family_name: config.family_name,
597
599
  family_mode: config.family_mode,
@@ -605,7 +607,8 @@ module Kettle
605
607
  release_mode: release_mode(command: command, options: options),
606
608
  command: command,
607
609
  results: results,
608
- warnings: discovery.warnings
610
+ warnings: discovery.warnings,
611
+ elapsed_seconds: elapsed_seconds
609
612
  )
610
613
  end
611
614
 
@@ -95,7 +95,7 @@ module Kettle
95
95
  end
96
96
 
97
97
  def read_response(chunk:)
98
- provided = secrets_provider&.rubygems_otp.to_s
98
+ provided = read_provider_response
99
99
  unless provided.empty?
100
100
  @output.puts("RubyGems MFA code loaded from configured secrets provider.")
101
101
  return provided
@@ -115,7 +115,26 @@ module Kettle
115
115
  def otp_prompt_label(chunk)
116
116
  chunk.to_s.lines.last&.strip.to_s.empty? ? "Code:" : chunk.to_s.lines.last.strip
117
117
  end
118
+
119
+ def read_provider_response
120
+ return "" unless secrets_provider
121
+
122
+ @output.print("\a") if @output.respond_to?(:print)
123
+ secrets_provider.rubygems_otp.to_s
124
+ rescue Error => error
125
+ raise unless @input.respond_to?(:tty?) && @input.tty?
126
+
127
+ @output.puts("#{error.message}; falling back to manual OTP entry.")
128
+ ""
129
+ end
130
+ end
131
+
132
+ FailureStatus = Struct.new(:exitstatus) do
133
+ def success?
134
+ false
135
+ end
118
136
  end
137
+ private_constant :FailureStatus
119
138
 
120
139
  def initialize(execute: false, accept: true, gem_signing_password: nil, otp_coordinator: nil)
121
140
  @execute = execute
@@ -227,6 +246,7 @@ module Kettle
227
246
 
228
247
  def run_interactive_pty(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
229
248
  stdout = +""
249
+ stderr = +""
230
250
  stdout_line_buffer = +""
231
251
  status = nil
232
252
  PTY.spawn(env, *argv, chdir: chdir, **process_options) do |output, input, pid|
@@ -248,11 +268,15 @@ module Kettle
248
268
  end
249
269
  rescue Errno::EIO
250
270
  # PTY raises EIO when the child process exits after closing the slave.
271
+ rescue Error => error
272
+ stderr << "#{error.message}\n"
273
+ terminate_process(pid)
251
274
  end
252
275
  flush_interactive_stdout(stdout_line_buffer, stdout_line_handler)
253
276
  _, status = Process.wait2(pid)
277
+ status = failure_status if !stderr.empty? && status.success?
254
278
  end
255
- [stdout, "", status]
279
+ [stdout, stderr, status]
256
280
  end
257
281
 
258
282
  def run_interactive_open3(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
@@ -263,28 +287,35 @@ module Kettle
263
287
  Open3.popen3(env, *argv, chdir: chdir, **process_options) do |input, output, error, wait_thread|
264
288
  readers = [output, error]
265
289
  readers << $stdin if $stdin.tty? && !otp_coordinator
266
- until readers.empty?
267
- ready = IO.select(readers)
268
- ready.first.each do |reader|
269
- if reader.equal?($stdin)
270
- input.write($stdin.readpartial(1024))
271
- else
272
- stdout_line_buffer = read_interactive_stream(
273
- reader,
274
- output,
275
- input,
276
- captured_stdout,
277
- captured_stderr,
278
- readers,
279
- member_name: member_name,
280
- stdout_line_buffer: stdout_line_buffer,
281
- stdout_line_handler: stdout_line_handler
282
- )
290
+ begin
291
+ until readers.empty?
292
+ ready = IO.select(readers)
293
+ ready.first.each do |reader|
294
+ if reader.equal?($stdin)
295
+ input.write($stdin.readpartial(1024))
296
+ else
297
+ stdout_line_buffer = read_interactive_stream(
298
+ reader,
299
+ output,
300
+ input,
301
+ captured_stdout,
302
+ captured_stderr,
303
+ readers,
304
+ member_name: member_name,
305
+ stdout_line_buffer: stdout_line_buffer,
306
+ stdout_line_handler: stdout_line_handler
307
+ )
308
+ end
283
309
  end
284
310
  end
311
+ rescue Error => error
312
+ captured_stderr << "#{error.message}\n"
313
+ terminate_process(wait_thread.pid)
314
+ status = failure_status
315
+ else
316
+ status = wait_thread.value
285
317
  end
286
318
  flush_interactive_stdout(stdout_line_buffer, stdout_line_handler)
287
- status = wait_thread.value
288
319
  end
289
320
  [captured_stdout, captured_stderr, status]
290
321
  end
@@ -415,6 +446,16 @@ module Kettle
415
446
  output.to_s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
416
447
  end
417
448
 
449
+ def failure_status
450
+ FailureStatus.new(1)
451
+ end
452
+
453
+ def terminate_process(pid)
454
+ Process.kill("TERM", pid)
455
+ rescue Errno::ESRCH, Errno::EPERM
456
+ nil
457
+ end
458
+
418
459
  def command_argv(member:, command:, env: {})
419
460
  argv = normalize_command(command)
420
461
  return argv unless mise_configured?(member)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "kettle/dev"
5
+
6
+ module Kettle
7
+ module Family
8
+ class NomonoBootstrap
9
+ GEM_NAME = "nomono"
10
+
11
+ def initialize(latest_version:, mode: :dry_run)
12
+ @latest_version = Gem::Version.new(latest_version.to_s)
13
+ @mode = mode
14
+ end
15
+
16
+ attr_reader :latest_version, :mode
17
+
18
+ def member_needs_bootstrap?(member)
19
+ !!(gemfile_floor_outdated?(member) || lockfile_version_outdated?(member))
20
+ end
21
+
22
+ def bootstrap_member(member)
23
+ edits = gemfile_edits(member)
24
+ write_edits(edits)
25
+ CommandResult.new(
26
+ member.name,
27
+ "template_bootstrap_dependencies",
28
+ ["internal", "nomono-bootstrap", latest_version.to_s],
29
+ member.root,
30
+ 0,
31
+ true,
32
+ bootstrap_stdout(member, edits),
33
+ "",
34
+ 0.0,
35
+ mode != :execute,
36
+ (mode == :execute) ? nil : "dry run"
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def bootstrap_stdout(member, edits)
43
+ lines = ["updated nomono bootstrap dependency for #{member.name}"]
44
+ lines.concat(edits.map { |edit| "updated #{edit.fetch(:path)}" })
45
+ locked_version = lockfile_nomono_version(member)
46
+ if locked_version && locked_version < latest_version
47
+ lines << "lockfile nomono #{locked_version} is below #{latest_version}"
48
+ end
49
+ "#{lines.join("\n")}\n"
50
+ end
51
+
52
+ def write_edits(edits)
53
+ return if mode != :execute || edits.empty?
54
+
55
+ Kettle::Dev::VersionBump.write_edits(edits)
56
+ end
57
+
58
+ def gemfile_floor_outdated?(member)
59
+ gemfile_edits(member).any?
60
+ end
61
+
62
+ def lockfile_version_outdated?(member)
63
+ locked_version = lockfile_nomono_version(member)
64
+ locked_version && locked_version < latest_version
65
+ end
66
+
67
+ def lockfile_nomono_version(member)
68
+ lockfile = File.join(member.root, "Gemfile.lock")
69
+ return nil unless File.file?(lockfile)
70
+
71
+ File.readlines(lockfile).each do |line|
72
+ match = line.match(/\A {4}nomono \(([^)]+)\)\s*\z/)
73
+ return Gem::Version.new(match[1]) if match
74
+ end
75
+ nil
76
+ end
77
+
78
+ def gemfile_edits(member)
79
+ gemfile = File.join(member.root, "Gemfile")
80
+ return [] unless File.file?(gemfile)
81
+
82
+ source = File.read(gemfile)
83
+ parse_result = Kettle::Dev::VersionBump.parse_source(source, gemfile)
84
+ Kettle::Dev::VersionBump.each_node(parse_result.value).filter_map do |node|
85
+ nomono_floor_edit(gemfile, source, node)
86
+ end
87
+ end
88
+
89
+ def nomono_floor_edit(path, source, node)
90
+ return unless node.is_a?(Prism::CallNode)
91
+ return unless node.name == :gem
92
+
93
+ arguments = node.arguments&.arguments || []
94
+ gem_name_node = arguments.first
95
+ return unless gem_name_node.is_a?(Prism::StringNode)
96
+ return unless gem_name_node.unescaped == GEM_NAME
97
+
98
+ floor_node = arguments.drop(1).find do |argument|
99
+ argument.is_a?(Prism::StringNode) && argument.unescaped.start_with?(">= ")
100
+ end
101
+ return unless floor_node
102
+
103
+ current_floor = Gem::Version.new(floor_node.unescaped.sub(/\A>=\s*/, ""))
104
+ return unless current_floor < latest_version
105
+
106
+ replacement = Kettle::Dev::VersionBump.quote_like(floor_node.location.slice, ">= #{latest_version}")
107
+ Kettle::Dev::VersionBump.file_edit(
108
+ path,
109
+ source,
110
+ floor_node.location.start_offset,
111
+ floor_node.location.end_offset,
112
+ replacement
113
+ )
114
+ end
115
+ end
116
+ end
117
+ end
@@ -11,13 +11,20 @@ require "yaml"
11
11
  module Kettle
12
12
  module Family
13
13
  class ReleaseStateCheck
14
- KETTLE_JEM_CONFIG_PATHS = [".structuredmerge/kettle-jem.yml", ".kettle-jem.yml"].freeze
14
+ KETTLE_JEM_STATE_PATHS = [
15
+ ".structuredmerge/kettle-jem.lock",
16
+ ".kettle-jem.lock",
17
+ ".structuredmerge/kettle-jem.yml",
18
+ ".kettle-jem.yml"
19
+ ].freeze
20
+ TRANSFER_CHANGELOG_TOTAL_UNSET = Object.new.freeze
15
21
 
16
22
  def initialize(members:, config: nil)
17
23
  @members = members
18
24
  @config = config
19
25
  @github_latest_release_by_repo = {}
20
26
  @transfer_changelog_lag_by_key = {}
27
+ @transfer_changelog_total_count = TRANSFER_CHANGELOG_TOTAL_UNSET
21
28
  end
22
29
 
23
30
  def results
@@ -329,20 +336,29 @@ module Kettle
329
336
  lag = transfer_changelog_lag(last_entry_key)
330
337
  return state unless lag
331
338
 
332
- state.merge("transfer_changelog_lag" => lag.fetch("missing_count", 0).to_i)
339
+ state.merge(
340
+ "transfer_changelog_lag" => lag.fetch("missing_count", 0).to_i,
341
+ "transfer_changelog_total" => transfer_changelog_total_count
342
+ )
333
343
  end
334
344
 
335
345
  def kettle_jem_config_state(root)
336
- path = KETTLE_JEM_CONFIG_PATHS.map { |relative| File.join(root.to_s, relative) }.find { |candidate| File.file?(candidate) }
346
+ path = KETTLE_JEM_STATE_PATHS.map { |relative| File.join(root.to_s, relative) }.find { |candidate| File.file?(candidate) }
337
347
  return nil unless path
338
348
 
339
349
  data = YAML.safe_load_file(path, permitted_classes: [], aliases: false)
340
- state = data.is_a?(Hash) ? data["kettle-jem"] : nil
350
+ state = kettle_jem_state_from_yaml(data)
341
351
  state.is_a?(Hash) ? state : {}
342
352
  rescue Psych::Exception, SystemCallError
343
353
  nil
344
354
  end
345
355
 
356
+ def kettle_jem_state_from_yaml(data)
357
+ return nil unless data.is_a?(Hash)
358
+
359
+ data["template_state"] || data["kettle-jem"]
360
+ end
361
+
346
362
  def transfer_changelog_lag(last_entry_key)
347
363
  cache_key = last_entry_key.to_s
348
364
  @transfer_changelog_lag_by_key.fetch(cache_key) do
@@ -352,6 +368,13 @@ module Kettle
352
368
  nil
353
369
  end
354
370
 
371
+ def transfer_changelog_total_count
372
+ return @transfer_changelog_total_count unless @transfer_changelog_total_count.equal?(TRANSFER_CHANGELOG_TOTAL_UNSET)
373
+
374
+ total_lag = transfer_changelog_lag(nil)
375
+ @transfer_changelog_total_count = total_lag ? total_lag.fetch("missing_count", 0).to_i : nil
376
+ end
377
+
355
378
  def in_process_transfer_changelog_lag(last_entry_key)
356
379
  return nil unless load_kettle_jem_transfer_api
357
380
 
@@ -375,7 +398,12 @@ module Kettle
375
398
  last_entry_key = nil if last_entry_key.empty?
376
399
  puts JSON.generate(Kettle::Jem.transfer_changelog_lag(last_entry_key))
377
400
  RUBY
378
- stdout, _stderr, status = Open3.capture3(RbConfig.ruby, "-rjson", "-rkettle/jem", "-e", code, cache_key)
401
+ command = [RbConfig.ruby, "-rjson", "-rkettle/jem", "-e", code, cache_key]
402
+ stdout, _stderr, status = if defined?(::Bundler)
403
+ ::Bundler.with_unbundled_env { Open3.capture3(*command) }
404
+ else
405
+ Open3.capture3(*command)
406
+ end
379
407
  status.success? ? normalize_transfer_changelog_lag(JSON.parse(stdout)) : nil
380
408
  rescue JSON::ParserError, SystemCallError
381
409
  nil
@@ -31,7 +31,7 @@ module Kettle
31
31
 
32
32
  attr_reader :family_name, :family_mode, :order_mode, :members, :selected_members, :config_path, :command, :results, :branch_lanes, :release_target_branches, :member_release_target_branches, :release_mode, :warnings
33
33
 
34
- def initialize(family_name:, order_mode:, members:, selected_members:, config_path:, family_mode: nil, branch_lanes: {}, release_target_branches: [], member_release_target_branches: {}, release_mode: nil, command: nil, results: [], warnings: [])
34
+ def initialize(family_name:, order_mode:, members:, selected_members:, config_path:, family_mode: nil, branch_lanes: {}, release_target_branches: [], member_release_target_branches: {}, release_mode: nil, command: nil, results: [], warnings: [], elapsed_seconds: nil)
35
35
  @family_name = family_name
36
36
  @family_mode = family_mode
37
37
  @order_mode = order_mode
@@ -45,6 +45,7 @@ module Kettle
45
45
  @member_release_target_branches = member_release_target_branches
46
46
  @release_mode = release_mode
47
47
  @warnings = warnings
48
+ @elapsed_seconds = elapsed_seconds
48
49
  end
49
50
 
50
51
  def to_h
@@ -72,13 +73,7 @@ module Kettle
72
73
  end
73
74
 
74
75
  def to_text
75
- lines = ["kettle-family: #{Kettle::Family::VERSION}", "family: #{family_name}"]
76
- lines << "mode: #{family_mode}" if family_mode
77
- lines << "config: #{config_path || "none"}"
78
- lines << "order: #{order_mode}"
79
- lines << "command: #{command}" if command
80
- lines << "release mode: #{release_mode}" if release_mode
81
- lines << "release targets: #{release_target_branches.join(", ")}" unless release_target_branches.empty?
76
+ lines = report_context_lines
82
77
  append_member_release_targets(lines)
83
78
  append_warnings(lines)
84
79
  lines << "members:"
@@ -128,8 +123,17 @@ module Kettle
128
123
 
129
124
  def append_summary(lines)
130
125
  data = summary
126
+ lines << "context:"
127
+ report_context_lines.each { |line| lines << " #{line}" }
128
+ unless member_release_target_branches.empty?
129
+ lines << " member release targets:"
130
+ member_release_target_branches.each do |member_name, branches|
131
+ lines << " #{member_name}: #{branches.join(", ")}"
132
+ end
133
+ end
131
134
  lines << "summary:"
132
135
  lines << " outcome: #{data.fetch("outcome")}"
136
+ lines << " elapsed: #{format_elapsed(data.fetch("elapsed_seconds"))}"
133
137
  lines << " selected: #{data.fetch("selected_count")}"
134
138
  lines << " results: #{data.fetch("result_count")}"
135
139
  lines << " succeeded: #{summary_list(data.fetch("succeeded"))}"
@@ -139,9 +143,21 @@ module Kettle
139
143
  lines << " resume: #{data.fetch("resume_hint")}" if data.fetch("resume_hint")
140
144
  end
141
145
 
146
+ def report_context_lines
147
+ lines = ["kettle-family: #{Kettle::Family::VERSION}", "family: #{family_name}"]
148
+ lines << "mode: #{family_mode}" if family_mode
149
+ lines << "config: #{config_path || "none"}"
150
+ lines << "order: #{order_mode}"
151
+ lines << "command: #{command}" if command
152
+ lines << "release mode: #{release_mode}" if release_mode
153
+ lines << "release targets: #{release_target_branches.join(", ")}" unless release_target_branches.empty?
154
+ lines
155
+ end
156
+
142
157
  def summary
143
158
  {
144
159
  "outcome" => success? ? "success" : "failure",
160
+ "elapsed_seconds" => elapsed_seconds,
145
161
  "selected_count" => selected_members.length,
146
162
  "result_count" => visible_results.length,
147
163
  "succeeded" => summary_succeeded,
@@ -170,6 +186,20 @@ module Kettle
170
186
  results.reject { |result| release_wave_result?(result) }
171
187
  end
172
188
 
189
+ def elapsed_seconds
190
+ (@elapsed_seconds || visible_results.sum { |result| result.elapsed_seconds.to_f }).round(3)
191
+ end
192
+
193
+ def format_elapsed(seconds)
194
+ total = seconds.to_f.round
195
+ hours = total / 3600
196
+ minutes = (total % 3600) / 60
197
+ remaining_seconds = total % 60
198
+ return format("%d:%02d:%02d", hours, minutes, remaining_seconds) if hours.positive?
199
+
200
+ format("%02d:%02d", minutes, remaining_seconds)
201
+ end
202
+
173
203
  def append_indented_output(lines, output)
174
204
  output.to_s.each_line(chomp: true) { |line| lines << " #{line}" }
175
205
  end
@@ -213,9 +243,10 @@ module Kettle
213
243
  end
214
244
 
215
245
  def streamed_output_summary(result)
216
- streamed_output_lines(result).reverse.find do |line|
246
+ lines = streamed_output_lines(result)
247
+ lines.reverse.find do |line|
217
248
  line.match?(/(?:failed|failure|error|exited|aborted)/i) && !line.match?(/\A(?:Files|CI|Actions)/)
218
- end
249
+ end || lines.reverse.find { |line| !line.match?(/\A(?:Files|CI|Actions)/) }
219
250
  end
220
251
 
221
252
  def streamed_recommended_fix(result)
@@ -440,7 +471,8 @@ module Kettle
440
471
  lines << " prep: V.ch.md matches V.rb and is ready to publish"
441
472
  lines << " pend: unrel or prep"
442
473
  lines << " bump: unrel is yes and V.rb matches V.rel"
443
- lines << " T📰: kettle-jem transfer changelog entries not yet replayed"
474
+ lines << " count columns:"
475
+ lines << " T(n): kettle-jem transfer changelog lag; n is the total transfer changelog entry count with no replay cursor"
444
476
  rows = release_state_header
445
477
  results.each do |result|
446
478
  rows << release_state_row(result)
@@ -478,12 +510,20 @@ module Kettle
478
510
  end
479
511
 
480
512
  def release_state_header
481
- header = [["gem", "checkout", "V.rb", "V.ch.md", "V.rel", "GH.rel", "T📰", "🔼 / 🔽", "unrel", "prep", "pend", "bump"]]
513
+ header = [["gem", "checkout", "V.rb", "V.ch.md", "V.rel", "GH.rel", release_state_transfer_changelog_header, "^ / v", "unrel", "prep", "pend", "bump"]]
482
514
  return header unless release_state_has_branches?
483
515
 
484
516
  [["branch", *header.first]]
485
517
  end
486
518
 
519
+ def release_state_transfer_changelog_header
520
+ total = results.filter_map do |result|
521
+ value = (result.state || {}).fetch("transfer_changelog_total", nil)
522
+ value unless value.to_s.empty?
523
+ end.first
524
+ "T(#{total || "?"})"
525
+ end
526
+
487
527
  def release_state_github_release(state)
488
528
  github_release = state.fetch("github_latest_release", nil).to_s
489
529
  return "unknown" if github_release.empty?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  begin
4
- require "kettle/dev/release_secrets"
4
+ require "kettle/dev"
5
5
  rescue LoadError
6
6
  require "open3"
7
7
  end
@@ -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.12"
8
+ VERSION = "1.2.13"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -9,7 +9,7 @@ require "pathname"
9
9
  require "rbconfig"
10
10
  require "shellwords"
11
11
  require "yaml"
12
- require "kettle/dev/ruby_gems_versions"
12
+ require "kettle/dev"
13
13
 
14
14
  require_relative "workflow_progress"
15
15
 
@@ -225,6 +225,9 @@ module Kettle
225
225
  end
226
226
 
227
227
  def template_member_workflow_results(workflow_members)
228
+ bootstrap_results = template_bootstrap_dependency_results(workflow_members)
229
+ return bootstrap_results unless bootstrap_results.all?(&:ok?)
230
+
228
231
  queue = Queue.new
229
232
  workflow_members.each_with_index { |member, index| queue << [index, member] }
230
233
  ordered_results = Array.new(workflow_members.length)
@@ -248,7 +251,85 @@ module Kettle
248
251
  end.each(&:join)
249
252
  flattened = ordered_results.compact.flatten
250
253
  emit_template_progress_summary(flattened, progress: template_progress)
251
- flattened
254
+ bootstrap_results + flattened
255
+ end
256
+
257
+ def template_bootstrap_dependency_results(workflow_members)
258
+ return [] if workflow_members.empty?
259
+
260
+ candidate_members = workflow_members.select { |member| nomono_bootstrap_candidate?(member) }
261
+ return [] if candidate_members.empty?
262
+
263
+ latest_nomono_version = latest_released_nomono_version
264
+ active_nomono_version = Gem.loaded_specs["nomono"]&.version
265
+ if active_nomono_version && active_nomono_version < latest_nomono_version
266
+ return [template_bootstrap_failure_result(
267
+ candidate_members.first,
268
+ "activated nomono #{active_nomono_version} is older than latest released #{latest_nomono_version}"
269
+ )]
270
+ end
271
+
272
+ bootstrap = NomonoBootstrap.new(latest_version: latest_nomono_version, mode: :execute)
273
+ runner = CommandRunner.new(execute: execute, accept: accept)
274
+ candidate_members.each_with_object([]) do |member, memo|
275
+ next unless bootstrap.member_needs_bootstrap?(member)
276
+
277
+ memo << bootstrap.bootstrap_member(member)
278
+ break memo unless memo.last.ok?
279
+
280
+ memo << runner.call(
281
+ member: member,
282
+ phase: "template_bootstrap_dependencies",
283
+ command: %w[bundle update nomono --bundler],
284
+ env: template_bootstrap_dependency_env(member)
285
+ )
286
+ break memo unless memo.last.ok?
287
+ end
288
+ rescue => error
289
+ [template_bootstrap_failure_result(workflow_members.first, error.message)]
290
+ end
291
+
292
+ def nomono_bootstrap_candidate?(member)
293
+ %w[Gemfile Gemfile.lock].any? do |basename|
294
+ path = File.join(member.root, basename)
295
+ File.file?(path) && File.read(path).include?(NomonoBootstrap::GEM_NAME)
296
+ end
297
+ end
298
+
299
+ def template_bootstrap_failure_result(member, message)
300
+ CommandResult.new(
301
+ member.name,
302
+ "template_bootstrap_dependencies",
303
+ ["internal", "nomono-bootstrap"],
304
+ member.root,
305
+ 1,
306
+ false,
307
+ "",
308
+ message,
309
+ 0.0,
310
+ false,
311
+ "command failed"
312
+ )
313
+ end
314
+
315
+ def latest_released_nomono_version
316
+ versions = Kettle::Dev::RubyGemsVersions.fetch(NomonoBootstrap::GEM_NAME).filter_map do |entry|
317
+ number = entry["number"] || entry[:number]
318
+ next if number.to_s.empty?
319
+
320
+ version = Gem::Version.new(number)
321
+ version unless version.prerelease?
322
+ end
323
+ raise Error, "could not determine latest released nomono version" if versions.empty?
324
+
325
+ versions.max
326
+ end
327
+
328
+ def template_bootstrap_dependency_env(member)
329
+ env = workflow_env.merge(release_lockfile_local_path_env_overrides(member))
330
+ family_env_name = config.family_local_path_env_name
331
+ env[family_env_name] = "false" if family_env_name && !env_overrides.key?(family_env_name)
332
+ env
252
333
  end
253
334
 
254
335
  def template_results_for_member(member, progress: nil)
@@ -584,6 +665,12 @@ module Kettle
584
665
  end
585
666
  rescue ThreadError
586
667
  break
668
+ rescue => error
669
+ mutex.synchronize do
670
+ ordered_results[index] = [release_worker_error_result(member, error)]
671
+ stop = true
672
+ end
673
+ break
587
674
  end
588
675
  end
589
676
  end.each(&:join)
@@ -606,6 +693,22 @@ module Kettle
606
693
  )
607
694
  end
608
695
 
696
+ def release_worker_error_result(member, error)
697
+ CommandResult.new(
698
+ member_name: member.name,
699
+ phase: release_phase,
700
+ command: ["internal", "release-worker"],
701
+ workdir: member.root,
702
+ status: 1,
703
+ success: false,
704
+ stdout: "",
705
+ stderr: "#{error.class}: #{error.message}\n",
706
+ elapsed_seconds: 0.0,
707
+ skipped: false,
708
+ reason: error.message
709
+ )
710
+ end
711
+
609
712
  def release_results_for_member(member, runner:)
610
713
  progress = @release_progress
611
714
  progress&.start_member(member, total: release_phase_total, status: "check")
@@ -624,6 +727,12 @@ module Kettle
624
727
  commit_normalized_lockfiles(branch_members: [member], runner: runner, memo: memo, reason: "release", force: true)
625
728
  emit_member_result_progress(member, memo.last, progress: progress)
626
729
  return memo unless memo.last&.ok?
730
+
731
+ if !execute && release_lockfile_readiness_would_fail?(member)
732
+ memo << release_skipped_after_lockfile_normalization_result(member)
733
+ emit_member_result_progress(member, memo.last, progress: progress)
734
+ return memo
735
+ end
627
736
  end
628
737
 
629
738
  append_release_internal_checks(member: member, memo: memo)
@@ -675,8 +784,8 @@ module Kettle
675
784
  CommandRunner.new(
676
785
  execute: execute,
677
786
  accept: release_command_uses_kettle_release_yes? ? false : accept,
678
- gem_signing_password: release_command_uses_kettle_release_secrets? ? nil : @gem_signing_password,
679
- otp_coordinator: release_command_uses_kettle_release_secrets? ? nil : release_otp_coordinator
787
+ gem_signing_password: release_command_delegates_secrets_to_kettle_release? ? nil : @gem_signing_password,
788
+ otp_coordinator: release_command_delegates_secrets_to_kettle_release? ? nil : release_otp_coordinator
680
789
  )
681
790
  end
682
791
 
@@ -865,14 +974,17 @@ module Kettle
865
974
  def workflow_bundle_gemfiles(member)
866
975
  Dir.glob(File.join(member.root, ".github", "workflows", "*.{yml,yaml}")).flat_map do |path|
867
976
  workflow_bundle_gemfile_entries(path).map do |entry|
868
- File.expand_path(entry, member.root)
977
+ File.expand_path(normalize_workflow_workspace_path(entry), member.root)
869
978
  end
870
979
  end
871
980
  end
872
981
 
873
982
  def workflow_bundle_gemfile_entries(path)
874
983
  data = YAML.safe_load_file(path, permitted_classes: [], permitted_symbols: [], aliases: true) || {}
875
- recursive_values_for_key(data, "bundle_gemfile").map(&:to_s).reject(&:empty?)
984
+ [
985
+ *recursive_values_for_key(data, "bundle_gemfile"),
986
+ *recursive_env_values_for_key(data, "BUNDLE_GEMFILE")
987
+ ].map(&:to_s).reject(&:empty?)
876
988
  rescue Psych::Exception
877
989
  []
878
990
  end
@@ -891,6 +1003,22 @@ module Kettle
891
1003
  end
892
1004
  end
893
1005
 
1006
+ def recursive_env_values_for_key(value, key)
1007
+ case value
1008
+ when Hash
1009
+ matches = value.fetch("env", nil).is_a?(Hash) ? [value["env"][key]] : []
1010
+ matches.concat(value.flat_map { |_entry_key, entry_value| recursive_env_values_for_key(entry_value, key) })
1011
+ when Array
1012
+ value.flat_map { |entry| recursive_env_values_for_key(entry, key) }
1013
+ else
1014
+ []
1015
+ end
1016
+ end
1017
+
1018
+ def normalize_workflow_workspace_path(path)
1019
+ path.to_s.sub(%r{\A\$\{\{\s*github\.workspace\s*\}\}/?}, "").sub(%r{\A\$GITHUB_WORKSPACE/?}, "")
1020
+ end
1021
+
894
1022
  def relative_path_from_member_root(member:, path:)
895
1023
  Pathname.new(path).relative_path_from(Pathname.new(member.root)).to_s
896
1024
  rescue ArgumentError
@@ -1169,6 +1297,22 @@ module Kettle
1169
1297
  memo << ChangelogCheck.call(member: member, config: config) if memo.last.ok?
1170
1298
  end
1171
1299
 
1300
+ def release_skipped_after_lockfile_normalization_result(member)
1301
+ CommandResult.new(
1302
+ member_name: member.name,
1303
+ phase: release_phase,
1304
+ command: release_command,
1305
+ workdir: member.root,
1306
+ status: nil,
1307
+ success: true,
1308
+ stdout: "",
1309
+ stderr: "",
1310
+ elapsed_seconds: 0.0,
1311
+ skipped: true,
1312
+ reason: "dry-run; release readiness requires lockfile normalization"
1313
+ )
1314
+ end
1315
+
1172
1316
  def append_family_changelog_result(runner:, memo:)
1173
1317
  return unless config.release_family_changelog?
1174
1318
 
@@ -1264,7 +1408,6 @@ module Kettle
1264
1408
  args << "--local-ci" if local_ci
1265
1409
  args << "--skip-bundle-audit" if skip_bundle_audit
1266
1410
  args << "--skip-remotes=#{skip_remotes}" if skip_remotes && !skip_remotes.to_s.empty?
1267
- args << "--secrets-provider=1password" if release_command_uses_kettle_release_secrets? && !command_includes_arg?(command, "--secrets-provider")
1268
1411
  args << "--yes" if release_command_uses_kettle_release_yes? && !command_includes_arg?(command, "--yes")
1269
1412
  args << "--events" unless command_includes_arg?(command, "--events")
1270
1413
  return command if args.empty?
@@ -1322,7 +1465,7 @@ module Kettle
1322
1465
  end
1323
1466
 
1324
1467
  def kettle_release_secrets_env
1325
- return {} unless release_command_uses_kettle_release_secrets?
1468
+ return {} unless release_command_delegates_secrets_to_kettle_release?
1326
1469
 
1327
1470
  secrets_config = config.release_secrets
1328
1471
  env = {
@@ -1345,9 +1488,10 @@ module Kettle
1345
1488
  env
1346
1489
  end
1347
1490
 
1348
- def release_command_uses_kettle_release_secrets?
1491
+ def release_command_delegates_secrets_to_kettle_release?
1349
1492
  kettle_release_supports_direct_secrets? &&
1350
1493
  kettle_release_command?(raw_release_command) &&
1494
+ command_includes_arg?(raw_release_command, "--secrets-provider") &&
1351
1495
  release_secrets_provider_one_password?
1352
1496
  end
1353
1497
 
@@ -1945,6 +2089,8 @@ module Kettle
1945
2089
  when "command_step"
1946
2090
  label = [event["phase"], event["name"]].map(&:to_s).reject(&:empty?).join(":")
1947
2091
  emit_template_event_line(member, template_event_mark(event), label)
2092
+ when "secret_provider"
2093
+ emit_template_event_line(member, release_event_status_mark(event), secret_provider_event_label(event))
1948
2094
  when "diagnostic"
1949
2095
  emit_template_event_line(member, "!", diagnostic_event_label(event))
1950
2096
  when "summary"
@@ -1958,6 +2104,8 @@ module Kettle
1958
2104
  "release"
1959
2105
  when "command_step"
1960
2106
  [event["phase"], event["name"]].map(&:to_s).reject(&:empty?).join(":")
2107
+ when "secret_provider"
2108
+ secret_provider_event_label(event)
1961
2109
  when "diagnostic"
1962
2110
  diagnostic_event_label(event)
1963
2111
  when "summary"
@@ -1973,6 +2121,8 @@ module Kettle
1973
2121
  ">"
1974
2122
  when "command_step"
1975
2123
  template_event_mark(event)
2124
+ when "secret_provider"
2125
+ template_event_mark(event)
1976
2126
  when "diagnostic"
1977
2127
  "!"
1978
2128
  when "summary"
@@ -1995,6 +2145,19 @@ module Kettle
1995
2145
  end
1996
2146
  end
1997
2147
 
2148
+ def secret_provider_event_label(event)
2149
+ action = event["action"].to_s
2150
+ purpose = event["purpose"].to_s
2151
+ label = event["label"].to_s
2152
+ source = event["source"].to_s
2153
+ parts = ["secret"]
2154
+ parts << action unless action.empty?
2155
+ parts << purpose unless purpose.empty?
2156
+ parts << label unless label.empty?
2157
+ parts << source if purpose.empty? && label.empty? && !source.empty?
2158
+ parts.join(":")
2159
+ end
2160
+
1998
2161
  def diagnostic_event_label(event)
1999
2162
  message = event["message"]
2000
2163
  return message.to_s unless message.to_s.empty? || message.is_a?(Hash)
@@ -2240,6 +2403,11 @@ module Kettle
2240
2403
  config.release_normalize_lockfiles? || release_lockfile_has_local_path_remote?(member)
2241
2404
  end
2242
2405
 
2406
+ def release_lockfile_readiness_would_fail?(member)
2407
+ result = ReadinessCheck.call(member: member, config: config, allowed_local_path_roots: release_allowed_local_path_roots)
2408
+ result.stdout.lines.any? { |line| line.start_with?("release lockfile has local path remote at ") }
2409
+ end
2410
+
2243
2411
  def release_lockfile_has_local_path_remote?(member)
2244
2412
  lockfile = File.join(member.root, "Gemfile.lock")
2245
2413
  return false unless File.file?(lockfile)
data/lib/kettle/family.rb CHANGED
@@ -15,6 +15,7 @@ require_relative "family/branch_target_config"
15
15
  require_relative "family/release_state_check"
16
16
  require_relative "family/unreleased_gem_cleanup"
17
17
  require_relative "family/local_install"
18
+ require_relative "family/nomono_bootstrap"
18
19
  require_relative "family/release_waves"
19
20
  require_relative "family/workflow"
20
21
  require_relative "family/discovery"
data.tar.gz.sig CHANGED
@@ -1,2 +1 @@
1
- ����F社�M ֥�Z��� r3
2
- ]����.y]��L�f�a� W�{�k�r!�胆?Ouz�Aw�&�S�W�W��|��i�dv�-˯P�2�=lwN��.(����P=����皑=y>>ԌX��:j`)є�]�:�:����<��b JfO�K�{H)y�~�(�J���l�o��;K�>!]��xj��u�|/�m���L���=nX�~�}G�`Ž>j'0Ŕ���>�=CDC�nh`߲=~� ��\[I��l.���"���It���]´��⒆[�Ƭ�XA�(Z�.��v�
1
+ ��8�+J�AzфV랱��Q6W�Ky����= E.�������T2;;�c
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.12
4
+ version: 1.2.13
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.11
77
+ version: 2.5.12
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.11
87
+ version: 2.5.12
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: kettle-test
90
90
  requirement: !ruby/object:Gem::Requirement
@@ -321,6 +321,7 @@ files:
321
321
  - lib/kettle/family/git_status.rb
322
322
  - lib/kettle/family/local_install.rb
323
323
  - lib/kettle/family/member.rb
324
+ - lib/kettle/family/nomono_bootstrap.rb
324
325
  - lib/kettle/family/orderer.rb
325
326
  - lib/kettle/family/readiness_check.rb
326
327
  - lib/kettle/family/release_state_check.rb
@@ -341,10 +342,10 @@ licenses:
341
342
  - AGPL-3.0-only
342
343
  metadata:
343
344
  homepage_uri: https://kettle-family.galtzo.com
344
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.12
345
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.12/CHANGELOG.md
345
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.13
346
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.13/CHANGELOG.md
346
347
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
347
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.12
348
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.13
348
349
  funding_uri: https://github.com/sponsors/pboling
349
350
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
350
351
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file