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