kettle-family 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +34 -1
- data/CONTRIBUTING.md +4 -1
- data/README.md +1 -1
- data/lib/kettle/family/cli.rb +19 -2
- data/lib/kettle/family/command_runner.rb +131 -16
- data/lib/kettle/family/config.rb +8 -0
- data/lib/kettle/family/local_install.rb +84 -8
- data/lib/kettle/family/report.rb +26 -1
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +228 -10
- data.tar.gz.sig +0 -0
- metadata +4 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8674366c5bb5bff14156cae0e04c58805e3401510d4f6efe865902fe2facf988
|
|
4
|
+
data.tar.gz: b4a1ae4344a06bffc03b8fee3e472bc2f9cd40c7caa3276e30598da9307ba546
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 718d504c273ced46600feb4f196db9d13301026f74f96288eaaab960ad0765dba6067944e65f2e9855ba70b4e59e52d33609fce7b3760cc5d752303191184cd2
|
|
7
|
+
data.tar.gz: 5f1648deea0b3e04ca0edb2a453bb5d919a0e7cd1dcc19562b1ec77d15fa1c5870fd03d7b69f328fb04fec3ff70686e23d2fcaff8d1c083c941d36ece6dd28d8
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,37 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [0.1.20] - 2026-06-26
|
|
34
|
+
|
|
35
|
+
- TAG: [v0.1.20][0.1.20t]
|
|
36
|
+
- COVERAGE: 94.99% -- 1802/1897 lines in 21 files
|
|
37
|
+
- BRANCH COVERAGE: 74.39% -- 613/824 branches in 21 files
|
|
38
|
+
- 38.60% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `kettle-family template --execute` now runs member templating in parallel by
|
|
43
|
+
default, with `--jobs` and `template.jobs` controls plus compact live progress
|
|
44
|
+
and changed-file summaries.
|
|
45
|
+
- `kettle-family release --execute` now runs dependency-safe member release
|
|
46
|
+
waves in parallel for distinct Git worktrees, coordinating concurrent
|
|
47
|
+
RubyGems MFA prompts.
|
|
48
|
+
- `kettle-family install --execute` now installs independent local gems in
|
|
49
|
+
dependency-safe waves, using `--jobs` for parallelism.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- Family templating now invokes `kettle-jem` in quiet JSON mode and suppresses
|
|
54
|
+
noisy Bundler/debug environment by default, overriding inherited debug
|
|
55
|
+
variables unless `--debug` is passed.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Parallel release MFA coordination now shares an entered OTP only with prompts
|
|
60
|
+
already queued at submission time, shows the live queued prompt count as
|
|
61
|
+
`N / Y` for the current release wave capacity, and asks again for later
|
|
62
|
+
RubyGems OTP prompts.
|
|
63
|
+
|
|
33
64
|
## [0.1.19] - 2026-06-25
|
|
34
65
|
|
|
35
66
|
- TAG: [v0.1.19][0.1.19t]
|
|
@@ -351,7 +382,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
351
382
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
352
383
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
353
384
|
|
|
354
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.
|
|
385
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v0.1.20...HEAD
|
|
386
|
+
[0.1.20]: https://github.com/kettle-dev/kettle-family/compare/v0.1.19...v0.1.20
|
|
387
|
+
[0.1.20t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.20
|
|
355
388
|
[0.1.19]: https://github.com/kettle-dev/kettle-family/compare/v0.1.18...v0.1.19
|
|
356
389
|
[0.1.19t]: https://github.com/kettle-dev/kettle-family/releases/tag/v0.1.19
|
|
357
390
|
[0.1.18]: https://github.com/kettle-dev/kettle-family/compare/v0.1.17...v0.1.18
|
data/CONTRIBUTING.md
CHANGED
|
@@ -131,9 +131,12 @@ toolchain, and it may be higher than the gemspec runtime floor.
|
|
|
131
131
|
They are created and updated with the commands:
|
|
132
132
|
|
|
133
133
|
```console
|
|
134
|
-
bin/rake appraisal:
|
|
134
|
+
bin/rake appraisal:generate
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
Use `bin/rake appraisal:update` when you intentionally need to resolve fresh
|
|
138
|
+
appraisal locks.
|
|
139
|
+
|
|
137
140
|
If you need to reset all gemfiles/*.gemfile.lock files:
|
|
138
141
|
|
|
139
142
|
```console
|
data/README.md
CHANGED
|
@@ -571,7 +571,7 @@ Thanks for RTFM. ☺️
|
|
|
571
571
|
[📌gitmoji]: https://gitmoji.dev
|
|
572
572
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
573
573
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
574
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.
|
|
574
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.897-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
575
575
|
[🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
|
|
576
576
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
577
577
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/lib/kettle/family/cli.rb
CHANGED
|
@@ -81,6 +81,8 @@ module Kettle
|
|
|
81
81
|
--report PATH Write JSON report to PATH
|
|
82
82
|
--execute Execute external workflow commands
|
|
83
83
|
--dry-run Plan external workflow commands without running them (default)
|
|
84
|
+
--debug Preserve debug environment for workflow commands
|
|
85
|
+
--jobs N Parallel jobs for executed family templating, release, or install
|
|
84
86
|
--env KEY=VALUE Override an environment variable for each member workflow command
|
|
85
87
|
--section NAME Changelog section for add-changelog
|
|
86
88
|
--entry TEXT Changelog entry for add-changelog
|
|
@@ -114,6 +116,8 @@ module Kettle
|
|
|
114
116
|
json: false,
|
|
115
117
|
report: nil,
|
|
116
118
|
execute: false,
|
|
119
|
+
debug: false,
|
|
120
|
+
jobs: nil,
|
|
117
121
|
workflow_env: {},
|
|
118
122
|
changelog_section: nil,
|
|
119
123
|
changelog_entry: nil,
|
|
@@ -139,6 +143,8 @@ module Kettle
|
|
|
139
143
|
parser.on("--report PATH") { |value| options[:report] = value }
|
|
140
144
|
parser.on("--execute") { options[:execute] = true }
|
|
141
145
|
parser.on("--dry-run") { options[:execute] = false }
|
|
146
|
+
parser.on("--debug") { options[:debug] = true }
|
|
147
|
+
parser.on("--jobs N", Integer) { |value| options[:jobs] = value }
|
|
142
148
|
parser.on("--env KEY=VALUE") { |value| parse_env_override(value, options[:workflow_env]) }
|
|
143
149
|
parser.on("--section NAME") { |value| options[:changelog_section] = value }
|
|
144
150
|
parser.on("--entry TEXT") { |value| options[:changelog_entry] = value }
|
|
@@ -228,10 +234,21 @@ module Kettle
|
|
|
228
234
|
continue_ci_failures: options[:release_continue_ci_failures],
|
|
229
235
|
gha_sha_pins_upgrade: options[:gha_sha_pins_upgrade],
|
|
230
236
|
gha_sha_pins_check: options[:check],
|
|
231
|
-
env_overrides: options[:workflow_env]
|
|
237
|
+
env_overrides: options[:workflow_env],
|
|
238
|
+
debug: options[:debug],
|
|
239
|
+
jobs: options[:jobs],
|
|
240
|
+
progress_io: progress_io(command, options)
|
|
232
241
|
).results
|
|
233
242
|
end
|
|
234
243
|
|
|
244
|
+
def progress_io(command, options)
|
|
245
|
+
return nil unless command == "template"
|
|
246
|
+
return nil unless options[:execute]
|
|
247
|
+
return nil if options[:json]
|
|
248
|
+
|
|
249
|
+
out
|
|
250
|
+
end
|
|
251
|
+
|
|
235
252
|
def branch_target_command?(command, config)
|
|
236
253
|
return false if config.release_target_branches.empty?
|
|
237
254
|
return false if command == "release-state"
|
|
@@ -401,7 +418,7 @@ module Kettle
|
|
|
401
418
|
end
|
|
402
419
|
|
|
403
420
|
def install_results(config:, members:, options:)
|
|
404
|
-
LocalInstall.new(config: config, members: members, execute: options[:execute]).results
|
|
421
|
+
LocalInstall.new(config: config, members: members, execute: options[:execute], jobs: options[:jobs]).results
|
|
405
422
|
end
|
|
406
423
|
|
|
407
424
|
def release_state_results(config:, members:)
|
|
@@ -1,14 +1,114 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require "io/console"
|
|
4
5
|
|
|
5
6
|
module Kettle
|
|
6
7
|
module Family
|
|
7
8
|
class CommandRunner
|
|
8
|
-
|
|
9
|
+
class OtpCoordinator
|
|
10
|
+
def initialize(input: $stdin, output: $stdout, queue_total: nil)
|
|
11
|
+
@input = input
|
|
12
|
+
@output = output
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@condition = ConditionVariable.new
|
|
15
|
+
@prompting = false
|
|
16
|
+
@queue_closed = false
|
|
17
|
+
@generation = 0
|
|
18
|
+
@completed_generation = nil
|
|
19
|
+
@response = nil
|
|
20
|
+
@queued_count = 0
|
|
21
|
+
@queue_total = queue_total
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def queue_total=(value)
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
@queue_total = value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def request(member_name:, chunk:)
|
|
31
|
+
generation = nil
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@condition.wait(@mutex) while @prompting && @queue_closed
|
|
34
|
+
|
|
35
|
+
if @prompting
|
|
36
|
+
generation = @generation
|
|
37
|
+
@queued_count += 1
|
|
38
|
+
render_queue_status_locked
|
|
39
|
+
return wait_for_response(generation)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@prompting = true
|
|
43
|
+
@queue_closed = false
|
|
44
|
+
@queued_count = 1
|
|
45
|
+
@generation += 1
|
|
46
|
+
generation = @generation
|
|
47
|
+
start_prompt_locked(member_name: member_name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
response = read_response(chunk: chunk)
|
|
51
|
+
close_queue
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
@response = response
|
|
54
|
+
@completed_generation = generation
|
|
55
|
+
@prompting = false
|
|
56
|
+
@queue_closed = false
|
|
57
|
+
@condition.broadcast
|
|
58
|
+
response
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def wait_for_response(generation)
|
|
65
|
+
@condition.wait(@mutex) while @prompting
|
|
66
|
+
return @response if @completed_generation == generation
|
|
67
|
+
|
|
68
|
+
""
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def close_queue
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
@queue_closed = true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def start_prompt_locked(member_name:)
|
|
78
|
+
@output.puts
|
|
79
|
+
@output.puts("[#{member_name}] RubyGems MFA requested.")
|
|
80
|
+
render_queue_status_locked
|
|
81
|
+
@output.puts("Queued prompts at entry will share this code; later prompts will ask again.")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_queue_status_locked
|
|
85
|
+
suffix = @queue_total ? " / #{@queue_total}" : ""
|
|
86
|
+
@output.puts("RubyGems MFA prompts queued: #{@queued_count}#{suffix}")
|
|
87
|
+
@output.flush if @output.respond_to?(:flush)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def read_response(chunk:)
|
|
91
|
+
@output.print("#{otp_prompt_label(chunk)} ")
|
|
92
|
+
@output.flush if @output.respond_to?(:flush)
|
|
93
|
+
if @input.respond_to?(:noecho) && @input.tty?
|
|
94
|
+
@input.noecho(&:gets)&.chomp.to_s
|
|
95
|
+
else
|
|
96
|
+
@input.gets&.chomp.to_s
|
|
97
|
+
end
|
|
98
|
+
ensure
|
|
99
|
+
@output.puts if @output.respond_to?(:puts)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def otp_prompt_label(chunk)
|
|
103
|
+
chunk.to_s.lines.last&.strip.to_s.empty? ? "Code:" : chunk.to_s.lines.last.strip
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def initialize(execute: false, accept: true, gem_signing_password: nil, otp_coordinator: nil)
|
|
9
108
|
@execute = execute
|
|
10
109
|
@accept = accept
|
|
11
110
|
@gem_signing_password = gem_signing_password
|
|
111
|
+
@otp_coordinator = otp_coordinator
|
|
12
112
|
end
|
|
13
113
|
|
|
14
114
|
def call(member:, phase:, command:, env: {}, interactive: false)
|
|
@@ -19,7 +119,7 @@ module Kettle
|
|
|
19
119
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
20
120
|
stdout, stderr, status = with_unbundled_environment do
|
|
21
121
|
if interactive
|
|
22
|
-
run_interactive(env: process_env, argv: argv, chdir: member.root)
|
|
122
|
+
run_interactive(env: process_env, argv: argv, chdir: member.root, member_name: member.name)
|
|
23
123
|
else
|
|
24
124
|
Open3.capture3(process_env, *argv, chdir: member.root)
|
|
25
125
|
end
|
|
@@ -44,29 +144,29 @@ module Kettle
|
|
|
44
144
|
|
|
45
145
|
private
|
|
46
146
|
|
|
47
|
-
attr_reader :execute, :accept, :gem_signing_password
|
|
147
|
+
attr_reader :execute, :accept, :gem_signing_password, :otp_coordinator
|
|
48
148
|
|
|
49
|
-
def run_interactive(env:, argv:, chdir:)
|
|
50
|
-
return run_interactive_pty(env: env, argv: argv, chdir: chdir) if pty_available?
|
|
149
|
+
def run_interactive(env:, argv:, chdir:, member_name:)
|
|
150
|
+
return run_interactive_pty(env: env, argv: argv, chdir: chdir, member_name: member_name) if pty_available?
|
|
51
151
|
|
|
52
|
-
run_interactive_open3(env: env, argv: argv, chdir: chdir)
|
|
152
|
+
run_interactive_open3(env: env, argv: argv, chdir: chdir, member_name: member_name)
|
|
53
153
|
end
|
|
54
154
|
|
|
55
|
-
def run_interactive_pty(env:, argv:, chdir:)
|
|
155
|
+
def run_interactive_pty(env:, argv:, chdir:, member_name:)
|
|
56
156
|
stdout = +""
|
|
57
157
|
status = nil
|
|
58
158
|
PTY.spawn(env, *argv, chdir: chdir) do |output, input, pid|
|
|
59
159
|
begin
|
|
60
160
|
loop do
|
|
61
161
|
readers = [output]
|
|
62
|
-
readers << $stdin if $stdin.tty?
|
|
162
|
+
readers << $stdin if $stdin.tty? && !otp_coordinator
|
|
63
163
|
ready = IO.select(readers)
|
|
64
164
|
ready.first.each do |reader|
|
|
65
165
|
if reader.equal?(output)
|
|
66
166
|
chunk = output.readpartial(1024)
|
|
67
167
|
stdout << chunk
|
|
68
168
|
$stdout.print(chunk)
|
|
69
|
-
handle_interactive_prompt(input, chunk)
|
|
169
|
+
handle_interactive_prompt(input, chunk, member_name: member_name)
|
|
70
170
|
else
|
|
71
171
|
input.write($stdin.readpartial(1024))
|
|
72
172
|
end
|
|
@@ -80,20 +180,20 @@ module Kettle
|
|
|
80
180
|
[stdout, "", status]
|
|
81
181
|
end
|
|
82
182
|
|
|
83
|
-
def run_interactive_open3(env:, argv:, chdir:)
|
|
183
|
+
def run_interactive_open3(env:, argv:, chdir:, member_name:)
|
|
84
184
|
captured_stdout = +""
|
|
85
185
|
captured_stderr = +""
|
|
86
186
|
status = nil
|
|
87
187
|
Open3.popen3(env, *argv, chdir: chdir) do |input, output, error, wait_thread|
|
|
88
188
|
readers = [output, error]
|
|
89
|
-
readers << $stdin if $stdin.tty?
|
|
189
|
+
readers << $stdin if $stdin.tty? && !otp_coordinator
|
|
90
190
|
until readers.empty?
|
|
91
191
|
ready = IO.select(readers)
|
|
92
192
|
ready.first.each do |reader|
|
|
93
193
|
if reader.equal?($stdin)
|
|
94
194
|
input.write($stdin.readpartial(1024))
|
|
95
195
|
else
|
|
96
|
-
read_interactive_stream(reader, output, input, captured_stdout, captured_stderr, readers)
|
|
196
|
+
read_interactive_stream(reader, output, input, captured_stdout, captured_stderr, readers, member_name: member_name)
|
|
97
197
|
end
|
|
98
198
|
end
|
|
99
199
|
end
|
|
@@ -102,7 +202,7 @@ module Kettle
|
|
|
102
202
|
[captured_stdout, captured_stderr, status]
|
|
103
203
|
end
|
|
104
204
|
|
|
105
|
-
def read_interactive_stream(reader, output, input, captured_stdout, captured_stderr, readers)
|
|
205
|
+
def read_interactive_stream(reader, output, input, captured_stdout, captured_stderr, readers, member_name:)
|
|
106
206
|
chunk = reader.readpartial(1024)
|
|
107
207
|
if reader.equal?(output)
|
|
108
208
|
captured_stdout << chunk
|
|
@@ -111,7 +211,7 @@ module Kettle
|
|
|
111
211
|
captured_stderr << chunk
|
|
112
212
|
$stderr.print(chunk)
|
|
113
213
|
end
|
|
114
|
-
handle_interactive_prompt(input, chunk)
|
|
214
|
+
handle_interactive_prompt(input, chunk, member_name: member_name)
|
|
115
215
|
rescue EOFError
|
|
116
216
|
readers.delete(reader)
|
|
117
217
|
end
|
|
@@ -132,8 +232,11 @@ module Kettle
|
|
|
132
232
|
input.flush
|
|
133
233
|
end
|
|
134
234
|
|
|
135
|
-
def handle_interactive_prompt(input, chunk)
|
|
136
|
-
|
|
235
|
+
def handle_interactive_prompt(input, chunk, member_name: nil)
|
|
236
|
+
if otp_prompt?(chunk)
|
|
237
|
+
write_otp_response(input, chunk, member_name: member_name) if otp_coordinator && otp_response_prompt?(chunk)
|
|
238
|
+
return
|
|
239
|
+
end
|
|
137
240
|
|
|
138
241
|
if accept_confirmation_prompt?(chunk)
|
|
139
242
|
write_accept_response(input) if accept
|
|
@@ -148,6 +251,14 @@ module Kettle
|
|
|
148
251
|
input.flush
|
|
149
252
|
end
|
|
150
253
|
|
|
254
|
+
def write_otp_response(input, chunk, member_name:)
|
|
255
|
+
response = otp_coordinator.request(member_name: member_name || "release", chunk: chunk)
|
|
256
|
+
return if response.to_s.empty?
|
|
257
|
+
|
|
258
|
+
input.write("#{response}\n")
|
|
259
|
+
input.flush
|
|
260
|
+
end
|
|
261
|
+
|
|
151
262
|
def accept_confirmation_prompt?(chunk)
|
|
152
263
|
chunk.match?(/\[[Yy]\/[Nn]\]\s*:?/)
|
|
153
264
|
end
|
|
@@ -156,6 +267,10 @@ module Kettle
|
|
|
156
267
|
chunk.match?(/(?:multi-factor authentication|OTP code|one-time password|\bCode:\s*)/i)
|
|
157
268
|
end
|
|
158
269
|
|
|
270
|
+
def otp_response_prompt?(chunk)
|
|
271
|
+
chunk.match?(/(?:OTP code|one-time password|\bCode:\s*)/i)
|
|
272
|
+
end
|
|
273
|
+
|
|
159
274
|
def signing_password_prompt?(chunk)
|
|
160
275
|
chunk.match?(/(?:enter\s+)?(?:PEM\s+)?pass(?:\s|-)?phrase\s*(?:for\s+[^:]+)?[:?]\s*\z/i) ||
|
|
161
276
|
chunk.match?(/(?:PEM|private key) password\s*[:?]\s*\z/i)
|
data/lib/kettle/family/config.rb
CHANGED
|
@@ -160,6 +160,10 @@ module Kettle
|
|
|
160
160
|
fetch_path("template", "repository_topology")
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
+
def template_jobs
|
|
164
|
+
fetch_path("template", "jobs")
|
|
165
|
+
end
|
|
166
|
+
|
|
163
167
|
def normalize_lockfiles?
|
|
164
168
|
fetch_path("template", "normalize_lockfiles") == true
|
|
165
169
|
end
|
|
@@ -204,6 +208,10 @@ module Kettle
|
|
|
204
208
|
stringify_env(fetch_path("release", "env") || {})
|
|
205
209
|
end
|
|
206
210
|
|
|
211
|
+
def release_jobs
|
|
212
|
+
fetch_path("release", "jobs")
|
|
213
|
+
end
|
|
214
|
+
|
|
207
215
|
def release_family_changelog?
|
|
208
216
|
fetch_path("release", "family_changelog", "enabled") == true
|
|
209
217
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "etc"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "json"
|
|
5
6
|
require "open3"
|
|
@@ -8,16 +9,18 @@ require "time"
|
|
|
8
9
|
module Kettle
|
|
9
10
|
module Family
|
|
10
11
|
class LocalInstall
|
|
11
|
-
def initialize(config:, members:, execute: false)
|
|
12
|
+
def initialize(config:, members:, execute: false, jobs: nil)
|
|
12
13
|
@config = config
|
|
13
14
|
@members = members
|
|
14
15
|
@execute = execute
|
|
16
|
+
@jobs = jobs
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def results
|
|
18
|
-
results = install_members
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
results = if execute && install_jobs(install_members) > 1
|
|
21
|
+
parallel_install_results
|
|
22
|
+
else
|
|
23
|
+
sequential_install_results(install_members)
|
|
21
24
|
end
|
|
22
25
|
write_local_install_marker if execute && results.all?(&:ok?)
|
|
23
26
|
results
|
|
@@ -25,11 +28,26 @@ module Kettle
|
|
|
25
28
|
|
|
26
29
|
private
|
|
27
30
|
|
|
28
|
-
attr_reader :config, :members, :execute
|
|
31
|
+
attr_reader :config, :members, :execute, :jobs
|
|
29
32
|
|
|
30
33
|
def install_members
|
|
34
|
+
@install_members ||= unique_members(local_dependency_members + members)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def local_dependency_members
|
|
38
|
+
@local_dependency_members ||= unique_members(
|
|
39
|
+
config.install_local_dependencies.map { |path| member_from_path(path) }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def selected_install_members
|
|
44
|
+
local_dependency_names = local_dependency_members.map(&:name)
|
|
45
|
+
unique_members(members).reject { |member| local_dependency_names.include?(member.name) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def unique_members(candidate_members)
|
|
31
49
|
seen = {}
|
|
32
|
-
|
|
50
|
+
candidate_members.each_with_object([]) do |member, memo|
|
|
33
51
|
next if seen.key?(member.name)
|
|
34
52
|
|
|
35
53
|
seen[member.name] = true
|
|
@@ -37,8 +55,66 @@ module Kettle
|
|
|
37
55
|
end
|
|
38
56
|
end
|
|
39
57
|
|
|
40
|
-
def
|
|
41
|
-
|
|
58
|
+
def sequential_install_results(candidate_members)
|
|
59
|
+
candidate_members.each_with_object([]) do |member, memo|
|
|
60
|
+
memo << install_member(member)
|
|
61
|
+
break memo unless memo.last.ok?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parallel_install_results
|
|
66
|
+
[local_dependency_members, selected_install_members].each_with_object([]) do |group, memo|
|
|
67
|
+
dependency_waves(group).each do |wave|
|
|
68
|
+
wave_results = run_install_wave(wave)
|
|
69
|
+
memo.concat(wave_results)
|
|
70
|
+
break unless wave_results.all?(&:ok?)
|
|
71
|
+
end
|
|
72
|
+
break memo unless memo.all?(&:ok?)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def run_install_wave(wave)
|
|
77
|
+
queue = Queue.new
|
|
78
|
+
wave.each_with_index { |member, index| queue << [index, member] }
|
|
79
|
+
ordered_results = Array.new(wave.length)
|
|
80
|
+
Array.new(install_jobs(wave)) do
|
|
81
|
+
Thread.new do
|
|
82
|
+
loop do
|
|
83
|
+
index, member = queue.pop(true)
|
|
84
|
+
ordered_results[index] = install_member(member)
|
|
85
|
+
rescue ThreadError
|
|
86
|
+
break
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end.each(&:join)
|
|
90
|
+
ordered_results.compact
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def dependency_waves(candidate_members)
|
|
94
|
+
by_name = candidate_members.to_h { |member| [member.name, member] }
|
|
95
|
+
pending = by_name.keys
|
|
96
|
+
completed = []
|
|
97
|
+
waves = []
|
|
98
|
+
until pending.empty?
|
|
99
|
+
wave_names = pending.select do |name|
|
|
100
|
+
selected_dependencies_for(by_name.fetch(name), by_name).all? { |dependency| completed.include?(dependency) }
|
|
101
|
+
end
|
|
102
|
+
raise Error, "cyclic install dependency order: #{pending.join(", ")}" if wave_names.empty?
|
|
103
|
+
|
|
104
|
+
waves << wave_names.map { |name| by_name.fetch(name) }
|
|
105
|
+
completed.concat(wave_names)
|
|
106
|
+
pending -= wave_names
|
|
107
|
+
end
|
|
108
|
+
waves
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def selected_dependencies_for(member, by_name)
|
|
112
|
+
Array(member.dependencies).map(&:to_s).select { |dependency| by_name.key?(dependency) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def install_jobs(candidate_members)
|
|
116
|
+
count = jobs ? jobs.to_i : [Etc.nprocessors, 4].min
|
|
117
|
+
[[count, 1].max, candidate_members.length].min
|
|
42
118
|
end
|
|
43
119
|
|
|
44
120
|
def member_from_path(path)
|
data/lib/kettle/family/report.rb
CHANGED
|
@@ -77,16 +77,41 @@ module Kettle
|
|
|
77
77
|
lines << "results:"
|
|
78
78
|
results.each do |result|
|
|
79
79
|
lines << " #{result_state(result)} #{result.member_name} #{result.phase} #{result.reason || ""}".rstrip
|
|
80
|
-
append_indented_output(lines, result.stdout) unless result
|
|
80
|
+
append_indented_output(lines, result.stdout) unless suppress_success_output?(result)
|
|
81
81
|
append_indented_output(lines, result.stderr) if !result.ok? && !result.stderr.to_s.empty?
|
|
82
82
|
lines << " resume: #{resume_hint_for(result)}" unless result.ok?
|
|
83
83
|
end
|
|
84
|
+
append_template_summary(lines) if command == "template"
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
def append_indented_output(lines, output)
|
|
87
88
|
output.to_s.each_line(chomp: true) { |line| lines << " #{line}" }
|
|
88
89
|
end
|
|
89
90
|
|
|
91
|
+
def suppress_success_output?(result)
|
|
92
|
+
result.stdout.to_s.empty? || (command == "template" && result.ok?)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def append_template_summary(lines)
|
|
96
|
+
template_results = results.select { |result| result.phase == "template" }
|
|
97
|
+
return if template_results.empty?
|
|
98
|
+
|
|
99
|
+
changed_files = template_results.sum { |result| template_changed_file_count(result) }
|
|
100
|
+
lines << "template summary:"
|
|
101
|
+
lines << " #{template_results.count(&:ok?)}/#{template_results.length} members ok"
|
|
102
|
+
lines << " #{changed_files} file#{"s" unless changed_files == 1} changed"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def template_changed_file_count(result)
|
|
106
|
+
payload = JSON.parse(result.stdout.to_s)
|
|
107
|
+
Array(payload["changed_files"] || payload[:changed_files]).length if payload.is_a?(Hash)
|
|
108
|
+
rescue JSON::ParserError
|
|
109
|
+
match = result.stdout.to_s.match(/(?:install|apply|prepare|template):\s+(\d+)\s+changed file/)
|
|
110
|
+
return match[1].to_i if match
|
|
111
|
+
|
|
112
|
+
0
|
|
113
|
+
end
|
|
114
|
+
|
|
90
115
|
def append_member_release_targets(lines)
|
|
91
116
|
return if member_release_target_branches.empty?
|
|
92
117
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "io/console"
|
|
4
4
|
require "json"
|
|
5
5
|
require "net/http"
|
|
6
|
+
require "etc"
|
|
7
|
+
require "open3"
|
|
6
8
|
require "uri"
|
|
7
9
|
|
|
8
10
|
module Kettle
|
|
@@ -20,8 +22,23 @@ module Kettle
|
|
|
20
22
|
"pull" => [["pull", %w[git pull --rebase]]],
|
|
21
23
|
"up" => [["pull", %w[git pull --rebase]], ["push", %w[git push]]]
|
|
22
24
|
}.freeze
|
|
25
|
+
TEMPLATE_QUIET_ENV = {
|
|
26
|
+
"KETTLE_JEM_QUIET" => "true",
|
|
27
|
+
"KETTLE_JEM_DEBUG" => "false",
|
|
28
|
+
"KETTLE_DEV_DEBUG" => "false",
|
|
29
|
+
"SMORG_RB_DEBUG" => "false",
|
|
30
|
+
"DEBUG" => "false",
|
|
31
|
+
"BUNDLE_QUIET" => "true",
|
|
32
|
+
"BUNDLE_DEBUG" => "false",
|
|
33
|
+
"BUNDLER_DEBUG" => "false",
|
|
34
|
+
"BUNDLE_VERBOSE" => "false",
|
|
35
|
+
"DEBUG_RESOLVER" => "false",
|
|
36
|
+
"BUNDLE_SILENCE_DEPRECATIONS" => "true",
|
|
37
|
+
"BUNDLE_SILENCE_ROOT_WARNING" => "true",
|
|
38
|
+
"BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES" => "true"
|
|
39
|
+
}.freeze
|
|
23
40
|
|
|
24
|
-
def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, local_ci: false, continue_ci_failures: false, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, env_overrides: {}, gem_signing_password: nil)
|
|
41
|
+
def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, local_ci: false, continue_ci_failures: false, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, env_overrides: {}, debug: false, gem_signing_password: nil, jobs: nil, progress_io: nil)
|
|
25
42
|
@command = command
|
|
26
43
|
@config = config
|
|
27
44
|
@members = members
|
|
@@ -38,7 +55,10 @@ module Kettle
|
|
|
38
55
|
@gha_sha_pins_upgrade = gha_sha_pins_upgrade
|
|
39
56
|
@gha_sha_pins_check = gha_sha_pins_check
|
|
40
57
|
@env_overrides = env_overrides
|
|
58
|
+
@debug = debug
|
|
41
59
|
@gem_signing_password = gem_signing_password
|
|
60
|
+
@jobs = jobs
|
|
61
|
+
@progress_io = progress_io
|
|
42
62
|
end
|
|
43
63
|
|
|
44
64
|
def results
|
|
@@ -51,7 +71,7 @@ module Kettle
|
|
|
51
71
|
|
|
52
72
|
private
|
|
53
73
|
|
|
54
|
-
attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :local_ci, :continue_ci_failures, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides
|
|
74
|
+
attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :local_ci, :continue_ci_failures, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides, :debug, :jobs, :progress_io
|
|
55
75
|
|
|
56
76
|
def current_branch_results(workflow_members)
|
|
57
77
|
return check_results(workflow_members) if command == "check"
|
|
@@ -62,6 +82,8 @@ module Kettle
|
|
|
62
82
|
end
|
|
63
83
|
|
|
64
84
|
def member_workflow_results(workflow_members)
|
|
85
|
+
return template_member_workflow_results(workflow_members) if command == "template" && execute && template_jobs(workflow_members) > 1
|
|
86
|
+
|
|
65
87
|
runner = CommandRunner.new(execute: execute, accept: accept)
|
|
66
88
|
workflow_members.each_with_object([]) do |member, memo|
|
|
67
89
|
if command == "template" && config.normalize_lockfiles?
|
|
@@ -79,6 +101,55 @@ module Kettle
|
|
|
79
101
|
end
|
|
80
102
|
end
|
|
81
103
|
|
|
104
|
+
def template_member_workflow_results(workflow_members)
|
|
105
|
+
queue = Queue.new
|
|
106
|
+
workflow_members.each_with_index { |member, index| queue << [index, member] }
|
|
107
|
+
ordered_results = Array.new(workflow_members.length)
|
|
108
|
+
mutex = Mutex.new
|
|
109
|
+
stop = false
|
|
110
|
+
emit_template_progress_start(workflow_members)
|
|
111
|
+
Array.new(template_jobs(workflow_members)) do
|
|
112
|
+
Thread.new do
|
|
113
|
+
loop do
|
|
114
|
+
break if mutex.synchronize { stop }
|
|
115
|
+
index, member = queue.pop(true)
|
|
116
|
+
member_results = template_results_for_member(member)
|
|
117
|
+
mutex.synchronize do
|
|
118
|
+
ordered_results[index] = member_results
|
|
119
|
+
stop = true unless member_results.all?(&:ok?)
|
|
120
|
+
emit_template_progress_mark(member_results)
|
|
121
|
+
end
|
|
122
|
+
rescue ThreadError
|
|
123
|
+
break
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end.each(&:join)
|
|
127
|
+
flattened = ordered_results.compact.flatten
|
|
128
|
+
emit_template_progress_summary(flattened)
|
|
129
|
+
flattened
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def template_results_for_member(member)
|
|
133
|
+
runner = CommandRunner.new(execute: execute, accept: accept)
|
|
134
|
+
[].tap do |memo|
|
|
135
|
+
if config.normalize_lockfiles?
|
|
136
|
+
normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "prepare_lockfiles")
|
|
137
|
+
return memo unless memo.last.ok?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
memo << runner.call(member: member, phase: command, command: workflow_command(member), env: workflow_env)
|
|
141
|
+
return memo unless memo.last.ok?
|
|
142
|
+
|
|
143
|
+
normalize_lockfiles(member: member, runner: runner, memo: memo, phase: "normalize_lockfiles")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def template_jobs(workflow_members)
|
|
148
|
+
requested = jobs || config.template_jobs
|
|
149
|
+
count = requested ? requested.to_i : [Etc.nprocessors, 4].min
|
|
150
|
+
[[count, 1].max, workflow_members.length].min
|
|
151
|
+
end
|
|
152
|
+
|
|
82
153
|
def check_results(workflow_members)
|
|
83
154
|
results = []
|
|
84
155
|
results.concat(BranchLaneAudit.new(config: config, members: workflow_members).results) unless config.branch_lanes.empty?
|
|
@@ -152,32 +223,72 @@ module Kettle
|
|
|
152
223
|
gha_sha_pins_upgrade: gha_sha_pins_upgrade,
|
|
153
224
|
gha_sha_pins_check: gha_sha_pins_check,
|
|
154
225
|
env_overrides: env_overrides,
|
|
155
|
-
gem_signing_password: @gem_signing_password
|
|
226
|
+
gem_signing_password: @gem_signing_password,
|
|
227
|
+
jobs: jobs,
|
|
228
|
+
progress_io: progress_io
|
|
156
229
|
)
|
|
157
230
|
end
|
|
158
231
|
|
|
159
232
|
def release_member_results(release_members, include_family_changelog: false)
|
|
160
|
-
runner =
|
|
233
|
+
runner = release_command_runner
|
|
161
234
|
results = []
|
|
162
235
|
append_family_changelog_result(runner: runner, memo: results) if include_family_changelog
|
|
163
236
|
return results unless results.all?(&:ok?)
|
|
237
|
+
return parallel_release_member_results(release_members, results) if parallel_release_members?(release_members)
|
|
164
238
|
|
|
165
239
|
release_members.each_with_object(results) do |member, memo|
|
|
240
|
+
memo.concat(release_results_for_member(member, runner: runner))
|
|
241
|
+
break memo unless memo.last.ok?
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def parallel_release_member_results(release_members, initial_results)
|
|
246
|
+
results = initial_results.dup
|
|
247
|
+
release_waves(release_members).each do |wave|
|
|
248
|
+
wave_results = run_release_wave(wave)
|
|
249
|
+
results.concat(wave_results.flatten)
|
|
250
|
+
break unless wave_results.all? { |member_results| member_results.all?(&:ok?) }
|
|
251
|
+
end
|
|
252
|
+
results
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def run_release_wave(wave)
|
|
256
|
+
queue = Queue.new
|
|
257
|
+
wave.each_with_index { |member, index| queue << [index, member] }
|
|
258
|
+
ordered_results = Array.new(wave.length)
|
|
259
|
+
wave_jobs = release_jobs(wave)
|
|
260
|
+
release_otp_coordinator&.queue_total = wave_jobs
|
|
261
|
+
Array.new(wave_jobs) do
|
|
262
|
+
Thread.new do
|
|
263
|
+
runner = release_command_runner
|
|
264
|
+
loop do
|
|
265
|
+
index, member = queue.pop(true)
|
|
266
|
+
ordered_results[index] = release_results_for_member(member, runner: runner)
|
|
267
|
+
rescue ThreadError
|
|
268
|
+
break
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end.each(&:join)
|
|
272
|
+
ordered_results.compact
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def release_results_for_member(member, runner:)
|
|
276
|
+
[].tap do |memo|
|
|
166
277
|
if skip_already_released?(member)
|
|
167
278
|
memo << already_released_result(member)
|
|
168
|
-
|
|
279
|
+
return memo
|
|
169
280
|
end
|
|
170
281
|
|
|
171
282
|
if config.release_normalize_lockfiles?
|
|
172
283
|
normalize_release_lockfiles(member: member, runner: runner, memo: memo)
|
|
173
|
-
|
|
284
|
+
return memo unless memo.last&.ok?
|
|
174
285
|
|
|
175
286
|
commit_normalized_lockfiles(branch_members: [member], runner: runner, memo: memo, reason: "release")
|
|
176
|
-
|
|
287
|
+
return memo unless memo.last&.ok?
|
|
177
288
|
end
|
|
178
289
|
|
|
179
290
|
append_release_internal_checks(member: member, memo: memo)
|
|
180
|
-
|
|
291
|
+
return memo unless memo.last(2).all?(&:ok?)
|
|
181
292
|
|
|
182
293
|
memo << runner.call(
|
|
183
294
|
member: member,
|
|
@@ -186,10 +297,9 @@ module Kettle
|
|
|
186
297
|
env: release_env,
|
|
187
298
|
interactive: release_command_interactive?
|
|
188
299
|
)
|
|
189
|
-
|
|
300
|
+
return memo unless memo.last.ok?
|
|
190
301
|
|
|
191
302
|
append_release_git_phases(member: member, runner: runner, memo: memo)
|
|
192
|
-
break memo unless memo.last.ok?
|
|
193
303
|
end
|
|
194
304
|
end
|
|
195
305
|
|
|
@@ -208,6 +318,66 @@ module Kettle
|
|
|
208
318
|
CommandRunner.new(execute: execute, accept: accept, gem_signing_password: @gem_signing_password)
|
|
209
319
|
end
|
|
210
320
|
|
|
321
|
+
def release_command_runner
|
|
322
|
+
CommandRunner.new(
|
|
323
|
+
execute: execute,
|
|
324
|
+
accept: accept,
|
|
325
|
+
gem_signing_password: @gem_signing_password,
|
|
326
|
+
otp_coordinator: release_otp_coordinator
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def release_otp_coordinator
|
|
331
|
+
return nil unless execute && release_command_interactive?
|
|
332
|
+
|
|
333
|
+
@release_otp_coordinator ||= CommandRunner::OtpCoordinator.new
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def parallel_release_members?(release_members)
|
|
337
|
+
execute &&
|
|
338
|
+
release_jobs(release_members) > 1 &&
|
|
339
|
+
release_members.length > 1 &&
|
|
340
|
+
distinct_git_roots?(release_members)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def release_jobs(release_members)
|
|
344
|
+
requested = jobs || config.release_jobs
|
|
345
|
+
count = requested ? requested.to_i : [Etc.nprocessors, 4].min
|
|
346
|
+
[[count, 1].max, release_members.length].min
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def release_waves(release_members)
|
|
350
|
+
by_name = release_members.to_h { |member| [member.name, member] }
|
|
351
|
+
pending = by_name.keys
|
|
352
|
+
completed = []
|
|
353
|
+
waves = []
|
|
354
|
+
until pending.empty?
|
|
355
|
+
wave_names = pending.select do |name|
|
|
356
|
+
selected_dependencies_for(by_name.fetch(name), by_name).all? { |dependency| completed.include?(dependency) }
|
|
357
|
+
end
|
|
358
|
+
raise Error, "cyclic release dependency order: #{pending.join(", ")}" if wave_names.empty?
|
|
359
|
+
|
|
360
|
+
waves << wave_names.map { |name| by_name.fetch(name) }
|
|
361
|
+
completed.concat(wave_names)
|
|
362
|
+
pending -= wave_names
|
|
363
|
+
end
|
|
364
|
+
waves
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def selected_dependencies_for(member, by_name)
|
|
368
|
+
Array(member.dependencies).map(&:to_s).select { |dependency| by_name.key?(dependency) }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def distinct_git_roots?(release_members)
|
|
372
|
+
roots = release_members.map { |member| git_root_for(member) }
|
|
373
|
+
roots.uniq.length == roots.length
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def git_root_for(member)
|
|
377
|
+
stdout, _stderr, status = Open3.capture3("git", "rev-parse", "--show-toplevel", chdir: member.root)
|
|
378
|
+
status.success? ? stdout.strip : File.expand_path(member.root)
|
|
379
|
+
end
|
|
380
|
+
|
|
211
381
|
def rediscovered_selected_members(selected_names)
|
|
212
382
|
discovered = Discovery.new(config: config).members
|
|
213
383
|
ordered = Orderer.new(members: discovered, mode: config.order_mode, hints: config.order_hints).ordered
|
|
@@ -394,6 +564,7 @@ module Kettle
|
|
|
394
564
|
|
|
395
565
|
def template_command(member)
|
|
396
566
|
command_text = config.template_command || default_template_command(member)
|
|
567
|
+
command_text = append_template_family_args(command_text) if kettle_jem_template_command?(command_text)
|
|
397
568
|
return command_text if commit
|
|
398
569
|
return command_text if command_text.is_a?(Array) && command_text.include?("--skip-commit")
|
|
399
570
|
return [*command_text, "--skip-commit"] if command_text.is_a?(Array)
|
|
@@ -423,9 +594,56 @@ module Kettle
|
|
|
423
594
|
env["KJ_REPOSITORY_TOPOLOGY"] = config.template_repository_topology if config.template_repository_topology
|
|
424
595
|
end
|
|
425
596
|
env.merge!(env_overrides)
|
|
597
|
+
env.merge!(TEMPLATE_QUIET_ENV) if command == "template" && !debug
|
|
426
598
|
end
|
|
427
599
|
end
|
|
428
600
|
|
|
601
|
+
def kettle_jem_template_command?(command_text)
|
|
602
|
+
command_text.is_a?(Array) ? command_text.map(&:to_s).include?("kettle-jem") : command_text.to_s.include?("kettle-jem")
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def append_template_family_args(command_text)
|
|
606
|
+
args = []
|
|
607
|
+
args << "--quiet" unless command_includes_arg?(command_text, "--quiet")
|
|
608
|
+
args << "--json" unless command_includes_arg?(command_text, "--json")
|
|
609
|
+
append_command_args(command_text, args)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def emit_template_progress_start(workflow_members)
|
|
613
|
+
return unless progress_io
|
|
614
|
+
|
|
615
|
+
progress_io.puts("templating #{workflow_members.length} member#{"s" unless workflow_members.length == 1} with #{template_jobs(workflow_members)} job#{"s" unless template_jobs(workflow_members) == 1}:")
|
|
616
|
+
progress_io.flush if progress_io.respond_to?(:flush)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def emit_template_progress_mark(member_results)
|
|
620
|
+
return unless progress_io
|
|
621
|
+
|
|
622
|
+
template_result = member_results.find { |result| result.phase == "template" } || member_results.last
|
|
623
|
+
progress_io.print(template_result.ok? ? "." : "F")
|
|
624
|
+
progress_io.flush if progress_io.respond_to?(:flush)
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def emit_template_progress_summary(results)
|
|
628
|
+
return unless progress_io
|
|
629
|
+
|
|
630
|
+
template_results = results.select { |result| result.phase == "template" }
|
|
631
|
+
changed_files = template_results.sum { |result| template_changed_file_count(result) }
|
|
632
|
+
progress_io.puts
|
|
633
|
+
progress_io.puts("template summary: #{template_results.count(&:ok?)}/#{template_results.length} members ok, #{changed_files} file#{"s" unless changed_files == 1} changed")
|
|
634
|
+
progress_io.flush if progress_io.respond_to?(:flush)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def template_changed_file_count(result)
|
|
638
|
+
payload = JSON.parse(result.stdout.to_s)
|
|
639
|
+
Array(payload["changed_files"] || payload[:changed_files]).length if payload.is_a?(Hash)
|
|
640
|
+
rescue JSON::ParserError
|
|
641
|
+
match = result.stdout.to_s.match(/(?:install|apply|prepare|template):\s+(\d+)\s+changed file/)
|
|
642
|
+
return match[1].to_i if match
|
|
643
|
+
|
|
644
|
+
0
|
|
645
|
+
end
|
|
646
|
+
|
|
429
647
|
def normalize_lockfiles(member:, runner:, memo:, phase:)
|
|
430
648
|
return unless config.normalize_lockfiles?
|
|
431
649
|
|
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: 0.1.
|
|
4
|
+
version: 0.1.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -310,10 +310,10 @@ licenses:
|
|
|
310
310
|
- AGPL-3.0-only
|
|
311
311
|
metadata:
|
|
312
312
|
homepage_uri: https://kettle-family.galtzo.com
|
|
313
|
-
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.
|
|
314
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.
|
|
313
|
+
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v0.1.20
|
|
314
|
+
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v0.1.20/CHANGELOG.md
|
|
315
315
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
|
|
316
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.
|
|
316
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-family/0.1.20
|
|
317
317
|
funding_uri: https://github.com/sponsors/pboling
|
|
318
318
|
wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
|
|
319
319
|
news_uri: https://www.railsbling.com/tags/kettle-family
|
metadata.gz.sig
CHANGED
|
Binary file
|