kettle-family 1.2.13 → 1.2.15
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 +74 -1
- data/README.md +1 -1
- data/lib/kettle/family/cli.rb +4 -0
- data/lib/kettle/family/command_result.rb +4 -2
- data/lib/kettle/family/command_runner.rb +165 -53
- data/lib/kettle/family/config.rb +4 -0
- data/lib/kettle/family/report.rb +21 -1
- data/lib/kettle/family/secrets.rb +12 -2
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +203 -46
- data/lib/kettle/family/workflow_progress.rb +3 -2
- data.tar.gz.sig +0 -0
- metadata +6 -6
- 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: 9ccc9063ef81ee8d6b8bee27424903371b47863d9fb2903b4461c2e132b757d9
|
|
4
|
+
data.tar.gz: ec46a0708e899590fb95d67d1f8e9dbe002195b21cf9a635c2c8b4a726a693c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 883955e631cd8b19ee2219a98267465307dec049d35c7f209f61ce23465eeb05665a88954b0dcac39e70f980aa39a02bdc6f2539c4897ef6418412a7d5ad1054
|
|
7
|
+
data.tar.gz: 8c584b648f8fe2914ac5077b8b181f398b2fe1507eb30278927c13408f5b3b7e6c4a7b1beba417f1dcc1a0ae80a0e7a460d433bb1336615a6ccb3759e2abee34
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,75 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.2.15] - 2026-07-31
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.2.15][1.2.15t]
|
|
36
|
+
- COVERAGE: 94.03% -- 4192/4458 lines in 28 files
|
|
37
|
+
- BRANCH COVERAGE: 76.46% -- 1634/2137 branches in 28 files
|
|
38
|
+
- 28.88% documented
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Family-owned release OTP lookups now render as structured secret-provider
|
|
43
|
+
progress events, including a visible `👀 🔒` authorization prompt cue, instead
|
|
44
|
+
of printing loose MFA status lines through the release tape.
|
|
45
|
+
|
|
46
|
+
## [1.2.14] - 2026-07-31
|
|
47
|
+
|
|
48
|
+
- TAG: [v1.2.14][1.2.14t]
|
|
49
|
+
- COVERAGE: 94.22% -- 4175/4431 lines in 28 files
|
|
50
|
+
- BRANCH COVERAGE: 76.69% -- 1622/2115 branches in 28 files
|
|
51
|
+
- 28.88% documented
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- Release publish/build commands now write per-member transcript logs under
|
|
56
|
+
`tmp/kettle-family/release-*`, and failure summaries include the log path.
|
|
57
|
+
|
|
58
|
+
- Release progress now renders child `kettle-release` `remote_parity` events,
|
|
59
|
+
keeping remote fetch, skip, failure, and completion activity visible without
|
|
60
|
+
scraping raw command output.
|
|
61
|
+
|
|
62
|
+
- Release progress now renders child `kettle-release` `ci_monitor` events with
|
|
63
|
+
provider and workflow or pipeline context, so CI activity is visible as
|
|
64
|
+
structured progress.
|
|
65
|
+
|
|
66
|
+
- Release progress now renders child `kettle-release` `pre_release` events, so
|
|
67
|
+
pre-release checks are visible as structured progress while raw output stays
|
|
68
|
+
in the transcript log.
|
|
69
|
+
|
|
70
|
+
- Release progress now renders child `kettle-changelog` `changelog` events, so
|
|
71
|
+
release plan, coverage, and changelog update activity is visible as structured
|
|
72
|
+
progress.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- Family release now keeps raw child `kettle-release` output in the per-member
|
|
77
|
+
transcript log by default and renders structured NDJSON progress in the
|
|
78
|
+
terminal, with raw passthrough still available under verbose/debug output.
|
|
79
|
+
- Release summary footers now include the transcript log directory when release
|
|
80
|
+
logs were written, including successful runs where raw output stayed quiet.
|
|
81
|
+
- Release progress now renders child `command_step` summaries, so phases can
|
|
82
|
+
show context such as `release:bundle_lock:Gemfile` or
|
|
83
|
+
`release:yard:documentation`.
|
|
84
|
+
- Release progress now renders child `release_lockfile` and `release_probe`
|
|
85
|
+
events, so lockfile reset retries and published-gem availability probes show
|
|
86
|
+
as structured progress.
|
|
87
|
+
- Release progress now renders GitHub CI wait/start/tick events with completion
|
|
88
|
+
counts, so CI monitoring no longer appears stuck while waiting for workflows.
|
|
89
|
+
- Release preflight now renders through the shared progress event tape instead
|
|
90
|
+
of printing separate start and finish lines for each successful phase.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- Release preflight no longer prints a nested 1Password authorization alert
|
|
95
|
+
between the phase start and finish lines; the preflight phase itself is the
|
|
96
|
+
user-facing authorization notice.
|
|
97
|
+
|
|
98
|
+
- Family release runs can now pass configured required release remotes through
|
|
99
|
+
to `kettle-release`, allowing mirrors to remain optional while primary remotes
|
|
100
|
+
still block release parity failures.
|
|
101
|
+
|
|
33
102
|
## [1.2.13] - 2026-07-31
|
|
34
103
|
|
|
35
104
|
- TAG: [v1.2.13][1.2.13t]
|
|
@@ -1443,7 +1512,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
1443
1512
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
1444
1513
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
1445
1514
|
|
|
1446
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.
|
|
1515
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.15...HEAD
|
|
1516
|
+
[1.2.15]: https://github.com/kettle-dev/kettle-family/compare/v1.2.14...v1.2.15
|
|
1517
|
+
[1.2.15t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.15
|
|
1518
|
+
[1.2.14]: https://github.com/kettle-dev/kettle-family/compare/v1.2.13...v1.2.14
|
|
1519
|
+
[1.2.14t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.14
|
|
1447
1520
|
[1.2.13]: https://github.com/kettle-dev/kettle-family/compare/v1.2.12...v1.2.13
|
|
1448
1521
|
[1.2.13t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.13
|
|
1449
1522
|
[1.2.12]: https://github.com/kettle-dev/kettle-family/compare/v1.2.11...v1.2.12
|
data/README.md
CHANGED
|
@@ -819,7 +819,7 @@ Thanks for RTFM. ☺️
|
|
|
819
819
|
[📌gitmoji]: https://gitmoji.dev
|
|
820
820
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
821
821
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
822
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.
|
|
822
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.458-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
823
823
|
[🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
|
|
824
824
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
825
825
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/lib/kettle/family/cli.rb
CHANGED
|
@@ -151,6 +151,7 @@ module Kettle
|
|
|
151
151
|
release_ci_workflows: nil,
|
|
152
152
|
release_skip_bundle_audit: false,
|
|
153
153
|
release_skip_remotes: nil,
|
|
154
|
+
release_required_remotes: nil,
|
|
154
155
|
release_secrets_provider: nil,
|
|
155
156
|
accept: true,
|
|
156
157
|
tag: false,
|
|
@@ -464,6 +465,7 @@ module Kettle
|
|
|
464
465
|
option :ci_workflows, long: "--ci-workflows", value: {type: String, usage: "LIST"}, desc: "Pass a comma-separated CI workflow monitor subset through to kettle-release commands"
|
|
465
466
|
option :skip_bundle_audit, long: "--skip-bundle-audit", desc: "Skip bundle:audit/update during release rake checks"
|
|
466
467
|
option :skip_remotes, long: "--skip-remotes", value: {type: String, usage: "LIST"}, desc: "Pass a comma-separated git remote skip list through to kettle-release commands"
|
|
468
|
+
option :required_remotes, long: "--required-remotes", value: {type: String, usage: "LIST"}, desc: "Pass a comma-separated required git remote list through to kettle-release commands"
|
|
467
469
|
option :secrets_provider, long: "--secrets-provider", value: {type: String, usage: "NAME"}, desc: "Release secrets provider: interactive, 1password"
|
|
468
470
|
option :no_auto_floors, long: "--no-auto-floors", desc: "Do not raise family dependency floors between member releases" do
|
|
469
471
|
options[:no_auto_floors] = true
|
|
@@ -487,6 +489,7 @@ module Kettle
|
|
|
487
489
|
release_ci_workflows: options[:ci_workflows],
|
|
488
490
|
release_skip_bundle_audit: truthy_option?(:skip_bundle_audit),
|
|
489
491
|
release_skip_remotes: options[:skip_remotes],
|
|
492
|
+
release_required_remotes: options[:required_remotes],
|
|
490
493
|
release_secrets_provider: options[:secrets_provider],
|
|
491
494
|
release_auto_dependency_floors: !truthy_option?(:no_auto_floors),
|
|
492
495
|
accept: !options.key?(:accept) || options[:accept],
|
|
@@ -662,6 +665,7 @@ module Kettle
|
|
|
662
665
|
ci_workflows: options[:release_ci_workflows],
|
|
663
666
|
skip_bundle_audit: options[:release_skip_bundle_audit],
|
|
664
667
|
skip_remotes: options[:release_skip_remotes],
|
|
668
|
+
required_remotes: options[:release_required_remotes],
|
|
665
669
|
secrets_provider: release_secrets_provider(command: command, config: config, options: options),
|
|
666
670
|
auto_dependency_floors: options[:release_auto_dependency_floors],
|
|
667
671
|
gha_sha_pins_upgrade: options[:gha_sha_pins_upgrade],
|
|
@@ -15,7 +15,8 @@ module Kettle
|
|
|
15
15
|
:skipped,
|
|
16
16
|
:reason,
|
|
17
17
|
:branch,
|
|
18
|
-
:output_streamed
|
|
18
|
+
:output_streamed,
|
|
19
|
+
:log_path
|
|
19
20
|
) do
|
|
20
21
|
def to_h
|
|
21
22
|
{
|
|
@@ -31,7 +32,8 @@ module Kettle
|
|
|
31
32
|
"elapsed_seconds" => elapsed_seconds,
|
|
32
33
|
"skipped" => skipped,
|
|
33
34
|
"reason" => reason,
|
|
34
|
-
"output_streamed" => output_streamed
|
|
35
|
+
"output_streamed" => output_streamed?,
|
|
36
|
+
"log_path" => log_path
|
|
35
37
|
}
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "io/console"
|
|
5
|
+
require "fileutils"
|
|
5
6
|
|
|
6
7
|
module Kettle
|
|
7
8
|
module Family
|
|
@@ -11,10 +12,11 @@ module Kettle
|
|
|
11
12
|
].freeze
|
|
12
13
|
|
|
13
14
|
class OtpCoordinator
|
|
14
|
-
def initialize(input: $stdin, output: $stdout, queue_total: nil, secrets_provider: nil)
|
|
15
|
+
def initialize(input: $stdin, output: $stdout, queue_total: nil, secrets_provider: nil, event_handler: nil)
|
|
15
16
|
@input = input
|
|
16
17
|
@output = output
|
|
17
18
|
@secrets_provider = secrets_provider
|
|
19
|
+
@event_handler = event_handler
|
|
18
20
|
@mutex = Mutex.new
|
|
19
21
|
@condition = ConditionVariable.new
|
|
20
22
|
@prompting = false
|
|
@@ -40,7 +42,7 @@ module Kettle
|
|
|
40
42
|
if @prompting
|
|
41
43
|
generation = @generation
|
|
42
44
|
@queued_count += 1
|
|
43
|
-
render_queue_status_locked
|
|
45
|
+
render_queue_status_locked(member_name: member_name)
|
|
44
46
|
return wait_for_response(generation)
|
|
45
47
|
end
|
|
46
48
|
|
|
@@ -52,7 +54,7 @@ module Kettle
|
|
|
52
54
|
start_prompt_locked(member_name: member_name)
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
response = read_response(chunk: chunk)
|
|
57
|
+
response = read_response(chunk: chunk, member_name: member_name)
|
|
56
58
|
close_queue
|
|
57
59
|
@mutex.synchronize do
|
|
58
60
|
@response = response
|
|
@@ -66,7 +68,7 @@ module Kettle
|
|
|
66
68
|
|
|
67
69
|
private
|
|
68
70
|
|
|
69
|
-
attr_reader :secrets_provider
|
|
71
|
+
attr_reader :secrets_provider, :event_handler
|
|
70
72
|
|
|
71
73
|
def wait_for_response(generation)
|
|
72
74
|
@condition.wait(@mutex) while @prompting
|
|
@@ -82,22 +84,49 @@ module Kettle
|
|
|
82
84
|
end
|
|
83
85
|
|
|
84
86
|
def start_prompt_locked(member_name:)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
unless emit_event(
|
|
88
|
+
member_name: member_name,
|
|
89
|
+
action: "mfa_requested",
|
|
90
|
+
label: "RubyGems MFA",
|
|
91
|
+
status: "requested",
|
|
92
|
+
mark: ">"
|
|
93
|
+
)
|
|
94
|
+
@output.puts
|
|
95
|
+
@output.puts("[#{member_name}] RubyGems MFA requested.")
|
|
96
|
+
end
|
|
97
|
+
render_queue_status_locked(member_name: member_name)
|
|
98
|
+
return if event_handler
|
|
99
|
+
|
|
88
100
|
@output.puts("Queued prompts at entry will share this code; later prompts will ask again.")
|
|
89
101
|
end
|
|
90
102
|
|
|
91
|
-
def render_queue_status_locked
|
|
103
|
+
def render_queue_status_locked(member_name:)
|
|
92
104
|
suffix = @queue_total ? " / #{@queue_total}" : ""
|
|
105
|
+
return if emit_event(
|
|
106
|
+
member_name: member_name,
|
|
107
|
+
action: "otp_queue",
|
|
108
|
+
label: "RubyGems MFA prompts",
|
|
109
|
+
status: "queued",
|
|
110
|
+
mark: ">",
|
|
111
|
+
queued: @queued_count,
|
|
112
|
+
total: @queue_total
|
|
113
|
+
)
|
|
114
|
+
|
|
93
115
|
@output.puts("RubyGems MFA prompts queued: #{@queued_count}#{suffix}")
|
|
94
116
|
@output.flush if @output.respond_to?(:flush)
|
|
95
117
|
end
|
|
96
118
|
|
|
97
|
-
def read_response(chunk:)
|
|
98
|
-
provided = read_provider_response
|
|
119
|
+
def read_response(chunk:, member_name:)
|
|
120
|
+
provided = read_provider_response(member_name: member_name)
|
|
99
121
|
unless provided.empty?
|
|
100
|
-
|
|
122
|
+
emit_event(
|
|
123
|
+
member_name: member_name,
|
|
124
|
+
action: "prompt_response",
|
|
125
|
+
label: "RubyGems MFA code",
|
|
126
|
+
source: secret_provider_source,
|
|
127
|
+
status: "ok",
|
|
128
|
+
mark: "."
|
|
129
|
+
) || @output.puts("RubyGems MFA code loaded from configured secrets provider.")
|
|
101
130
|
return provided
|
|
102
131
|
end
|
|
103
132
|
|
|
@@ -116,17 +145,57 @@ module Kettle
|
|
|
116
145
|
chunk.to_s.lines.last&.strip.to_s.empty? ? "Code:" : chunk.to_s.lines.last.strip
|
|
117
146
|
end
|
|
118
147
|
|
|
119
|
-
def read_provider_response
|
|
148
|
+
def read_provider_response(member_name:)
|
|
120
149
|
return "" unless secrets_provider
|
|
121
150
|
|
|
122
151
|
@output.print("\a") if @output.respond_to?(:print)
|
|
152
|
+
emit_event(
|
|
153
|
+
member_name: member_name,
|
|
154
|
+
action: "prompt_request",
|
|
155
|
+
label: "👀 🔒 watch for authorization prompt",
|
|
156
|
+
source: secret_provider_source,
|
|
157
|
+
status: "started",
|
|
158
|
+
mark: ">"
|
|
159
|
+
)
|
|
123
160
|
secrets_provider.rubygems_otp.to_s
|
|
124
161
|
rescue Error => error
|
|
125
162
|
raise unless @input.respond_to?(:tty?) && @input.tty?
|
|
126
163
|
|
|
164
|
+
emit_event(
|
|
165
|
+
member_name: member_name,
|
|
166
|
+
action: "prompt_response",
|
|
167
|
+
label: "RubyGems MFA code",
|
|
168
|
+
source: secret_provider_source,
|
|
169
|
+
status: "failed",
|
|
170
|
+
mark: "F"
|
|
171
|
+
)
|
|
127
172
|
@output.puts("#{error.message}; falling back to manual OTP entry.")
|
|
128
173
|
""
|
|
129
174
|
end
|
|
175
|
+
|
|
176
|
+
def emit_event(member_name:, action:, label:, status:, mark:, **payload)
|
|
177
|
+
return false unless event_handler
|
|
178
|
+
|
|
179
|
+
event_handler.call(
|
|
180
|
+
member_name,
|
|
181
|
+
{
|
|
182
|
+
"event_version" => 1,
|
|
183
|
+
"type" => "secret_provider",
|
|
184
|
+
"action" => action,
|
|
185
|
+
"label" => label,
|
|
186
|
+
"status" => status,
|
|
187
|
+
"mark" => mark
|
|
188
|
+
}.merge(payload.transform_keys(&:to_s))
|
|
189
|
+
)
|
|
190
|
+
true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def secret_provider_source
|
|
194
|
+
return "" unless secrets_provider
|
|
195
|
+
return "1Password" if secrets_provider.is_a?(Secrets::OnePassword)
|
|
196
|
+
|
|
197
|
+
secrets_provider.class.name.to_s.split("::").last.to_s
|
|
198
|
+
end
|
|
130
199
|
end
|
|
131
200
|
|
|
132
201
|
FailureStatus = Struct.new(:exitstatus) do
|
|
@@ -143,26 +212,33 @@ module Kettle
|
|
|
143
212
|
@otp_coordinator = otp_coordinator
|
|
144
213
|
end
|
|
145
214
|
|
|
146
|
-
def call(member:, phase:, command:, env: {}, interactive: false, stdout_line_handler: nil)
|
|
215
|
+
def call(member:, phase:, command:, env: {}, interactive: false, stdout_line_handler: nil, log_path: nil, passthrough_output: true)
|
|
147
216
|
argv = command_argv(member: member, command: command, env: env)
|
|
148
217
|
process_env = process_env(member: member, env: env)
|
|
149
218
|
spawn_options = process_options
|
|
150
219
|
return skipped_result(member: member, phase: phase, argv: argv) unless execute
|
|
151
220
|
|
|
152
221
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
153
|
-
stdout, stderr, status =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
222
|
+
stdout, stderr, status = with_transcript(log_path, argv: argv, chdir: member.root) do |log_io|
|
|
223
|
+
if interactive
|
|
224
|
+
run_interactive(
|
|
225
|
+
env: process_env,
|
|
226
|
+
argv: argv,
|
|
227
|
+
chdir: member.root,
|
|
228
|
+
member_name: member.name,
|
|
229
|
+
process_options: spawn_options,
|
|
230
|
+
stdout_line_handler: stdout_line_handler,
|
|
231
|
+
log_io: log_io,
|
|
232
|
+
passthrough_output: passthrough_output
|
|
233
|
+
)
|
|
234
|
+
elsif stdout_line_handler
|
|
235
|
+
run_streaming(env: process_env, argv: argv, chdir: member.root, process_options: spawn_options, stdout_line_handler: stdout_line_handler, log_io: log_io)
|
|
236
|
+
else
|
|
237
|
+
result = Open3.capture3(process_env, *argv, chdir: member.root, **spawn_options)
|
|
238
|
+
transcript_write(log_io, result[0])
|
|
239
|
+
transcript_write(log_io, result[1])
|
|
240
|
+
result
|
|
241
|
+
end
|
|
166
242
|
end
|
|
167
243
|
stdout = normalize_output(stdout)
|
|
168
244
|
stderr = normalize_output(stderr)
|
|
@@ -179,7 +255,8 @@ module Kettle
|
|
|
179
255
|
elapsed_seconds: elapsed.round(3),
|
|
180
256
|
skipped: false,
|
|
181
257
|
reason: status.success? ? nil : "command failed",
|
|
182
|
-
output_streamed: interactive
|
|
258
|
+
output_streamed: interactive,
|
|
259
|
+
log_path: log_path
|
|
183
260
|
)
|
|
184
261
|
end
|
|
185
262
|
|
|
@@ -187,7 +264,7 @@ module Kettle
|
|
|
187
264
|
|
|
188
265
|
attr_reader :execute, :accept, :gem_signing_password, :otp_coordinator
|
|
189
266
|
|
|
190
|
-
def run_streaming(env:, argv:, chdir:, process_options:, stdout_line_handler:)
|
|
267
|
+
def run_streaming(env:, argv:, chdir:, process_options:, stdout_line_handler:, log_io: nil)
|
|
191
268
|
captured_stdout = +""
|
|
192
269
|
captured_stderr = +""
|
|
193
270
|
stdout_line_buffer = +""
|
|
@@ -197,12 +274,14 @@ module Kettle
|
|
|
197
274
|
until readers.empty?
|
|
198
275
|
ready = IO.select(readers)
|
|
199
276
|
ready.first.each do |reader|
|
|
277
|
+
chunk = reader.readpartial(1024)
|
|
200
278
|
if reader.equal?(output)
|
|
201
|
-
chunk = reader.readpartial(1024)
|
|
202
279
|
captured_stdout << chunk
|
|
280
|
+
transcript_write(log_io, chunk)
|
|
203
281
|
stdout_line_buffer = stream_stdout_lines(stdout_line_buffer, chunk, stdout_line_handler)
|
|
204
282
|
else
|
|
205
|
-
captured_stderr <<
|
|
283
|
+
captured_stderr << chunk
|
|
284
|
+
transcript_write(log_io, chunk)
|
|
206
285
|
end
|
|
207
286
|
rescue EOFError
|
|
208
287
|
readers.delete(reader)
|
|
@@ -222,7 +301,7 @@ module Kettle
|
|
|
222
301
|
remainder
|
|
223
302
|
end
|
|
224
303
|
|
|
225
|
-
def run_interactive(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
|
|
304
|
+
def run_interactive(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
|
|
226
305
|
if pty_available?
|
|
227
306
|
return run_interactive_pty(
|
|
228
307
|
env: env,
|
|
@@ -230,7 +309,9 @@ module Kettle
|
|
|
230
309
|
chdir: chdir,
|
|
231
310
|
member_name: member_name,
|
|
232
311
|
process_options: process_options,
|
|
233
|
-
stdout_line_handler: stdout_line_handler
|
|
312
|
+
stdout_line_handler: stdout_line_handler,
|
|
313
|
+
log_io: log_io,
|
|
314
|
+
passthrough_output: passthrough_output
|
|
234
315
|
)
|
|
235
316
|
end
|
|
236
317
|
|
|
@@ -240,11 +321,13 @@ module Kettle
|
|
|
240
321
|
chdir: chdir,
|
|
241
322
|
member_name: member_name,
|
|
242
323
|
process_options: process_options,
|
|
243
|
-
stdout_line_handler: stdout_line_handler
|
|
324
|
+
stdout_line_handler: stdout_line_handler,
|
|
325
|
+
log_io: log_io,
|
|
326
|
+
passthrough_output: passthrough_output
|
|
244
327
|
)
|
|
245
328
|
end
|
|
246
329
|
|
|
247
|
-
def run_interactive_pty(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
|
|
330
|
+
def run_interactive_pty(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
|
|
248
331
|
stdout = +""
|
|
249
332
|
stderr = +""
|
|
250
333
|
stdout_line_buffer = +""
|
|
@@ -259,10 +342,12 @@ module Kettle
|
|
|
259
342
|
if reader.equal?(output)
|
|
260
343
|
chunk = output.readpartial(1024)
|
|
261
344
|
stdout << chunk
|
|
262
|
-
|
|
345
|
+
transcript_write(log_io, chunk)
|
|
346
|
+
stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler, passthrough_output: passthrough_output)
|
|
263
347
|
handle_interactive_prompt(input, chunk, member_name: member_name)
|
|
264
348
|
else
|
|
265
|
-
|
|
349
|
+
chunk = $stdin.readpartial(1024)
|
|
350
|
+
input.write(chunk)
|
|
266
351
|
end
|
|
267
352
|
end
|
|
268
353
|
end
|
|
@@ -270,16 +355,17 @@ module Kettle
|
|
|
270
355
|
# PTY raises EIO when the child process exits after closing the slave.
|
|
271
356
|
rescue Error => error
|
|
272
357
|
stderr << "#{error.message}\n"
|
|
358
|
+
transcript_write(log_io, "#{error.message}\n")
|
|
273
359
|
terminate_process(pid)
|
|
274
360
|
end
|
|
275
|
-
flush_interactive_stdout(stdout_line_buffer, stdout_line_handler)
|
|
361
|
+
flush_interactive_stdout(stdout_line_buffer, stdout_line_handler, passthrough_output: passthrough_output)
|
|
276
362
|
_, status = Process.wait2(pid)
|
|
277
363
|
status = failure_status if !stderr.empty? && status.success?
|
|
278
364
|
end
|
|
279
365
|
[stdout, stderr, status]
|
|
280
366
|
end
|
|
281
367
|
|
|
282
|
-
def run_interactive_open3(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:)
|
|
368
|
+
def run_interactive_open3(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
|
|
283
369
|
captured_stdout = +""
|
|
284
370
|
captured_stderr = +""
|
|
285
371
|
stdout_line_buffer = +""
|
|
@@ -303,19 +389,22 @@ module Kettle
|
|
|
303
389
|
readers,
|
|
304
390
|
member_name: member_name,
|
|
305
391
|
stdout_line_buffer: stdout_line_buffer,
|
|
306
|
-
stdout_line_handler: stdout_line_handler
|
|
392
|
+
stdout_line_handler: stdout_line_handler,
|
|
393
|
+
log_io: log_io,
|
|
394
|
+
passthrough_output: passthrough_output
|
|
307
395
|
)
|
|
308
396
|
end
|
|
309
397
|
end
|
|
310
398
|
end
|
|
311
399
|
rescue Error => error
|
|
312
400
|
captured_stderr << "#{error.message}\n"
|
|
401
|
+
transcript_write(log_io, "#{error.message}\n")
|
|
313
402
|
terminate_process(wait_thread.pid)
|
|
314
403
|
status = failure_status
|
|
315
404
|
else
|
|
316
405
|
status = wait_thread.value
|
|
317
406
|
end
|
|
318
|
-
flush_interactive_stdout(stdout_line_buffer, stdout_line_handler)
|
|
407
|
+
flush_interactive_stdout(stdout_line_buffer, stdout_line_handler, passthrough_output: passthrough_output)
|
|
319
408
|
end
|
|
320
409
|
[captured_stdout, captured_stderr, status]
|
|
321
410
|
end
|
|
@@ -329,15 +418,19 @@ module Kettle
|
|
|
329
418
|
readers,
|
|
330
419
|
member_name:,
|
|
331
420
|
stdout_line_buffer:,
|
|
332
|
-
stdout_line_handler
|
|
421
|
+
stdout_line_handler:,
|
|
422
|
+
log_io: nil,
|
|
423
|
+
passthrough_output: true
|
|
333
424
|
)
|
|
334
425
|
chunk = reader.readpartial(1024)
|
|
335
426
|
if reader.equal?(output)
|
|
336
427
|
captured_stdout << chunk
|
|
337
|
-
|
|
428
|
+
transcript_write(log_io, chunk)
|
|
429
|
+
stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler, passthrough_output: passthrough_output)
|
|
338
430
|
else
|
|
339
431
|
captured_stderr << chunk
|
|
340
|
-
|
|
432
|
+
transcript_write(log_io, chunk)
|
|
433
|
+
$stderr.print(chunk) if passthrough_output
|
|
341
434
|
end
|
|
342
435
|
handle_interactive_prompt(input, chunk, member_name: member_name)
|
|
343
436
|
stdout_line_buffer
|
|
@@ -346,36 +439,55 @@ module Kettle
|
|
|
346
439
|
stdout_line_buffer
|
|
347
440
|
end
|
|
348
441
|
|
|
349
|
-
def
|
|
350
|
-
return
|
|
442
|
+
def with_transcript(log_path, argv:, chdir:)
|
|
443
|
+
return yield(nil) if log_path.to_s.empty?
|
|
444
|
+
|
|
445
|
+
FileUtils.mkdir_p(File.dirname(log_path))
|
|
446
|
+
File.open(log_path, "wb") do |log_io|
|
|
447
|
+
log_io.sync = true
|
|
448
|
+
log_io.puts("$ #{argv.join(" ")}")
|
|
449
|
+
log_io.puts("workdir: #{chdir}")
|
|
450
|
+
log_io.puts
|
|
451
|
+
yield(log_io)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def transcript_write(log_io, chunk)
|
|
456
|
+
return if log_io.nil? || chunk.to_s.empty?
|
|
457
|
+
|
|
458
|
+
log_io.write(chunk)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def print_interactive_stdout(buffer, chunk, stdout_line_handler, passthrough_output: true)
|
|
462
|
+
return print_interactive_chunk(chunk, passthrough_output: passthrough_output) unless stdout_line_handler
|
|
351
463
|
|
|
352
464
|
pending = buffer + chunk
|
|
353
465
|
lines = pending.lines
|
|
354
466
|
remainder = pending.end_with?("\n") ? +"" : lines.pop.to_s
|
|
355
|
-
lines.each { |line| print_interactive_line(line, stdout_line_handler) }
|
|
467
|
+
lines.each { |line| print_interactive_line(line, stdout_line_handler, passthrough_output: passthrough_output) }
|
|
356
468
|
return +"" if remainder.empty?
|
|
357
469
|
unless possible_event_line?(remainder)
|
|
358
|
-
$stdout.print(remainder)
|
|
470
|
+
$stdout.print(remainder) if passthrough_output
|
|
359
471
|
return +""
|
|
360
472
|
end
|
|
361
473
|
|
|
362
474
|
remainder
|
|
363
475
|
end
|
|
364
476
|
|
|
365
|
-
def flush_interactive_stdout(buffer, stdout_line_handler)
|
|
477
|
+
def flush_interactive_stdout(buffer, stdout_line_handler, passthrough_output: true)
|
|
366
478
|
return if buffer.empty?
|
|
367
479
|
|
|
368
|
-
print_interactive_line(buffer, stdout_line_handler)
|
|
480
|
+
print_interactive_line(buffer, stdout_line_handler, passthrough_output: passthrough_output)
|
|
369
481
|
end
|
|
370
482
|
|
|
371
|
-
def print_interactive_chunk(chunk)
|
|
372
|
-
$stdout.print(chunk)
|
|
483
|
+
def print_interactive_chunk(chunk, passthrough_output: true)
|
|
484
|
+
$stdout.print(chunk) if passthrough_output
|
|
373
485
|
+""
|
|
374
486
|
end
|
|
375
487
|
|
|
376
|
-
def print_interactive_line(line, stdout_line_handler)
|
|
488
|
+
def print_interactive_line(line, stdout_line_handler, passthrough_output: true)
|
|
377
489
|
consumed = possible_event_line?(line) && stdout_line_handler.call(line.chomp)
|
|
378
|
-
$stdout.print(line)
|
|
490
|
+
$stdout.print(line) if !consumed && passthrough_output
|
|
379
491
|
end
|
|
380
492
|
|
|
381
493
|
def possible_event_line?(line)
|
data/lib/kettle/family/config.rb
CHANGED
|
@@ -249,6 +249,10 @@ module Kettle
|
|
|
249
249
|
fetch_path("release", "publish_command") || command_for("release_publish") || "bundle exec kettle-release"
|
|
250
250
|
end
|
|
251
251
|
|
|
252
|
+
def release_required_remotes
|
|
253
|
+
fetch_path("release", "required_remotes")
|
|
254
|
+
end
|
|
255
|
+
|
|
252
256
|
def release_env
|
|
253
257
|
stringify_env(fetch_path("release", "env") || {})
|
|
254
258
|
end
|
data/lib/kettle/family/report.rb
CHANGED
|
@@ -134,6 +134,7 @@ module Kettle
|
|
|
134
134
|
lines << "summary:"
|
|
135
135
|
lines << " outcome: #{data.fetch("outcome")}"
|
|
136
136
|
lines << " elapsed: #{format_elapsed(data.fetch("elapsed_seconds"))}"
|
|
137
|
+
lines << " logs: #{data.fetch("release_log_dirs").join(", ")}" unless data.fetch("release_log_dirs").empty?
|
|
137
138
|
lines << " selected: #{data.fetch("selected_count")}"
|
|
138
139
|
lines << " results: #{data.fetch("result_count")}"
|
|
139
140
|
lines << " succeeded: #{summary_list(data.fetch("succeeded"))}"
|
|
@@ -164,10 +165,20 @@ module Kettle
|
|
|
164
165
|
"skipped" => summary_skipped,
|
|
165
166
|
"failed" => summary_failed,
|
|
166
167
|
"pending" => summary_pending,
|
|
168
|
+
"release_log_dirs" => release_log_dirs,
|
|
167
169
|
"resume_hint" => resume_hint
|
|
168
170
|
}
|
|
169
171
|
end
|
|
170
172
|
|
|
173
|
+
def release_log_dirs
|
|
174
|
+
return [] unless command == "release"
|
|
175
|
+
|
|
176
|
+
visible_results.filter_map do |result|
|
|
177
|
+
path = result.log_path.to_s if result.respond_to?(:log_path)
|
|
178
|
+
File.dirname(path) unless path.to_s.empty?
|
|
179
|
+
end.uniq
|
|
180
|
+
end
|
|
181
|
+
|
|
171
182
|
def append_release_waves(lines)
|
|
172
183
|
wave_results = results.select { |result| release_wave_result?(result) }
|
|
173
184
|
return if wave_results.empty?
|
|
@@ -205,7 +216,10 @@ module Kettle
|
|
|
205
216
|
end
|
|
206
217
|
|
|
207
218
|
def append_result_stdout(lines, result)
|
|
208
|
-
|
|
219
|
+
if suppress_success_output?(result)
|
|
220
|
+
append_result_log_path(lines, result) unless result.ok?
|
|
221
|
+
return
|
|
222
|
+
end
|
|
209
223
|
|
|
210
224
|
if output_streamed?(result)
|
|
211
225
|
append_streamed_output_summary(lines, result) unless result.ok?
|
|
@@ -216,6 +230,11 @@ module Kettle
|
|
|
216
230
|
else
|
|
217
231
|
append_indented_output(lines, result.stdout)
|
|
218
232
|
end
|
|
233
|
+
append_result_log_path(lines, result) unless result.ok? || output_streamed?(result)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def append_result_log_path(lines, result)
|
|
237
|
+
lines << " log: #{result.log_path}" if result.respond_to?(:log_path) && !result.log_path.to_s.empty?
|
|
219
238
|
end
|
|
220
239
|
|
|
221
240
|
def suppress_success_output?(result)
|
|
@@ -239,6 +258,7 @@ module Kettle
|
|
|
239
258
|
lines << " summary: #{summary}" if summary
|
|
240
259
|
recommended_fix = streamed_recommended_fix(result)
|
|
241
260
|
lines << " recommended fix: #{recommended_fix}" if recommended_fix
|
|
261
|
+
append_result_log_path(lines, result)
|
|
242
262
|
lines << " output: omitted because it was already streamed"
|
|
243
263
|
end
|
|
244
264
|
|
|
@@ -19,7 +19,7 @@ module Kettle
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def authorize!
|
|
22
|
-
gem_signing_passphrase
|
|
22
|
+
with_release_secret_alert_suppressed { gem_signing_passphrase }
|
|
23
23
|
rescue => error
|
|
24
24
|
raise Error, error.message
|
|
25
25
|
end
|
|
@@ -31,10 +31,20 @@ module Kettle
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def rubygems_otp
|
|
34
|
-
super
|
|
34
|
+
with_release_secret_alert_suppressed { super }
|
|
35
35
|
rescue => error
|
|
36
36
|
raise Error, error.message
|
|
37
37
|
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def with_release_secret_alert_suppressed
|
|
42
|
+
previous = ENV.fetch("KETTLE_RELEASE_SECRET_ALERT", nil)
|
|
43
|
+
ENV["KETTLE_RELEASE_SECRET_ALERT"] = "false"
|
|
44
|
+
yield
|
|
45
|
+
ensure
|
|
46
|
+
previous.nil? ? ENV.delete("KETTLE_RELEASE_SECRET_ALERT") : ENV["KETTLE_RELEASE_SECRET_ALERT"] = previous
|
|
47
|
+
end
|
|
38
48
|
end
|
|
39
49
|
else
|
|
40
50
|
class Provider
|
|
@@ -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.
|
|
8
|
+
VERSION = "1.2.15"
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
|
@@ -16,6 +16,8 @@ require_relative "workflow_progress"
|
|
|
16
16
|
module Kettle
|
|
17
17
|
module Family
|
|
18
18
|
class Workflow
|
|
19
|
+
PreflightProgressMember = Struct.new(:name)
|
|
20
|
+
|
|
19
21
|
DEFAULT_COMMANDS = {
|
|
20
22
|
"template" => "bundle exec kettle-jem install",
|
|
21
23
|
"test" => "bundle exec kettle-test",
|
|
@@ -100,7 +102,7 @@ module Kettle
|
|
|
100
102
|
REGISTRY_WAIT_ATTEMPTS = 15
|
|
101
103
|
REGISTRY_WAIT_INTERVAL_SECONDS = 15
|
|
102
104
|
|
|
103
|
-
def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, auto_dependency_floors: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, 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, **options)
|
|
105
|
+
def initialize(command:, config:, members:, execute: false, accept: true, commit: true, allow_dirty: false, publish: false, push: false, tag: false, start_step: nil, skip_steps: nil, local_ci: false, continue_ci_failures: false, ci_workflows: nil, skip_bundle_audit: false, skip_remotes: nil, required_remotes: nil, auto_dependency_floors: nil, gha_sha_pins_upgrade: "patch", gha_sha_pins_check: false, 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, **options)
|
|
104
106
|
@command = command
|
|
105
107
|
@config = config
|
|
106
108
|
@members = members
|
|
@@ -117,7 +119,8 @@ module Kettle
|
|
|
117
119
|
@continue_ci_failures = continue_ci_failures
|
|
118
120
|
@ci_workflows = validate_ci_workflows(ci_workflows)
|
|
119
121
|
@skip_bundle_audit = skip_bundle_audit
|
|
120
|
-
@skip_remotes =
|
|
122
|
+
@skip_remotes = validate_remote_list(skip_remotes, "--skip-remotes")
|
|
123
|
+
@required_remotes = validate_remote_list(required_remotes.nil? ? config.release_required_remotes : required_remotes, "--required-remotes")
|
|
121
124
|
@auto_dependency_floors = auto_dependency_floors.nil? ? config.release_auto_dependency_floors? : auto_dependency_floors
|
|
122
125
|
@gha_sha_pins_upgrade = gha_sha_pins_upgrade
|
|
123
126
|
@gha_sha_pins_check = gha_sha_pins_check
|
|
@@ -159,7 +162,7 @@ module Kettle
|
|
|
159
162
|
|
|
160
163
|
private
|
|
161
164
|
|
|
162
|
-
attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :skip_steps, :local_ci, :continue_ci_failures, :ci_workflows, :skip_bundle_audit, :skip_remotes, :auto_dependency_floors, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides, :debug, :verbose, :jobs, :progress_io, :reset_target, :bup_args, :bex_args, :start_member, :start_branch
|
|
165
|
+
attr_reader :command, :config, :members, :execute, :accept, :commit, :allow_dirty, :publish, :push, :tag, :start_step, :skip_steps, :local_ci, :continue_ci_failures, :ci_workflows, :skip_bundle_audit, :skip_remotes, :required_remotes, :auto_dependency_floors, :gha_sha_pins_upgrade, :gha_sha_pins_check, :env_overrides, :debug, :verbose, :jobs, :progress_io, :reset_target, :bup_args, :bex_args, :start_member, :start_branch
|
|
163
166
|
|
|
164
167
|
def current_branch_results(workflow_members)
|
|
165
168
|
return check_results(workflow_members) if command == "check"
|
|
@@ -421,14 +424,23 @@ module Kettle
|
|
|
421
424
|
|
|
422
425
|
def release_preflight_results
|
|
423
426
|
phases = release_preflight_phases
|
|
424
|
-
|
|
427
|
+
progress = start_release_preflight_progress(phases)
|
|
428
|
+
progress&.start
|
|
429
|
+
preflight_member = release_preflight_progress_member
|
|
430
|
+
progress&.start_member(preflight_member, total: phases.length, status: phases.first.fetch(:label))
|
|
425
431
|
phases.each_with_index do |phase, index|
|
|
426
|
-
|
|
432
|
+
label = phase.fetch(:label)
|
|
433
|
+
progress&.update(preflight_member, status: label, mark: ">")
|
|
427
434
|
result = send(phase.fetch(:method))
|
|
428
|
-
|
|
435
|
+
progress&.advance(preflight_member, status: label, success: result.empty?, mark: result.empty? ? "." : "F")
|
|
436
|
+
unless result.empty?
|
|
437
|
+
progress&.finish_member(preflight_member, success: false, status: label)
|
|
438
|
+
progress&.stop
|
|
439
|
+
end
|
|
429
440
|
return result unless result.empty?
|
|
430
441
|
end
|
|
431
|
-
|
|
442
|
+
progress&.finish_member(preflight_member, success: true, status: "ok")
|
|
443
|
+
progress&.stop
|
|
432
444
|
[]
|
|
433
445
|
end
|
|
434
446
|
|
|
@@ -745,7 +757,9 @@ module Kettle
|
|
|
745
757
|
command: release_command,
|
|
746
758
|
env: release_env,
|
|
747
759
|
interactive: release_command_interactive?,
|
|
748
|
-
stdout_line_handler: release_event_line_handler(member, progress: progress)
|
|
760
|
+
stdout_line_handler: release_event_line_handler(member, progress: progress),
|
|
761
|
+
log_path: release_command_log_path(member, release_phase),
|
|
762
|
+
passthrough_output: release_command_passthrough_output?
|
|
749
763
|
)
|
|
750
764
|
emit_member_result_progress(member, memo.last, progress: progress)
|
|
751
765
|
return memo unless memo.last.ok?
|
|
@@ -792,7 +806,29 @@ module Kettle
|
|
|
792
806
|
def release_otp_coordinator
|
|
793
807
|
return nil unless execute && release_command_interactive?
|
|
794
808
|
|
|
795
|
-
@release_otp_coordinator ||= CommandRunner::OtpCoordinator.new(
|
|
809
|
+
@release_otp_coordinator ||= CommandRunner::OtpCoordinator.new(
|
|
810
|
+
secrets_provider: @secrets_provider,
|
|
811
|
+
event_handler: method(:handle_release_otp_event)
|
|
812
|
+
)
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def handle_release_otp_event(member_name, event)
|
|
816
|
+
progress = @release_progress
|
|
817
|
+
return unless progress
|
|
818
|
+
|
|
819
|
+
member = release_progress_member_for(member_name)
|
|
820
|
+
if progress_io
|
|
821
|
+
if verbose || debug
|
|
822
|
+
emit_release_event_progress(member, event)
|
|
823
|
+
elsif progress.tty?
|
|
824
|
+
emit_release_event_status(member, event, progress: progress)
|
|
825
|
+
end
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def release_progress_member_for(member_name)
|
|
830
|
+
@release_progress_members_by_name ||= members.to_h { |member| [member.name, member] }
|
|
831
|
+
@release_progress_members_by_name.fetch(member_name.to_s) { PreflightProgressMember.new(member_name.to_s) }
|
|
796
832
|
end
|
|
797
833
|
|
|
798
834
|
def parallel_release_members?(release_members)
|
|
@@ -1378,6 +1414,10 @@ module Kettle
|
|
|
1378
1414
|
publish || !!@gem_signing_password
|
|
1379
1415
|
end
|
|
1380
1416
|
|
|
1417
|
+
def release_command_passthrough_output?
|
|
1418
|
+
verbose || debug
|
|
1419
|
+
end
|
|
1420
|
+
|
|
1381
1421
|
def kettle_release_command?(command)
|
|
1382
1422
|
case command
|
|
1383
1423
|
when Array
|
|
@@ -1408,6 +1448,7 @@ module Kettle
|
|
|
1408
1448
|
args << "--local-ci" if local_ci
|
|
1409
1449
|
args << "--skip-bundle-audit" if skip_bundle_audit
|
|
1410
1450
|
args << "--skip-remotes=#{skip_remotes}" if skip_remotes && !skip_remotes.to_s.empty?
|
|
1451
|
+
args << "--required-remotes=#{required_remotes}" if required_remotes && !required_remotes.to_s.empty?
|
|
1411
1452
|
args << "--yes" if release_command_uses_kettle_release_yes? && !command_includes_arg?(command, "--yes")
|
|
1412
1453
|
args << "--events" unless command_includes_arg?(command, "--events")
|
|
1413
1454
|
return command if args.empty?
|
|
@@ -1436,12 +1477,14 @@ module Kettle
|
|
|
1436
1477
|
workflows.join(",")
|
|
1437
1478
|
end
|
|
1438
1479
|
|
|
1439
|
-
def
|
|
1440
|
-
return nil if value.nil?
|
|
1480
|
+
def validate_remote_list(value, option_name)
|
|
1481
|
+
return nil if value.nil?
|
|
1482
|
+
|
|
1483
|
+
remotes = Array(value).flat_map { |entry| entry.to_s.split(",") }.map(&:strip)
|
|
1484
|
+
return nil if remotes.all?(&:empty?)
|
|
1441
1485
|
|
|
1442
|
-
remotes = value.to_s.split(",").map(&:strip)
|
|
1443
1486
|
invalid = remotes.find { |remote| remote.empty? || !remote.match?(/\A[A-Za-z0-9_.-]+\z/) }
|
|
1444
|
-
raise Error, "invalid
|
|
1487
|
+
raise Error, "invalid #{option_name} value #{value.inspect}" if invalid
|
|
1445
1488
|
|
|
1446
1489
|
remotes.join(",")
|
|
1447
1490
|
end
|
|
@@ -1460,6 +1503,7 @@ module Kettle
|
|
|
1460
1503
|
env["K_RELEASE_CI_WORKFLOWS"] = ci_workflows if ci_workflows && !ci_workflows.to_s.empty?
|
|
1461
1504
|
env["KETTLE_DEV_SKIP_BUNDLE_AUDIT"] = "true" if skip_bundle_audit
|
|
1462
1505
|
env["K_RELEASE_SKIP_REMOTES"] = skip_remotes if skip_remotes && !skip_remotes.to_s.empty?
|
|
1506
|
+
env["K_RELEASE_REQUIRED_REMOTES"] = required_remotes if required_remotes && !required_remotes.to_s.empty?
|
|
1463
1507
|
env.merge!(kettle_release_secrets_env)
|
|
1464
1508
|
env
|
|
1465
1509
|
end
|
|
@@ -1903,6 +1947,24 @@ module Kettle
|
|
|
1903
1947
|
progress
|
|
1904
1948
|
end
|
|
1905
1949
|
|
|
1950
|
+
def start_release_preflight_progress(phases)
|
|
1951
|
+
return nil unless progress_io
|
|
1952
|
+
|
|
1953
|
+
phase_label = (phases.length == 1) ? "phase" : "phases"
|
|
1954
|
+
WorkflowProgress.new(
|
|
1955
|
+
io: progress_io,
|
|
1956
|
+
label: "release preflight",
|
|
1957
|
+
total: phases.length,
|
|
1958
|
+
jobs: 1,
|
|
1959
|
+
members: [release_preflight_progress_member],
|
|
1960
|
+
heading: "release preflight #{phases.length} #{phase_label}:"
|
|
1961
|
+
)
|
|
1962
|
+
end
|
|
1963
|
+
|
|
1964
|
+
def release_preflight_progress_member
|
|
1965
|
+
@release_preflight_progress_member ||= PreflightProgressMember.new("preflight")
|
|
1966
|
+
end
|
|
1967
|
+
|
|
1906
1968
|
def template_phase_total(member = nil)
|
|
1907
1969
|
total = 1
|
|
1908
1970
|
total += 2 if config.normalize_lockfiles?
|
|
@@ -1927,6 +1989,20 @@ module Kettle
|
|
|
1927
1989
|
end
|
|
1928
1990
|
end
|
|
1929
1991
|
|
|
1992
|
+
def release_command_log_path(member, phase)
|
|
1993
|
+
return nil unless execute
|
|
1994
|
+
|
|
1995
|
+
File.join(release_log_dir, "#{safe_log_name(member.name)}-#{safe_log_name(phase)}.log")
|
|
1996
|
+
end
|
|
1997
|
+
|
|
1998
|
+
def release_log_dir
|
|
1999
|
+
@release_log_dir ||= File.join(config.root, "tmp", "kettle-family", "release-#{Time.now.strftime("%Y%m%d-%H%M%S")}-#{$$}")
|
|
2000
|
+
end
|
|
2001
|
+
|
|
2002
|
+
def safe_log_name(value)
|
|
2003
|
+
value.to_s.gsub(/[^A-Za-z0-9_.-]+/, "_")
|
|
2004
|
+
end
|
|
2005
|
+
|
|
1930
2006
|
def template_initial_status(member = nil)
|
|
1931
2007
|
return "prepare_lockfiles" if config.normalize_lockfiles?
|
|
1932
2008
|
return "prepare_template_dependencies" if member.nil? || template_prepares_dependencies?(member)
|
|
@@ -1977,36 +2053,6 @@ module Kettle
|
|
|
1977
2053
|
progress.summary("release summary: #{release_results.count(&:ok?)}/#{release_results.length} members ok")
|
|
1978
2054
|
end
|
|
1979
2055
|
|
|
1980
|
-
def emit_release_preflight_start(phases)
|
|
1981
|
-
return unless progress_io
|
|
1982
|
-
|
|
1983
|
-
phase_label = (phases.length == 1) ? "phase" : "phases"
|
|
1984
|
-
progress_io.puts("release preflight #{phases.length} #{phase_label}:")
|
|
1985
|
-
progress_io.flush
|
|
1986
|
-
end
|
|
1987
|
-
|
|
1988
|
-
def emit_release_preflight_phase(label, index:, total:)
|
|
1989
|
-
return unless progress_io
|
|
1990
|
-
|
|
1991
|
-
progress_io.puts("[release preflight] (#{index + 1}/#{total}) > #{label}")
|
|
1992
|
-
progress_io.flush
|
|
1993
|
-
end
|
|
1994
|
-
|
|
1995
|
-
def emit_release_preflight_phase_finish(label, results)
|
|
1996
|
-
return unless progress_io
|
|
1997
|
-
|
|
1998
|
-
mark = results.empty? ? "." : "F"
|
|
1999
|
-
progress_io.puts("[release preflight] #{mark} #{label}")
|
|
2000
|
-
progress_io.flush
|
|
2001
|
-
end
|
|
2002
|
-
|
|
2003
|
-
def emit_release_preflight_summary(phases)
|
|
2004
|
-
return unless progress_io
|
|
2005
|
-
|
|
2006
|
-
progress_io.puts("release preflight summary: #{phases.length}/#{phases.length} phases ok")
|
|
2007
|
-
progress_io.flush
|
|
2008
|
-
end
|
|
2009
|
-
|
|
2010
2056
|
def template_event_line_handler(member, progress: nil)
|
|
2011
2057
|
lambda do |line|
|
|
2012
2058
|
event = parse_template_event(line)
|
|
@@ -2087,10 +2133,21 @@ module Kettle
|
|
|
2087
2133
|
when "run_start"
|
|
2088
2134
|
emit_template_event_line(member, ">", "release")
|
|
2089
2135
|
when "command_step"
|
|
2090
|
-
|
|
2091
|
-
emit_template_event_line(member, template_event_mark(event), label)
|
|
2136
|
+
emit_template_event_line(member, template_event_mark(event), command_step_event_label(event))
|
|
2092
2137
|
when "secret_provider"
|
|
2093
2138
|
emit_template_event_line(member, release_event_status_mark(event), secret_provider_event_label(event))
|
|
2139
|
+
when "remote_parity"
|
|
2140
|
+
emit_template_event_line(member, release_event_status_mark(event), remote_parity_event_label(event))
|
|
2141
|
+
when "ci_monitor"
|
|
2142
|
+
emit_template_event_line(member, release_event_status_mark(event), ci_monitor_event_label(event))
|
|
2143
|
+
when "pre_release"
|
|
2144
|
+
emit_template_event_line(member, release_event_status_mark(event), pre_release_event_label(event))
|
|
2145
|
+
when "changelog"
|
|
2146
|
+
emit_template_event_line(member, release_event_status_mark(event), changelog_event_label(event))
|
|
2147
|
+
when "release_lockfile"
|
|
2148
|
+
emit_template_event_line(member, release_event_status_mark(event), release_lockfile_event_label(event))
|
|
2149
|
+
when "release_probe"
|
|
2150
|
+
emit_template_event_line(member, release_event_status_mark(event), release_probe_event_label(event))
|
|
2094
2151
|
when "diagnostic"
|
|
2095
2152
|
emit_template_event_line(member, "!", diagnostic_event_label(event))
|
|
2096
2153
|
when "summary"
|
|
@@ -2103,9 +2160,21 @@ module Kettle
|
|
|
2103
2160
|
when "run_start"
|
|
2104
2161
|
"release"
|
|
2105
2162
|
when "command_step"
|
|
2106
|
-
|
|
2163
|
+
command_step_event_label(event)
|
|
2107
2164
|
when "secret_provider"
|
|
2108
2165
|
secret_provider_event_label(event)
|
|
2166
|
+
when "remote_parity"
|
|
2167
|
+
remote_parity_event_label(event)
|
|
2168
|
+
when "ci_monitor"
|
|
2169
|
+
ci_monitor_event_label(event)
|
|
2170
|
+
when "pre_release"
|
|
2171
|
+
pre_release_event_label(event)
|
|
2172
|
+
when "changelog"
|
|
2173
|
+
changelog_event_label(event)
|
|
2174
|
+
when "release_lockfile"
|
|
2175
|
+
release_lockfile_event_label(event)
|
|
2176
|
+
when "release_probe"
|
|
2177
|
+
release_probe_event_label(event)
|
|
2109
2178
|
when "diagnostic"
|
|
2110
2179
|
diagnostic_event_label(event)
|
|
2111
2180
|
when "summary"
|
|
@@ -2123,6 +2192,18 @@ module Kettle
|
|
|
2123
2192
|
template_event_mark(event)
|
|
2124
2193
|
when "secret_provider"
|
|
2125
2194
|
template_event_mark(event)
|
|
2195
|
+
when "remote_parity"
|
|
2196
|
+
template_event_mark(event)
|
|
2197
|
+
when "ci_monitor"
|
|
2198
|
+
template_event_mark(event)
|
|
2199
|
+
when "pre_release"
|
|
2200
|
+
template_event_mark(event)
|
|
2201
|
+
when "changelog"
|
|
2202
|
+
template_event_mark(event)
|
|
2203
|
+
when "release_lockfile"
|
|
2204
|
+
template_event_mark(event)
|
|
2205
|
+
when "release_probe"
|
|
2206
|
+
template_event_mark(event)
|
|
2126
2207
|
when "diagnostic"
|
|
2127
2208
|
"!"
|
|
2128
2209
|
when "summary"
|
|
@@ -2155,9 +2236,85 @@ module Kettle
|
|
|
2155
2236
|
parts << purpose unless purpose.empty?
|
|
2156
2237
|
parts << label unless label.empty?
|
|
2157
2238
|
parts << source if purpose.empty? && label.empty? && !source.empty?
|
|
2239
|
+
parts << "#{event["queued"]}/#{event["total"]}" if event["queued"] && event["total"]
|
|
2240
|
+
parts.join(":")
|
|
2241
|
+
end
|
|
2242
|
+
|
|
2243
|
+
def command_step_event_label(event)
|
|
2244
|
+
parts = [event["phase"], event["name"]].map(&:to_s).reject(&:empty?)
|
|
2245
|
+
summary = event["summary"].to_s
|
|
2246
|
+
parts << summary unless summary.empty?
|
|
2158
2247
|
parts.join(":")
|
|
2159
2248
|
end
|
|
2160
2249
|
|
|
2250
|
+
def remote_parity_event_label(event)
|
|
2251
|
+
action = event["action"].to_s
|
|
2252
|
+
remote = event["remote"].to_s
|
|
2253
|
+
trunk = event["trunk"].to_s
|
|
2254
|
+
parts = ["remote"]
|
|
2255
|
+
parts << action unless action.empty?
|
|
2256
|
+
parts << remote unless remote.empty?
|
|
2257
|
+
parts << trunk if remote.empty? && !trunk.empty?
|
|
2258
|
+
parts.join(":")
|
|
2259
|
+
end
|
|
2260
|
+
|
|
2261
|
+
def ci_monitor_event_label(event)
|
|
2262
|
+
action = event["action"].to_s
|
|
2263
|
+
provider = event["provider"].to_s
|
|
2264
|
+
target = event["workflow"].to_s
|
|
2265
|
+
target = "pipeline" if target.empty? && provider == "gitlab"
|
|
2266
|
+
parts = ["ci"]
|
|
2267
|
+
parts << action unless action.empty?
|
|
2268
|
+
parts << provider unless provider.empty?
|
|
2269
|
+
parts << target unless target.empty?
|
|
2270
|
+
if %w[github_wait github_started github_tick].include?(action) && event["completed"] && event["total"]
|
|
2271
|
+
parts << "#{event["completed"]}/#{event["total"]}"
|
|
2272
|
+
end
|
|
2273
|
+
completed_workflow = event["completed_workflow"].to_s
|
|
2274
|
+
parts << completed_workflow unless completed_workflow.empty? || completed_workflow == target
|
|
2275
|
+
parts.join(":")
|
|
2276
|
+
end
|
|
2277
|
+
|
|
2278
|
+
def pre_release_event_label(event)
|
|
2279
|
+
action = event["action"].to_s
|
|
2280
|
+
check = event["check"].to_s
|
|
2281
|
+
parts = ["pre"]
|
|
2282
|
+
parts << action unless action.empty?
|
|
2283
|
+
parts << check unless check.empty?
|
|
2284
|
+
parts.join(":")
|
|
2285
|
+
end
|
|
2286
|
+
|
|
2287
|
+
def changelog_event_label(event)
|
|
2288
|
+
action = event["action"].to_s
|
|
2289
|
+
plan = event["plan"].to_s
|
|
2290
|
+
parts = ["changelog"]
|
|
2291
|
+
parts << action unless action.empty?
|
|
2292
|
+
parts << plan unless plan.empty? || action == "coverage"
|
|
2293
|
+
parts.join(":")
|
|
2294
|
+
end
|
|
2295
|
+
|
|
2296
|
+
def release_lockfile_event_label(event)
|
|
2297
|
+
action = event["action"].to_s
|
|
2298
|
+
stage = event["stage"].to_s
|
|
2299
|
+
stage = stage.tr(" ", "_")
|
|
2300
|
+
parts = ["lockfile"]
|
|
2301
|
+
parts << action unless action.empty?
|
|
2302
|
+
parts << stage unless stage.empty?
|
|
2303
|
+
parts << "#{event["attempt"]}/#{event["attempts"]}" if event["attempt"] && event["attempts"]
|
|
2304
|
+
parts.join(":")
|
|
2305
|
+
end
|
|
2306
|
+
|
|
2307
|
+
def release_probe_event_label(event)
|
|
2308
|
+
action = event["action"].to_s
|
|
2309
|
+
gem_name = event["gem"].to_s
|
|
2310
|
+
version = event["version"].to_s
|
|
2311
|
+
parts = ["probe"]
|
|
2312
|
+
parts << action unless action.empty?
|
|
2313
|
+
parts << [gem_name, version].reject(&:empty?).join("-")
|
|
2314
|
+
parts << "#{event["attempt"]}/#{event["attempts"]}" if event["attempt"] && event["attempts"]
|
|
2315
|
+
parts.reject(&:empty?).join(":")
|
|
2316
|
+
end
|
|
2317
|
+
|
|
2161
2318
|
def diagnostic_event_label(event)
|
|
2162
2319
|
message = event["message"]
|
|
2163
2320
|
return message.to_s unless message.to_s.empty? || message.is_a?(Hash)
|
|
@@ -15,11 +15,12 @@ module Kettle
|
|
|
15
15
|
DEFAULT_TERMINAL_WIDTH = 80
|
|
16
16
|
ELLIPSIS = "..."
|
|
17
17
|
|
|
18
|
-
def initialize(io:, label:, total:, jobs:, members: [], enabled: true, clock: nil)
|
|
18
|
+
def initialize(io:, label:, total:, jobs:, members: [], enabled: true, clock: nil, heading: nil)
|
|
19
19
|
@io = io
|
|
20
20
|
@label = label
|
|
21
21
|
@total = total.to_i
|
|
22
22
|
@jobs = jobs.to_i
|
|
23
|
+
@heading = heading
|
|
23
24
|
@clock = clock || lambda { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
24
25
|
@enabled = enabled && !!io
|
|
25
26
|
@bars = {}
|
|
@@ -43,7 +44,7 @@ module Kettle
|
|
|
43
44
|
synchronize do
|
|
44
45
|
next if @started
|
|
45
46
|
|
|
46
|
-
write_line("#{@label} #{@total} member#{plural(@total)} with #{@jobs} job#{plural(@jobs)}:")
|
|
47
|
+
write_line(@heading || "#{@label} #{@total} member#{plural(@total)} with #{@jobs} job#{plural(@jobs)}:")
|
|
47
48
|
@started = true
|
|
48
49
|
@line_order.each { |member_name| render_name(member_name, status: "") } if @tty
|
|
49
50
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kettle-family
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.15
|
|
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.
|
|
77
|
+
version: 2.5.13
|
|
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.
|
|
87
|
+
version: 2.5.13
|
|
88
88
|
- !ruby/object:Gem::Dependency
|
|
89
89
|
name: kettle-test
|
|
90
90
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -342,10 +342,10 @@ licenses:
|
|
|
342
342
|
- AGPL-3.0-only
|
|
343
343
|
metadata:
|
|
344
344
|
homepage_uri: https://kettle-family.galtzo.com
|
|
345
|
-
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.
|
|
346
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.
|
|
345
|
+
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.15
|
|
346
|
+
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.15/CHANGELOG.md
|
|
347
347
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
|
|
348
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.
|
|
348
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.15
|
|
349
349
|
funding_uri: https://github.com/sponsors/pboling
|
|
350
350
|
wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
|
|
351
351
|
news_uri: https://www.railsbling.com/tags/kettle-family
|
metadata.gz.sig
CHANGED
|
Binary file
|