kettle-family 1.3.28 → 1.3.30

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: 367d63bb92d37a073b9f7bfd89c81dd888abc3096ea0f40a38a84420676baa1b
4
- data.tar.gz: f90a8ff1e68e5906b016813c2b7f9b1bb863e22df3a217ac18aac97820b05b14
3
+ metadata.gz: e1834021ad7c719f89aee9e9c7408bb2fd772797bf85087620e913f2d0b5cbc5
4
+ data.tar.gz: abcb663c3eaffcebd396befa0443c3d5eb331cfdbcce6de417881cbda9e15679
5
5
  SHA512:
6
- metadata.gz: 2475a71506fa039212ababd06370f8fe2472147324f9d928e33fe5857e88bdf370fb63b24ffa70f08fe0d0d0bc632ac227bc146878160b26eae6d64f958010a3
7
- data.tar.gz: 7bb3b6df1a83fed732d8b6dcf43004efb5f1a00ff21dbd51ba084b7336292d4d1b6a3a2ffdbad5504e6a007265ec23fd8623fd9972c5a5cdb21aea994837f43c
6
+ metadata.gz: fd431e1b40c4c7b7119461eabb4cf55625a2ebea4f37e9c553ee96f3fb7f66af690441f063a33b2118a1d60ed7fdd48be98e9a6c85016cbbc106946206a152b8
7
+ data.tar.gz: b91b8eeb298ec02137d844df600e1ac2fe8ffefc5ff3c94fd055c8f10f57c162499deb1118a2c4034cd7ced3e2b0154b005255f2d95cdafe74744323ae9e6fb1
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,31 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [1.3.30] - 2026-10-22
34
+
35
+ - TAG: [v1.3.30][1.3.30t]
36
+ - COVERAGE: 93.05% -- 7526/8088 lines in 45 files
37
+ - BRANCH COVERAGE: 77.62% -- 3020/3891 branches in 45 files
38
+ - 38.45% documented
39
+
40
+ ### Fixed
41
+
42
+ - Family templating and mise-tasks now refresh the generated family task suite and migrate standard task definitions out of mise.toml, preserving custom tasks and family release settings. Generated lint/template commands execute their workflows, and dependency-task env wrappers use valid shell syntax.
43
+
44
+ - Family templating no longer crashes with Errno::ENOENT when the mise executable is unavailable to the spawned command environment: CommandRunner resolves mise to an absolute PATH-derived path for the `mise exec -C` wrapper prefix and the post-template family root trust step (spawned envs strip the orchestrator's Ruby/gem bin dirs from PATH), runs commands UNWRAPPED when mise cannot be resolved at all (no member runtime exists to activate), and skips the trust step when mise is absent (it only enables subsequent nested mise runs).
45
+ + Mise is now optional everywhere the template flow can reach: the new `Kettle::Family::MiseAdapter` seam resolves the binary at runtime, wraps member commands in `mise exec` when available, and passes them through UNWRAPPED (with `env(1)` injection preserved) when absent — no ENOENT on mise-less CI. The post-template family root trust step and worktree bootstraps skip when unresolvable. The adapter is injectable via `mise_adapter:` kwarg on `CommandRunner` and `Workflow`, with a module-level `Kettle::Family.mise_adapter` default for deep CLI construction. All eight `CommandRunner.new` sites, the family-root trust phase, `worktree_mise_trust_results`, and `CLI.mise_trust_results` delegate to it. Specs default to the nil flavor (hermetic, identical locally and in CI); examples asserting wrapped argv shapes opt into an available adapter locally.
46
+
47
+ ## [1.3.29] - 2026-10-21
48
+
49
+ - TAG: [v1.3.29][1.3.29t]
50
+ - COVERAGE: 93.07% -- 7464/8020 lines in 44 files
51
+ - BRANCH COVERAGE: 77.56% -- 2996/3863 branches in 44 files
52
+ - 37.45% documented
53
+
54
+ ### Fixed
55
+
56
+ - Release: the closing full-cohort aggregate GitHub Release now runs on execute runs, not only dry-runs. Per-member finalization appends only cover members published in the current run, so a resumed wave (--only on the unpublished remainder) previously never contributed artifacts published by earlier interrupted waves to the shared release. kettle-gh-release skips existing assets, so the full-cohort pass stays cheap on complete waves
57
+
33
58
  ## [1.3.28] - 2026-10-21
34
59
 
35
60
  - TAG: [v1.3.28][1.3.28t]
@@ -3189,7 +3214,11 @@ Please file a bug if you notice a violation of semantic versioning.
3189
3214
  - Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
3190
3215
  - Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
3191
3216
 
3192
- [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.28...HEAD
3217
+ [Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.30...HEAD
3218
+ [1.3.30]: https://github.com/kettle-dev/kettle-family/compare/v1.3.29...v1.3.30
3219
+ [1.3.30t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.30
3220
+ [1.3.29]: https://github.com/kettle-dev/kettle-family/compare/v1.3.28...v1.3.29
3221
+ [1.3.29t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.29
3193
3222
  [1.3.28]: https://github.com/kettle-dev/kettle-family/compare/v1.3.27...v1.3.28
3194
3223
  [1.3.28t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.28
3195
3224
  [1.3.27]: https://github.com/kettle-dev/kettle-family/compare/v1.3.26...v1.3.27
data/README.md CHANGED
@@ -1225,7 +1225,7 @@ Thanks for RTFM. ☺️
1225
1225
  [📌gitmoji]: https://gitmoji.dev
1226
1226
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1227
1227
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1228
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-8.019-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1228
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-8.088-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1229
1229
  [🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
1230
1230
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1231
1231
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -267,7 +267,7 @@ module Kettle
267
267
  class MiseTasks < BaseCommand
268
268
  command_name "mise-tasks"
269
269
  usage "[options]"
270
- description "Ensure the root mise.toml includes the kettle-family managed task file; warn on redefinitions."
270
+ description "Refresh managed family tasks and migrate standard root definitions; ensure the task-file include."
271
271
 
272
272
  option :task_file, long: "--task-file", value: {type: String, usage: "PATH"},
273
273
  desc: "Managed TOML task file name, relative to the root (default: mise-family.tasks.toml)"
@@ -288,6 +288,13 @@ module Kettle
288
288
  return 1
289
289
  end
290
290
 
291
+ if File.file?(File.join(root, ".kettle-family.yml"))
292
+ Kettle::Family::MiseTasks.materialize_managed_suite(
293
+ context: {project_root: root}, task_file_name: task_file_name
294
+ )
295
+ stdout.puts("refreshed managed family tasks and migrated standard root definitions")
296
+ end
297
+
291
298
  unless File.file?(task_file_path)
292
299
  baseline = File.join(__dir__, "templates", "mise-family.tasks.toml")
293
300
  FileUtils.cp(baseline, task_file_path)
@@ -1065,11 +1072,28 @@ module Kettle
1065
1072
  end
1066
1073
 
1067
1074
  def mise_trust_results(config:, members:)
1075
+ adapter = Kettle::Family.mise_adapter
1068
1076
  roots = [config.root, *members.map(&:root)].uniq
1069
1077
  runner = CommandRunner.new(execute: true, accept: true)
1070
1078
  roots.map.with_index do |root, index|
1071
1079
  member = members.find { |candidate| candidate.root == root } || Member.new("family", root, nil, nil, nil, [], [], nil, [], [])
1072
- runner.call(member: member, phase: "mise_trust", command: ["mise", "trust", "-C", root], env: {})
1080
+ trust_argv = adapter.trust_argv(root)
1081
+ unless trust_argv
1082
+ next CommandResult.new(
1083
+ member_name: member.name,
1084
+ phase: "mise_trust",
1085
+ command: [],
1086
+ workdir: root,
1087
+ status: nil,
1088
+ success: true,
1089
+ stdout: "",
1090
+ stderr: "",
1091
+ elapsed_seconds: 0.0,
1092
+ skipped: true,
1093
+ reason: "mise executable not available"
1094
+ )
1095
+ end
1096
+ runner.call(member: member, phase: "mise_trust", command: trust_argv, env: {}, raw: true)
1073
1097
  end
1074
1098
  end
1075
1099
 
@@ -11,17 +11,7 @@ module Kettle
11
11
  "KETTLE_RELEASE_GEM_SIGNING_PASSPHRASE"
12
12
  ].freeze
13
13
  MISE_SESSION_ENV_PREFIX = "__MISE_"
14
- WINDOWS_ENV_EXEC_SCRIPT = <<~RUBY
15
- unset_count = Integer(ARGV.shift)
16
- unset_count.times { ENV.delete(ARGV.shift) }
17
- assignment_count = Integer(ARGV.shift)
18
- assignment_count.times do
19
- key, value = ARGV.shift.split("=", 2)
20
- ENV[key] = value
21
- end
22
- abort "missing command after environment arguments" if ARGV.shift != "--"
23
- exec(*ARGV)
24
- RUBY
14
+ WINDOWS_ENV_EXEC_SCRIPT = MiseAdapter::WINDOWS_ENV_EXEC_SCRIPT
25
15
 
26
16
  class OtpCoordinator
27
17
  def initialize(input: $stdin, output: $stdout, queue_total: nil, secrets_provider: nil, event_handler: nil)
@@ -235,11 +225,12 @@ module Kettle
235
225
  end
236
226
  private_constant :FailureStatus
237
227
 
238
- def initialize(execute: false, accept: true, gem_signing_password: nil, otp_coordinator: nil)
228
+ def initialize(execute: false, accept: true, gem_signing_password: nil, otp_coordinator: nil, mise_adapter: nil)
239
229
  @execute = execute
240
230
  @accept = accept
241
231
  @gem_signing_password = gem_signing_password
242
232
  @otp_coordinator = otp_coordinator
233
+ @mise_adapter = mise_adapter || Kettle::Family.mise_adapter
243
234
  end
244
235
 
245
236
  def call(member:, phase:, command:, env: {}, interactive: false, raw: false, stdout_line_handler: nil, log_path: nil, passthrough_output: true)
@@ -688,31 +679,12 @@ module Kettle
688
679
 
689
680
  command_env = nonsensitive_env(env)
690
681
  unset_env, set_env = command_env.partition { |_key, value| value.nil? }
691
- injected_env = [
692
- *unset_env.flat_map { |key, _value| ["-u", key.to_s] },
693
- *set_env.map { |key, value| "#{key}=#{value}" }
694
- ]
695
- mise_argv = ["mise", "exec", "-C", member.root, "--"]
696
- return [*mise_argv, *argv] if injected_env.empty?
697
-
698
- if Gem.win_platform?
699
- return [
700
- *mise_argv,
701
- "ruby",
702
- "--disable=rubyopt",
703
- "-e",
704
- WINDOWS_ENV_EXEC_SCRIPT,
705
- "--",
706
- unset_env.length.to_s,
707
- *unset_env.map { |key, _value| key.to_s },
708
- set_env.length.to_s,
709
- *set_env.map { |key, value| "#{key}=#{value}" },
710
- "--",
711
- *argv
712
- ]
713
- end
714
-
715
- [*mise_argv, "env", *injected_env, *argv]
682
+ @mise_adapter.exec_argv(
683
+ member_root: member.root,
684
+ argv: argv,
685
+ unset_env: unset_env.map { |key, _value| key },
686
+ set_env: set_env.to_h
687
+ )
716
688
  end
717
689
 
718
690
  def process_env(member:, env:)
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Family
5
+ # Adapter seam around the mise binary.
6
+ #
7
+ # Mise is operational glue, not a code dependency: family tooling uses it
8
+ # for `mise trust -C <root>` after materializing managed task files, and
9
+ # for `mise exec -C <member.root> -- <cmd>` when running member commands
10
+ # under the member's declared runtime. It is absent in CI and on mise-less
11
+ # hosts, so every use degrades deliberately:
12
+ #
13
+ # - `trust_argv` returns nil when mise is unavailable (callers skip the
14
+ # trust phase — it only enables subsequent nested mise runs).
15
+ # - `exec_argv` passes the command through unwrapped when mise is
16
+ # unavailable (there is no member runtime to activate), preserving env
17
+ # injection via env(1) (or the portable ruby script on Windows).
18
+ #
19
+ # Specs inject a fake or stubbed adapter rather than spawning the real
20
+ # binary; only these adapter specs exercise the real PATH scan.
21
+ class MiseAdapter
22
+ # Portable env-manipulation wrapper used when commands must run with env
23
+ # changes on Windows (no env(1) there).
24
+ WINDOWS_ENV_EXEC_SCRIPT = <<~RUBY
25
+ unset_count = Integer(ARGV.shift)
26
+ unset_count.times { ENV.delete(ARGV.shift) }
27
+ assignment_count = Integer(ARGV.shift)
28
+ assignment_count.times do
29
+ key, value = ARGV.shift.split("=", 2)
30
+ ENV[key] = value
31
+ end
32
+ abort "missing command after environment arguments" if ARGV.shift != "--"
33
+ exec(*ARGV)
34
+ RUBY
35
+
36
+ # Absolute path to the mise executable resolved from the current PATH,
37
+ # or nil when mise is not installed. The absolute path is required for
38
+ # real spawns: CommandRunner's child env strips the orchestrator's
39
+ # Ruby/gem bin dirs from PATH, so a bare "mise" argv is unresolvable
40
+ # whenever the binary lives in one of those directories.
41
+ #
42
+ # @return [String, nil]
43
+ def executable
44
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
45
+ path = File.join(dir, "mise")
46
+ return path if File.file?(path) && File.executable?(path)
47
+ end
48
+ nil
49
+ end
50
+
51
+ # @return [Boolean] whether mise-wrapping and trust are possible
52
+ def available?
53
+ !executable.nil?
54
+ end
55
+
56
+ # argv for `mise trust -C <root>`, or nil when mise is unavailable.
57
+ #
58
+ # @param root [String]
59
+ # @return [Array<String>, nil]
60
+ def trust_argv(root)
61
+ exe = executable
62
+ return nil unless exe
63
+
64
+ [exe, "trust", "-C", root]
65
+ end
66
+
67
+ # Build the argv for running a member command under the member's
68
+ # mise-declared runtime. Passes through unwrapped (preserving env
69
+ # injection) when mise is unavailable.
70
+ #
71
+ # @param member_root [String]
72
+ # @param argv [Array<String>] the normalized command
73
+ # @param unset_env [Array<String>] env keys to unset in the child
74
+ # @param set_env [Hash{String => String}] env assignments for the child
75
+ # @return [Array<String>]
76
+ def exec_argv(member_root:, argv:, unset_env: [], set_env: {})
77
+ injected_env = [
78
+ *unset_env.flat_map { |key| ["-u", key.to_s] },
79
+ *set_env.map { |key, value| "#{key}=#{value}" }
80
+ ]
81
+
82
+ exe = executable
83
+ unless exe
84
+ # No mise executable: there is no member runtime to activate, and a
85
+ # wrapped spawn would be an unresolvable bare "mise" (ENOENT under
86
+ # the filtered child PATH). Run unwrapped, preserving env injection.
87
+ return argv if injected_env.empty?
88
+
89
+ return windows_env_argv(unset_env: unset_env, set_env: set_env, argv: argv) if Gem.win_platform?
90
+
91
+ return ["env", *injected_env, *argv]
92
+ end
93
+
94
+ mise_argv = [exe, "exec", "-C", member_root, "--"]
95
+ return [*mise_argv, *argv] if injected_env.empty?
96
+
97
+ if Gem.win_platform?
98
+ return [*mise_argv, *windows_env_argv(unset_env: unset_env, set_env: set_env, argv: argv)]
99
+ end
100
+
101
+ [*mise_argv, "env", *injected_env, *argv]
102
+ end
103
+
104
+ private
105
+
106
+ def windows_env_argv(unset_env:, set_env:, argv:)
107
+ [
108
+ "ruby",
109
+ "--disable=rubyopt",
110
+ "-e",
111
+ WINDOWS_ENV_EXEC_SCRIPT,
112
+ "--",
113
+ unset_env.length.to_s,
114
+ *unset_env.map { |key| key.to_s },
115
+ set_env.length.to_s,
116
+ *set_env.map { |key, value| "#{key}=#{value}" },
117
+ "--",
118
+ *argv
119
+ ]
120
+ end
121
+ end
122
+ end
123
+ end
@@ -9,11 +9,9 @@ module Kettle
9
9
  # Slice-1044 family mise tasks primitive: ensure a destination root
10
10
  # `mise.toml` references the kettle-family-managed TOML task file via
11
11
  # `[task_config] includes`, and report (optionally clean) task
12
- # redefinitions between the root file and the managed file.
13
- #
14
- # Precedence contract (mise): a task defined in the root `mise.toml` wins
15
- # over the same name from a task file. Redefinition is therefore allowed
16
- # and only reported; `clean` is always opt-in.
12
+ # redefinitions between the root file and the managed file. Family
13
+ # refreshes migrate managed task names out of the root so old commands
14
+ # cannot shadow generated fixes. Other tasks and settings remain local.
17
15
  #
18
16
  # All edits go through ast-crispr Actors with the ast-crispr-toml adapter.
19
17
  module MiseTasks
@@ -168,7 +166,7 @@ module Kettle
168
166
  end
169
167
 
170
168
  # Remove duplicate task definitions from the root mise.toml.
171
- # Opt-in (`--clean`); never runs implicitly.
169
+ # Used by family refreshes and the explicit non-family `--clean` path.
172
170
  #
173
171
  # @param root_content [String] the destination's mise.toml content
174
172
  # @param task_file_content [String] managed task file content
@@ -180,10 +178,11 @@ module Kettle
180
178
  redefs.each do |name|
181
179
  # A task can be defined as a `[tasks.<name>]` table block or as a
182
180
  # trivial `name = "…"` pair inside `[tasks]`; try both owners.
183
- [
184
- TomlSelectors.table(path: "tasks.#{name}"),
185
- TomlSelectors.pair(path: "tasks.#{name}")
186
- ].each do |target|
181
+ tables = document_context(content).structural_owners(owner_scope: :tables)
182
+ paths = tables.map(&:name).select { |path| path == "tasks.#{name}" || path.start_with?("tasks.#{name}.") }
183
+ targets = paths.reverse.map { |path| TomlSelectors.table(path: path) }
184
+ targets << TomlSelectors.pair(path: "tasks.#{name}")
185
+ targets.each do |target|
187
186
  result = Ast::Crispr::Delete.result(
188
187
  content: content,
189
188
  target: target,
@@ -261,6 +260,16 @@ module Kettle
261
260
  end
262
261
 
263
262
  pairs = table.wrapper.pairs
263
+ if pairs.any? { |pair| pair.key_name.to_s == "includes" }
264
+ entries = includes_entries(context) + [task_file_name]
265
+ result = Ast::Crispr::Replace.result(
266
+ content: content,
267
+ target: TomlSelectors.pair(path: "task_config.includes"),
268
+ replacement: "includes = [#{entries.map(&:inspect).join(", ")}]\n",
269
+ source_label: "mise.toml"
270
+ )
271
+ return fail_or(result)
272
+ end
264
273
  result =
265
274
  if pairs.empty?
266
275
  Ast::Crispr::Replace.result(
@@ -295,31 +304,30 @@ module Kettle
295
304
  # .kettle-family.yml).
296
305
  #
297
306
  # @param context [Hash] kettle-jem plugin context (:project_root)
298
- def self.materialize_managed_suite(context:)
307
+ def self.materialize_managed_suite(context:, task_file_name: DEFAULT_TASK_FILE_NAME, **options)
299
308
  root = context[:project_root].to_s
300
309
  return if root.empty?
301
310
 
302
311
  root = File.expand_path(root)
303
312
  return unless File.file?(File.join(root, ".kettle-family.yml"))
304
313
 
314
+ root_mise = File.join(root, "mise.toml")
315
+ return unless File.file?(root_mise)
316
+
305
317
  config = Config.load(root: root)
306
318
  content = ManagedSuite.new(config).content
307
319
  content += "\n" unless content.end_with?("\n")
308
320
 
309
- task_file_path = File.join(root, DEFAULT_TASK_FILE_NAME)
310
- File.write(task_file_path, content) unless File.exist?(task_file_path) && File.read(task_file_path) == content
311
-
312
- root_mise = File.join(root, "mise.toml")
313
- return unless File.file?(root_mise)
314
-
315
- result = ensure_include(
316
- root_content: File.read(root_mise),
317
- task_file_name: DEFAULT_TASK_FILE_NAME,
321
+ task_file_path = File.join(root, task_file_name)
322
+ original = File.read(root_mise)
323
+ included = ensure_include(root_content: original, task_file_name: task_file_name)
324
+ result = clean_redefinitions(
325
+ root_content: included.content,
326
+ task_file_name: task_file_name,
318
327
  task_file_content: content
319
328
  )
320
- File.write(root_mise, result.content) if result.changed
321
- rescue Error, SystemCallError => e
322
- warn("kettle-family: could not materialize managed mise tasks: #{e.class}: #{e.message}")
329
+ File.write(task_file_path, content) unless File.exist?(task_file_path) && File.read(task_file_path) == content
330
+ File.write(root_mise, result.content) unless result.content == original
323
331
  end
324
332
 
325
333
  # Comprehensive managed family mise task suite, generated from family
@@ -352,16 +360,25 @@ module Kettle
352
360
  def content
353
361
  tasks = {
354
362
  "test" => "bundle exec kettle-family test --execute",
355
- "lint" => "bundle exec kettle-family lint",
363
+ "lint" => "bundle exec kettle-family lint --execute",
356
364
  "state" => "bundle exec kettle-family release-state",
357
365
  "release-state" => "bundle exec kettle-family release-state",
358
- "template" => "bundle exec kettle-family template",
366
+ "template" => "bundle exec kettle-family template --execute",
359
367
  "bump" => "bundle exec kettle-family bump --execute",
360
368
  # Targeting is resolved by kettle-family from the family config:
361
369
  # shared-changelog families write the root changelog, member-local
362
370
  # families write each member's own.
363
371
  "changelog" => "bundle exec kettle-family add-changelog --execute"
364
372
  }
373
+ %w[install docs clean-unreleased gha-sha-pins bex branch-lanes discover metadata report pull push up sync reset mise-trust reconcile-releases].each do |command|
374
+ name = {"clean-unreleased" => "clean", "gha-sha-pins" => "pins"}.fetch(command, command)
375
+ flags = %w[branch-lanes discover metadata report mise-trust].include?(command) ? "" : " --execute"
376
+ flags += " --upgrade major" if name == "pins"
377
+ tasks[name] = "bundle exec kettle-family #{command}#{flags}"
378
+ end
379
+ tasks["readiness"] = "bundle exec kettle-family check --execute"
380
+ tasks["deps"] = "bundle exec kettle-family bup --execute --no-commit"
381
+ tasks["lint-fix"] = "bundle exec kettle-family bex --execute --no-commit -- bin/rake rubocop_gradual:autocorrect"
365
382
  tasks["bup"] = "bundle exec kettle-family bup --execute --commit"
366
383
  tasks["bupb"] = "bundle exec kettle-family update-bundler --execute --commit"
367
384
  tasks["update-bundler"] = tasks["bupb"]
@@ -369,7 +386,7 @@ module Kettle
369
386
  neutral = neutralize_assignments
370
387
  wiring = env_wiring_flags
371
388
  %w[bup bupb].each do |name|
372
- prefix = neutral.empty? ? "" : "env #{neutral} -- "
389
+ prefix = neutral.empty? ? "" : "env -- #{neutral} "
373
390
  tasks[name] = "#{prefix}#{tasks[name]}#{wiring}"
374
391
  end
375
392
  tasks["update-bundler"] = tasks["bupb"]
@@ -422,11 +439,13 @@ module Kettle
422
439
  #
423
440
  # Family-specific tasks (for example a release task with the
424
441
  # family's publish flags and secrets provider) belong in the root
425
- # mise.toml, where they shadow the managed definitions.
442
+ # mise.toml. Standard task names are managed here and migrated out
443
+ # of that file during refreshes.
426
444
  HEADER
445
+ out << "\n[check]\ndescription = \"Run readiness, lint, and tests\"\ndepends = [\"readiness\", \"lint\", \"test\"]\nrun = \"true\"\n"
427
446
  tasks.each do |name, run|
428
447
  out << "\n[#{name}]\n"
429
- out << "description = #{TASK_DESCRIPTIONS.fetch(name).inspect}\n"
448
+ out << "description = #{TASK_DESCRIPTIONS.fetch(name) { "Run family #{name}" }.inspect}\n"
430
449
  out << "raw = true\n"
431
450
  out << "run = #{run.inspect}\n"
432
451
  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.3.28"
8
+ VERSION = "1.3.30"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -109,7 +109,7 @@ module Kettle
109
109
  REGISTRY_WAIT_INTERVAL_SECONDS = 15
110
110
  TEMPLATE_GEMSPEC_RETRY_DELAY_SECONDS = 0.25
111
111
 
112
- def initialize(command:, config:, members:, family_members: nil, execute: false, accept: true, commit: true, allow_dirty: false, autostash: true, template_cleanup: true, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, fast_recovery: nil, fast_recovery_members: nil, skip_ci: false, skip_changelog: false, skip_appraisals: false, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, required_remotes: nil, auto_dependency_floors: nil, validate_ci_bundles: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, gha_sha_pins_ttl_days: nil, env_overrides: {}, debug: false, verbose: false, gem_signing_password: nil, secrets_provider: nil, jobs: nil, progress_io: nil, reset_target: nil, bup_args: [], bex_args: [], start_member: nil, start_branch: nil, template_branch_selection: {}, release_state_results: nil, event_tape: nil, **options)
112
+ def initialize(command:, config:, members:, family_members: nil, execute: false, accept: true, commit: true, allow_dirty: false, autostash: true, template_cleanup: true, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, fast_recovery: nil, fast_recovery_members: nil, skip_ci: false, skip_changelog: false, skip_appraisals: false, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, required_remotes: nil, auto_dependency_floors: nil, validate_ci_bundles: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, gha_sha_pins_ttl_days: nil, env_overrides: {}, debug: false, verbose: false, gem_signing_password: nil, secrets_provider: nil, jobs: nil, progress_io: nil, reset_target: nil, bup_args: [], bex_args: [], start_member: nil, start_branch: nil, template_branch_selection: {}, release_state_results: nil, event_tape: nil, mise_adapter: nil, **options)
113
113
  @command = command
114
114
  @config = config
115
115
  @members = members
@@ -156,6 +156,7 @@ module Kettle
156
156
  @verbose = verbose
157
157
  @gem_signing_password = gem_signing_password
158
158
  @secrets_provider = secrets_provider || Secrets::Provider.new
159
+ @mise_adapter = mise_adapter || Kettle::Family.mise_adapter
159
160
  @jobs = jobs
160
161
  @progress_io = progress_io
161
162
  @reset_target = reset_target
@@ -256,7 +257,30 @@ module Kettle
256
257
  runner: runner,
257
258
  preserve_members: template_cleanup ? [] : failed_members
258
259
  )
259
- checkout_preflight + sync_results + workflow_results + rollback_results + restore_results
260
+ results = checkout_preflight + sync_results + workflow_results + rollback_results + restore_results
261
+ if results.all?(&:ok?)
262
+ MiseTasks.materialize_managed_suite(context: {project_root: config.root})
263
+ # `mise trust` requires the mise executable; spec environments and
264
+ # minimal CI images may not have it installed. Trust is only an
265
+ # enabler for subsequent nested mise runs, so skip when absent.
266
+ trust_argv = @mise_adapter.trust_argv(config.root)
267
+ if trust_argv && File.file?(File.join(config.root, "mise.toml"))
268
+ results << runner.call(
269
+ member: family_member, phase: "family_mise_trust",
270
+ command: trust_argv, raw: true
271
+ )
272
+ end
273
+ if results.all?(&:ok?) && family_root_bundle_commit? && File.file?(File.join(config.root, "mise.toml"))
274
+ results << runner.call(
275
+ member: family_member,
276
+ phase: "commit_family_mise_tasks",
277
+ command: ["sh", "-lc", "files=$(git ls-files --modified --others --exclude-standard -- mise.toml mise-family.tasks.toml); " \
278
+ "if [ -n \"$files\" ]; then git add -- mise.toml mise-family.tasks.toml && " \
279
+ "git commit --only -m '🔧 Refresh managed family mise tasks' -- mise.toml mise-family.tasks.toml; fi"]
280
+ )
281
+ end
282
+ end
283
+ results
260
284
  end
261
285
 
262
286
  def template_worktree_sync_results(runner:)
@@ -535,10 +559,18 @@ module Kettle
535
559
  return results unless results.all?(&:ok?)
536
560
 
537
561
  results.concat(release_member_results(workflow_members, include_family_changelog: !skip_changelog))
538
- return results unless !execute && explicit_monorepo_mode? && results.all?(&:ok?)
539
-
562
+ return results unless explicit_monorepo_mode? && results.all?(&:ok?)
563
+
564
+ # Per-member appends during finalization only cover members published
565
+ # in THIS run. A resumed wave selects only the unpublished remainder,
566
+ # so the closing aggregate must always span the full shared-version
567
+ # cohort to contribute artifacts published by earlier interrupted
568
+ # waves. Existing assets are skipped by kettle-gh-release, so the
569
+ # full-cohort pass is cheap on a complete wave.
540
570
  selected_aggregate_members = aggregate_release_members(workflow_members)
541
- results << aggregate_monorepo_github_release(aggregate_github_release_members) unless selected_aggregate_members.empty?
571
+ if selected_aggregate_members.any? && (!execute || publish)
572
+ results << aggregate_monorepo_github_release(aggregate_github_release_members)
573
+ end
542
574
  return results
543
575
  end
544
576
  return git_sync_results(workflow_members) if GIT_SYNC_COMMANDS.key?(command)
@@ -548,7 +580,7 @@ module Kettle
548
580
 
549
581
  def reset_member_results(workflow_members)
550
582
  target = normalized_reset_target
551
- runner = CommandRunner.new(execute: execute, accept: accept)
583
+ runner = CommandRunner.new(execute: execute, accept: accept, mise_adapter: @mise_adapter)
552
584
  workflow_members.each_with_object([]) do |member, memo|
553
585
  case target
554
586
  when "Gemfile.lock"
@@ -575,7 +607,7 @@ module Kettle
575
607
  return template_member_workflow_results(workflow_members) if command == "template" && execute
576
608
  return test_member_workflow_results(workflow_members) if command == "test" && execute
577
609
 
578
- runner = CommandRunner.new(execute: execute, accept: accept)
610
+ runner = CommandRunner.new(execute: execute, accept: accept, mise_adapter: @mise_adapter)
579
611
  results = []
580
612
  update_family_root_bundle(runner: runner, memo: results) if %w[bup bupb update-bundler].include?(command)
581
613
  return results unless results.all?(&:ok?)
@@ -682,7 +714,7 @@ module Kettle
682
714
 
683
715
  Array.new(wave_jobs) do
684
716
  Thread.new do # rubocop:disable ThreadSafety/NewThread -- sibling test suites run in isolated project roots.
685
- runner = CommandRunner.new(execute: execute, accept: accept)
717
+ runner = CommandRunner.new(execute: execute, accept: accept, mise_adapter: @mise_adapter)
686
718
  loop do
687
719
  break if mutex.synchronize { stop }
688
720
 
@@ -796,7 +828,7 @@ module Kettle
796
828
 
797
829
  index, entry = queue.pop(true)
798
830
  member = entry.fetch(:member)
799
- result = CommandRunner.new(execute: execute, accept: accept).call(
831
+ result = CommandRunner.new(execute: execute, accept: accept, mise_adapter: @mise_adapter).call(
800
832
  member: member,
801
833
  phase: command,
802
834
  command: workflow_command(member),
@@ -1437,7 +1469,7 @@ module Kettle
1437
1469
  end
1438
1470
 
1439
1471
  bootstrap = NomonoBootstrap.new(latest_version: latest_nomono_version, mode: :execute)
1440
- runner = CommandRunner.new(execute: execute, accept: accept)
1472
+ runner = CommandRunner.new(execute: execute, accept: accept, mise_adapter: @mise_adapter)
1441
1473
  candidate_members.each_with_object([]) do |member, memo|
1442
1474
  next unless bootstrap.member_needs_bootstrap?(member)
1443
1475
 
@@ -1517,7 +1549,7 @@ module Kettle
1517
1549
  progress_key ||= member.name
1518
1550
  progress_label ||= member.name
1519
1551
  start_template_member_progress(progress, member, progress_key: progress_key, progress_label: progress_label) unless progress_started
1520
- runner = CommandRunner.new(execute: execute, accept: accept)
1552
+ runner = CommandRunner.new(execute: execute, accept: accept, mise_adapter: @mise_adapter)
1521
1553
  [].tap do |memo|
1522
1554
  if config.normalize_lockfiles?
1523
1555
  normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "prepare_lockfiles", commit_changes: commit_changes)
@@ -1738,16 +1770,20 @@ module Kettle
1738
1770
  # tracks trust by config path. Trust every active config between the
1739
1771
  # worktree root and the member command directory before invoking a
1740
1772
  # command there. This bootstrap must run raw: wrapping it in `mise exec`
1741
- # would require the trust it is establishing.
1773
+ # would require the trust it is establishing. Roots are skipped when the
1774
+ # mise executable is unavailable (mise is operational glue, absent in CI).
1742
1775
  def worktree_mise_trust_results(entry, runner:, phase:)
1743
1776
  member = entry.fetch(:member)
1744
1777
  original_member = entry.fetch(:original_member, member)
1745
1778
  worktree_root = entry.fetch(:worktree_root)
1746
- mise_config_roots_in_worktree(worktree_root, member.root).map do |root|
1779
+ mise_config_roots_in_worktree(worktree_root, member.root).filter_map do |root|
1780
+ trust_argv = @mise_adapter.trust_argv(root)
1781
+ next unless trust_argv
1782
+
1747
1783
  result = runner.call(
1748
1784
  member: member,
1749
1785
  phase: phase,
1750
- command: ["mise", "trust", "-C", root],
1786
+ command: trust_argv,
1751
1787
  raw: true
1752
1788
  )
1753
1789
  result.member_name = original_member.name
@@ -2693,7 +2729,7 @@ module Kettle
2693
2729
  end
2694
2730
 
2695
2731
  def command_runner
2696
- CommandRunner.new(execute: execute, accept: accept, gem_signing_password: @gem_signing_password)
2732
+ CommandRunner.new(execute: execute, accept: accept, gem_signing_password: @gem_signing_password, mise_adapter: @mise_adapter)
2697
2733
  end
2698
2734
 
2699
2735
  def release_command_runner
@@ -2704,7 +2740,8 @@ module Kettle
2704
2740
  # RubyGems MFA belongs to the child release command. Keeping an OTP
2705
2741
  # coordinator here can make a family-level gem retry look successful
2706
2742
  # while skipping the child's checksums and GitHub release steps.
2707
- otp_coordinator: nil
2743
+ otp_coordinator: nil,
2744
+ mise_adapter: @mise_adapter
2708
2745
  )
2709
2746
  end
2710
2747
 
data/lib/kettle/family.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "family/member"
4
4
  require_relative "family/command_result"
5
5
  require_relative "family/release_state_result"
6
+ require_relative "family/mise_adapter"
6
7
  require_relative "family/command_runner"
7
8
  require_relative "family/readiness_check"
8
9
  require_relative "family/published_version_check"
@@ -43,6 +44,25 @@ module Kettle
43
44
  module Family
44
45
  class Error < StandardError; end
45
46
 
47
+ # Default MiseAdapter for the whole family stack. CommandRunner, Workflow,
48
+ # and CLI use this when no adapter is injected; specs swap it to control
49
+ # mise availability without spawning the real binary (mise is operational
50
+ # glue, absent in CI).
51
+ #
52
+ # @return [MiseAdapter]
53
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
54
+ def self.mise_adapter
55
+ @mise_adapter ||= MiseAdapter.new
56
+ end
57
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
58
+
59
+ # @param adapter [MiseAdapter]
60
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
61
+ def self.mise_adapter=(adapter)
62
+ @mise_adapter = adapter
63
+ end
64
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
65
+
46
66
  # kettle-jem plugin registration (kettle-jem-appraisals pattern). The gem
47
67
  # name "kettle-family" resolves to this module as the plugin handle, and
48
68
  # kettle-jem calls +register_kettle_jem_plugin(registrar)+ on it when the
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.3.28
4
+ version: 1.3.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -134,7 +134,7 @@ dependencies:
134
134
  version: '3.1'
135
135
  - - ">="
136
136
  - !ruby/object:Gem::Version
137
- version: 3.1.20
137
+ version: 3.1.23
138
138
  type: :runtime
139
139
  prerelease: false
140
140
  version_requirements: !ruby/object:Gem::Requirement
@@ -144,7 +144,7 @@ dependencies:
144
144
  version: '3.1'
145
145
  - - ">="
146
146
  - !ruby/object:Gem::Version
147
- version: 3.1.20
147
+ version: 3.1.23
148
148
  - !ruby/object:Gem::Dependency
149
149
  name: kettle-gha-pins
150
150
  requirement: !ruby/object:Gem::Requirement
@@ -537,6 +537,7 @@ files:
537
537
  - lib/kettle/family/local_install.rb
538
538
  - lib/kettle/family/member.rb
539
539
  - lib/kettle/family/member_loader.rb
540
+ - lib/kettle/family/mise_adapter.rb
540
541
  - lib/kettle/family/mise_tasks.rb
541
542
  - lib/kettle/family/nomono_bootstrap.rb
542
543
  - lib/kettle/family/orderer.rb
@@ -567,10 +568,10 @@ licenses:
567
568
  - AGPL-3.0-only
568
569
  metadata:
569
570
  homepage_uri: https://kettle-family.galtzo.com
570
- source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.28
571
- changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.28/CHANGELOG.md
571
+ source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.3.30
572
+ changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.3.30/CHANGELOG.md
572
573
  bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
573
- documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.28
574
+ documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.3.30
574
575
  funding_uri: https://github.com/sponsors/pboling
575
576
  wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
576
577
  news_uri: https://www.railsbling.com/tags/kettle-family
metadata.gz.sig CHANGED
Binary file