kettle-family 1.3.0 → 1.3.2
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 +93 -1
- data/README.md +1 -1
- data/lib/kettle/family/branch_target_config.rb +8 -5
- data/lib/kettle/family/cli.rb +2 -2
- data/lib/kettle/family/command_runner.rb +105 -5
- data/lib/kettle/family/config.rb +1 -3
- data/lib/kettle/family/discovery.rb +43 -14
- data/lib/kettle/family/local_install.rb +3 -2
- data/lib/kettle/family/nomono_bootstrap.rb +3 -1
- data/lib/kettle/family/orderer.rb +4 -1
- data/lib/kettle/family/paths.rb +57 -0
- data/lib/kettle/family/readiness_check.rb +8 -7
- data/lib/kettle/family/release_graph.rb +4 -6
- data/lib/kettle/family/release_state_check.rb +40 -12
- data/lib/kettle/family/report.rb +23 -1
- data/lib/kettle/family/secrets_broker.rb +12 -1
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/version_bump.rb +13 -0
- data/lib/kettle/family/workflow.rb +237 -71
- data.tar.gz.sig +0 -0
- metadata +40 -25
- 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: 7b4a15c15f854710fb75ab2c866cf5ebb2b6dd24f1418a1e694446139a347421
|
|
4
|
+
data.tar.gz: 70bd34977e27daf2f6e5f8ef465b02c938438eb1346d3574fd7080f2f54d3661
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 271e6a288806993a5cf584f9ba8293854c9b7ecb32dbec18b8b6cf293856d8ca7f92bb6d320f8fd97de6ba82622573dee01112974416c95ee2d87e11312dc463
|
|
7
|
+
data.tar.gz: 3fd884813e4bb730575704ceda96b874cacac2b83a80d597f86e1212ba266cb575526808abe1eb5061404cb5446c64946fa0575d10fae1993dd96868c3479639
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,94 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.3.2] - 2026-09-28
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.3.2][1.3.2t]
|
|
36
|
+
- COVERAGE: 93.25% -- 6537/7010 lines in 37 files
|
|
37
|
+
- BRANCH COVERAGE: 77.77% -- 2659/3419 branches in 37 files
|
|
38
|
+
- 33.57% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- kettle-jem-template-20260913-001 - Templating now also surfaces a review
|
|
43
|
+
entry in `dependency_conflicts.resolve` when a direct development
|
|
44
|
+
dependency doesn't support one or more of this project's declared
|
|
45
|
+
`engines:` and has no template-managed modular home (e.g. `sqlite3` on
|
|
46
|
+
`jruby`). Review each entry and pick a resolution per the project's own
|
|
47
|
+
engine support needs.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- [kc] kettle-jem/prepare: updated 13 project files:
|
|
52
|
+
- dependencies (13)
|
|
53
|
+
|
|
54
|
+
- [kc] kettle-jem/template: updated 14 project files:
|
|
55
|
+
- code and tests (2)
|
|
56
|
+
- dependencies (2)
|
|
57
|
+
- other (1)
|
|
58
|
+
- workflows (9)
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- Run local Kettle Jem through a prepared member bundle so local StructuredMerge dependencies are activated during templating.
|
|
63
|
+
|
|
64
|
+
- Pin bupb to an installed stable Bundler version, including its managed child updates.
|
|
65
|
+
|
|
66
|
+
- [kc] nomono-self-upgrade-bundler-env: Preserve RUBYOPT bundle activation for local Kettle Jem execution, but omit eager setup from nomono self-upgrade lockfile updates so the stale self-version can be reconciled without falling back from the local template stack.
|
|
67
|
+
|
|
68
|
+
- Recover template lockfiles with Bundler's valid bundle lock --update GEM syntax instead of passing gem names as invalid positional arguments.
|
|
69
|
+
|
|
70
|
+
- Template preparation now starts local Kettle Jem from its own bundle when the destination lockfile path-locks Nomono, allowing bootstrap repair before the destination bundle loads.
|
|
71
|
+
|
|
72
|
+
- Refresh dependent lockfiles before committing newly raised family dependency floors.
|
|
73
|
+
|
|
74
|
+
- Handle macOS canonical temp paths, Unix socket limits, and PTY EOF during family workflows.
|
|
75
|
+
|
|
76
|
+
- Canonicalize Windows system temporary paths for Git-based specs, launch the Mise integration fixture on Windows, and include WEBrick for local registry specs.
|
|
77
|
+
|
|
78
|
+
- Readiness checks accept existing binstub files on Windows, where Unix executable-bit checks are not portable.
|
|
79
|
+
|
|
80
|
+
- Make family specs portable on Windows and use valid generated gemspec fixtures.
|
|
81
|
+
|
|
82
|
+
- Support Windows pipe-driven interactive commands, absolute local-path lockfile remotes, and equivalent filesystem path spellings in family workflows.
|
|
83
|
+
|
|
84
|
+
- Apply workflow environment overrides portably after mise and normalize Windows filesystem paths in discovery and branch configuration.
|
|
85
|
+
|
|
86
|
+
- Normalize filesystem glob separators across supported platforms so family discovery, installs, and workflow asset lookup find paths on Windows.
|
|
87
|
+
|
|
88
|
+
- Normalize Windows path aliases and subprocess line endings in family workflows.
|
|
89
|
+
|
|
90
|
+
- Close the child input stream when a configured OTP provider returns no code, preventing interactive release commands from hanging.
|
|
91
|
+
|
|
92
|
+
- Start the Windows environment wrapper without inherited RUBYOPT so Bundler cannot activate the parent bundle before wrapper overrides are applied.
|
|
93
|
+
|
|
94
|
+
- Windows family workflows now reset lockfiles without relying on the POSIX env utility, terminate failed interactive child processes, and apply member exclusion patterns case-insensitively.
|
|
95
|
+
|
|
96
|
+
- Windows family workflow specs now preserve command arguments and handle case-insensitive checkout paths.
|
|
97
|
+
|
|
98
|
+
- Detect Windows drive-absolute local paths when preparing release lockfile normalization.
|
|
99
|
+
|
|
100
|
+
- Delegate canonical, identity, and containment path checks to kettle-dev so family workflows consistently handle symlink aliases and platform path semantics.
|
|
101
|
+
|
|
102
|
+
- Windows family discovery now applies exclusion globs across path separators, and branch-target config lookup uses shared path normalization.
|
|
103
|
+
|
|
104
|
+
- Resolve family-relative paths by filesystem identity so discovery exclusions and branch-stack configs work with Windows path aliases.
|
|
105
|
+
|
|
106
|
+
## [1.3.1] - 2026-09-10
|
|
107
|
+
|
|
108
|
+
- TAG: [v1.3.1][1.3.1t]
|
|
109
|
+
- COVERAGE: 93.48% -- 6352/6795 lines in 36 files
|
|
110
|
+
- BRANCH COVERAGE: 77.98% -- 2582/3311 branches in 36 files
|
|
111
|
+
- 33.49% documented
|
|
112
|
+
|
|
113
|
+
### Fixed
|
|
114
|
+
|
|
115
|
+
- Honor configured ordering in release-state reports, emit all five phases for shared-changelog members, and distinguish missing exact GitHub releases from lookup errors.
|
|
116
|
+
|
|
117
|
+
- Publish shared monorepo GitHub releases incrementally after each finalized member, retain successful workers from interrupted waves, attach both checksum formats, and report redundant family state cells once.
|
|
118
|
+
|
|
119
|
+
- Keep independent monorepo GitHub release identities exact and avoid false unlisted-member warnings during filtered state inspection.
|
|
120
|
+
|
|
33
121
|
## [1.3.0] - 2026-09-10
|
|
34
122
|
|
|
35
123
|
- TAG: [v1.3.0][1.3.0t]
|
|
@@ -2731,7 +2819,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
2731
2819
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
2732
2820
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
2733
2821
|
|
|
2734
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.
|
|
2822
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.2...HEAD
|
|
2823
|
+
[1.3.2]: https://github.com/kettle-dev/kettle-family/compare/v1.3.1...v1.3.2
|
|
2824
|
+
[1.3.2t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.2
|
|
2825
|
+
[1.3.1]: https://github.com/kettle-dev/kettle-family/compare/v1.3.0...v1.3.1
|
|
2826
|
+
[1.3.1t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.1
|
|
2735
2827
|
[1.3.0]: https://github.com/kettle-dev/kettle-family/compare/v1.2.100...v1.3.0
|
|
2736
2828
|
[1.3.0t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.0
|
|
2737
2829
|
[1.2.100]: https://github.com/kettle-dev/kettle-family/compare/v1.2.99...v1.2.100
|
data/README.md
CHANGED
|
@@ -1186,7 +1186,7 @@ Thanks for RTFM. ☺️
|
|
|
1186
1186
|
[📌gitmoji]: https://gitmoji.dev
|
|
1187
1187
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1188
1188
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1189
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
|
1189
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-7.010-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1190
1190
|
[🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
|
|
1191
1191
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1192
1192
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "yaml"
|
|
5
|
+
require_relative "paths"
|
|
5
6
|
|
|
6
7
|
module Kettle
|
|
7
8
|
module Family
|
|
@@ -75,20 +76,22 @@ module Kettle
|
|
|
75
76
|
raise Error, "could not determine git root for #{member.root}: #{stderr}" unless status.success?
|
|
76
77
|
|
|
77
78
|
File.realpath(stdout.strip)
|
|
79
|
+
rescue SystemCallError
|
|
80
|
+
raise Error, "could not determine git root for #{member.root}: invalid Git root response"
|
|
78
81
|
end
|
|
79
82
|
|
|
80
83
|
def member_relative_root(member, root)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return member_root.delete_prefix("#{root}/") if member_root.start_with?("#{root}/")
|
|
84
|
+
relative = Paths.relative(member.root, root)
|
|
85
|
+
raise Error, "member root #{member.root} is outside git root #{root}" unless relative
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
relative
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
def member_local_config_paths(root, relative_root)
|
|
89
91
|
branches = local_branches(root)
|
|
90
92
|
candidates = Config::DEFAULT_PATHS.map do |path|
|
|
91
|
-
(relative_root == ".") ? path : File.join(relative_root, path)
|
|
93
|
+
candidate = (relative_root == ".") ? path : File.join(relative_root, path)
|
|
94
|
+
candidate.tr("\\", "/")
|
|
92
95
|
end
|
|
93
96
|
branches.each_with_object([]) do |branch, memo|
|
|
94
97
|
candidates.each do |path|
|
data/lib/kettle/family/cli.rb
CHANGED
|
@@ -684,10 +684,10 @@ module Kettle
|
|
|
684
684
|
members = discovery.members
|
|
685
685
|
ordered = if command == "install"
|
|
686
686
|
install_order(members, config)
|
|
687
|
-
elsif
|
|
687
|
+
elsif command == "metadata"
|
|
688
688
|
members.sort_by(&:name)
|
|
689
689
|
else
|
|
690
|
-
Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered
|
|
690
|
+
Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered(allow_cycles: command == "release-state")
|
|
691
691
|
end
|
|
692
692
|
state_event_tape = release_state_event_tape(command: command, config: config, options: options)
|
|
693
693
|
Selection.validate_release_state_only_filter!(effective_only)
|
|
@@ -11,6 +11,17 @@ module Kettle
|
|
|
11
11
|
"KETTLE_RELEASE_GEM_SIGNING_PASSPHRASE"
|
|
12
12
|
].freeze
|
|
13
13
|
MISE_SESSION_ENV_PREFIX = "__MISE_"
|
|
14
|
+
WINDOWS_ENV_EXEC_SCRIPT = <<~RUBY
|
|
15
|
+
unset_count = Integer(ARGV.shift)
|
|
16
|
+
unset_count.times { ENV.delete(ARGV.shift) }
|
|
17
|
+
assignment_count = Integer(ARGV.shift)
|
|
18
|
+
assignment_count.times do
|
|
19
|
+
key, value = ARGV.shift.split("=", 2)
|
|
20
|
+
ENV[key] = value
|
|
21
|
+
end
|
|
22
|
+
abort "missing command after environment arguments" if ARGV.shift != "--"
|
|
23
|
+
exec(*ARGV)
|
|
24
|
+
RUBY
|
|
14
25
|
|
|
15
26
|
class OtpCoordinator
|
|
16
27
|
def initialize(input: $stdin, output: $stdout, queue_total: nil, secrets_provider: nil, event_handler: nil)
|
|
@@ -371,7 +382,7 @@ module Kettle
|
|
|
371
382
|
end
|
|
372
383
|
end
|
|
373
384
|
end
|
|
374
|
-
rescue Errno::EIO
|
|
385
|
+
rescue Errno::EIO, EOFError
|
|
375
386
|
# PTY raises EIO when the child process exits after closing the slave.
|
|
376
387
|
rescue Error => error
|
|
377
388
|
stderr << "#{error.message}\n"
|
|
@@ -386,6 +397,19 @@ module Kettle
|
|
|
386
397
|
end
|
|
387
398
|
|
|
388
399
|
def run_interactive_open3(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
|
|
400
|
+
if Gem.win_platform?
|
|
401
|
+
return run_interactive_open3_threads(
|
|
402
|
+
env: env,
|
|
403
|
+
argv: argv,
|
|
404
|
+
chdir: chdir,
|
|
405
|
+
member_name: member_name,
|
|
406
|
+
process_options: process_options,
|
|
407
|
+
stdout_line_handler: stdout_line_handler,
|
|
408
|
+
log_io: log_io,
|
|
409
|
+
passthrough_output: passthrough_output
|
|
410
|
+
)
|
|
411
|
+
end
|
|
412
|
+
|
|
389
413
|
captured_stdout = +""
|
|
390
414
|
captured_stderr = +""
|
|
391
415
|
stdout_line_buffer = +""
|
|
@@ -431,6 +455,62 @@ module Kettle
|
|
|
431
455
|
[captured_stdout, captured_stderr, status]
|
|
432
456
|
end
|
|
433
457
|
|
|
458
|
+
def run_interactive_open3_threads(env:, argv:, chdir:, member_name:, process_options:, stdout_line_handler:, log_io: nil, passthrough_output: true)
|
|
459
|
+
captured_stdout = +""
|
|
460
|
+
captured_stderr = +""
|
|
461
|
+
stdout_line_buffer = +""
|
|
462
|
+
prompt_buffer = +""
|
|
463
|
+
status = nil
|
|
464
|
+
Open3.popen3(env, *argv, chdir: chdir, **process_options) do |input, output, stderr_stream, wait_thread|
|
|
465
|
+
streams = [output, stderr_stream]
|
|
466
|
+
streams << $stdin if $stdin.tty? && !otp_coordinator
|
|
467
|
+
queue = Queue.new
|
|
468
|
+
readers = streams.map do |stream|
|
|
469
|
+
Thread.new do # rubocop:disable ThreadSafety/NewThread -- Windows pipes are not selectable with IO.select.
|
|
470
|
+
loop { queue << [stream, stream.readpartial(1024)] }
|
|
471
|
+
rescue IOError
|
|
472
|
+
queue << [stream, nil]
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
input_reader = readers[streams.index($stdin)] if streams.include?($stdin)
|
|
476
|
+
active_streams = streams.dup
|
|
477
|
+
begin
|
|
478
|
+
until active_streams.empty?
|
|
479
|
+
stream, chunk = queue.pop
|
|
480
|
+
unless chunk
|
|
481
|
+
active_streams.delete(stream)
|
|
482
|
+
next
|
|
483
|
+
end
|
|
484
|
+
if stream.equal?($stdin)
|
|
485
|
+
input.write(chunk)
|
|
486
|
+
next
|
|
487
|
+
end
|
|
488
|
+
if stream.equal?(output)
|
|
489
|
+
captured_stdout << chunk
|
|
490
|
+
transcript_write(log_io, chunk)
|
|
491
|
+
stdout_line_buffer = print_interactive_stdout(stdout_line_buffer, chunk, stdout_line_handler, passthrough_output: passthrough_output)
|
|
492
|
+
else
|
|
493
|
+
captured_stderr << chunk
|
|
494
|
+
transcript_write(log_io, chunk)
|
|
495
|
+
$stderr.print(chunk) if passthrough_output
|
|
496
|
+
end
|
|
497
|
+
prompt_buffer = handle_interactive_prompt(input, chunk, member_name: member_name, prompt_buffer: prompt_buffer)
|
|
498
|
+
end
|
|
499
|
+
status = wait_thread.value
|
|
500
|
+
rescue Error => exception
|
|
501
|
+
captured_stderr << "#{exception.message}\n"
|
|
502
|
+
transcript_write(log_io, "#{exception.message}\n")
|
|
503
|
+
terminate_process(wait_thread.pid)
|
|
504
|
+
status = failure_status
|
|
505
|
+
ensure
|
|
506
|
+
input_reader&.kill if input_reader&.alive?
|
|
507
|
+
readers.each(&:join)
|
|
508
|
+
end
|
|
509
|
+
flush_interactive_stdout(stdout_line_buffer, stdout_line_handler, passthrough_output: passthrough_output)
|
|
510
|
+
end
|
|
511
|
+
[captured_stdout, captured_stderr, status]
|
|
512
|
+
end
|
|
513
|
+
|
|
434
514
|
def read_interactive_stream(
|
|
435
515
|
reader,
|
|
436
516
|
output,
|
|
@@ -562,7 +642,10 @@ module Kettle
|
|
|
562
642
|
|
|
563
643
|
def write_otp_response(input, chunk, member_name:)
|
|
564
644
|
response = otp_coordinator.request(member_name: member_name || "release", chunk: chunk)
|
|
565
|
-
|
|
645
|
+
if response.to_s.empty?
|
|
646
|
+
input.close unless input.closed?
|
|
647
|
+
return
|
|
648
|
+
end
|
|
566
649
|
|
|
567
650
|
input.write("#{response}\n")
|
|
568
651
|
input.flush
|
|
@@ -586,7 +669,7 @@ module Kettle
|
|
|
586
669
|
end
|
|
587
670
|
|
|
588
671
|
def normalize_output(output)
|
|
589
|
-
output.to_s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
672
|
+
output.to_s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "").gsub("\r\n", "\n")
|
|
590
673
|
end
|
|
591
674
|
|
|
592
675
|
def failure_status
|
|
@@ -594,8 +677,8 @@ module Kettle
|
|
|
594
677
|
end
|
|
595
678
|
|
|
596
679
|
def terminate_process(pid)
|
|
597
|
-
Process.kill("TERM", pid)
|
|
598
|
-
rescue Errno::ESRCH, Errno::EPERM
|
|
680
|
+
Process.kill(Gem.win_platform? ? "KILL" : "TERM", pid)
|
|
681
|
+
rescue Errno::ESRCH, Errno::EPERM, Errno::EINVAL
|
|
599
682
|
nil
|
|
600
683
|
end
|
|
601
684
|
|
|
@@ -612,6 +695,23 @@ module Kettle
|
|
|
612
695
|
mise_argv = ["mise", "exec", "-C", member.root, "--"]
|
|
613
696
|
return [*mise_argv, *argv] if injected_env.empty?
|
|
614
697
|
|
|
698
|
+
if Gem.win_platform?
|
|
699
|
+
return [
|
|
700
|
+
*mise_argv,
|
|
701
|
+
"ruby",
|
|
702
|
+
"--disable=rubyopt",
|
|
703
|
+
"-e",
|
|
704
|
+
WINDOWS_ENV_EXEC_SCRIPT,
|
|
705
|
+
"--",
|
|
706
|
+
unset_env.length.to_s,
|
|
707
|
+
*unset_env.map { |key, _value| key.to_s },
|
|
708
|
+
set_env.length.to_s,
|
|
709
|
+
*set_env.map { |key, value| "#{key}=#{value}" },
|
|
710
|
+
"--",
|
|
711
|
+
*argv
|
|
712
|
+
]
|
|
713
|
+
end
|
|
714
|
+
|
|
615
715
|
[*mise_argv, "env", *injected_env, *argv]
|
|
616
716
|
end
|
|
617
717
|
|
data/lib/kettle/family/config.rb
CHANGED
|
@@ -402,9 +402,7 @@ module Kettle
|
|
|
402
402
|
private
|
|
403
403
|
|
|
404
404
|
def path_within_root?(path)
|
|
405
|
-
|
|
406
|
-
expanded_root = File.expand_path(root)
|
|
407
|
-
expanded_path == expanded_root || expanded_path.start_with?("#{expanded_root}/")
|
|
405
|
+
Paths.within?(path, root)
|
|
408
406
|
end
|
|
409
407
|
|
|
410
408
|
def expand_config_relative_path(value)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require_relative "paths"
|
|
4
5
|
|
|
5
6
|
module Kettle
|
|
6
7
|
module Family
|
|
@@ -36,7 +37,7 @@ module Kettle
|
|
|
36
37
|
|
|
37
38
|
(configured + discovered).each do |member|
|
|
38
39
|
existing = by_name[member.name]
|
|
39
|
-
raise Error, "duplicate family member #{member.name.inspect}" if existing && existing.root
|
|
40
|
+
raise Error, "duplicate family member #{member.name.inspect}" if existing && !same_directory?(existing.root, member.root)
|
|
40
41
|
|
|
41
42
|
by_name[member.name] = member
|
|
42
43
|
end
|
|
@@ -46,7 +47,7 @@ module Kettle
|
|
|
46
47
|
|
|
47
48
|
def discover_members
|
|
48
49
|
gemspecs = config.discover_member_roots.flat_map do |root|
|
|
49
|
-
|
|
50
|
+
Paths.glob(root, "**", "*.gemspec")
|
|
50
51
|
end
|
|
51
52
|
gemspecs.reject! { |path| excluded_gemspec?(path) }
|
|
52
53
|
gemspecs.map { |path| member_from_gemspec(path) }
|
|
@@ -82,7 +83,7 @@ module Kettle
|
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
def primary_gemspec(root)
|
|
85
|
-
gemspecs =
|
|
86
|
+
gemspecs = Paths.glob(root, "*.gemspec")
|
|
86
87
|
raise Error, "no gemspec found in #{root}" if gemspecs.empty?
|
|
87
88
|
return gemspecs.first if gemspecs.one?
|
|
88
89
|
|
|
@@ -92,9 +93,10 @@ module Kettle
|
|
|
92
93
|
def warn_unlisted_discovered_members(discovered, configured)
|
|
93
94
|
return unless config.configured_member_roots? || config.explicit_members.any?
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
# Selection limits evaluation, not membership in the configuration.
|
|
97
|
+
roots = configured.map(&:root) + config.configured_member_roots + config.explicit_members.map { |entry| entry.fetch("root") }
|
|
96
98
|
discovered.each do |member|
|
|
97
|
-
next if
|
|
99
|
+
next if roots.any? { |root| same_directory?(root, member.root) }
|
|
98
100
|
|
|
99
101
|
warnings << {
|
|
100
102
|
"kind" => "unlisted_discovered_member",
|
|
@@ -106,7 +108,11 @@ module Kettle
|
|
|
106
108
|
end
|
|
107
109
|
|
|
108
110
|
def normalized_path(path)
|
|
109
|
-
|
|
111
|
+
Paths.canonical(path).tr("\\", "/")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def same_directory?(left, right)
|
|
115
|
+
Paths.same?(left, right)
|
|
110
116
|
end
|
|
111
117
|
|
|
112
118
|
def member_from_gemspec(path)
|
|
@@ -170,7 +176,7 @@ module Kettle
|
|
|
170
176
|
canonical = File.join(root, "lib", gem_name.tr("-", "_"), "version.rb")
|
|
171
177
|
return canonical if File.file?(canonical)
|
|
172
178
|
|
|
173
|
-
candidates =
|
|
179
|
+
candidates = Paths.glob(root, "lib", "**", "version.rb")
|
|
174
180
|
candidates.min
|
|
175
181
|
end
|
|
176
182
|
|
|
@@ -218,8 +224,36 @@ module Kettle
|
|
|
218
224
|
|
|
219
225
|
def path_matches_pattern?(path, pattern)
|
|
220
226
|
relative_candidates(path).any? do |relative|
|
|
221
|
-
|
|
227
|
+
path_glob_match?(pattern, relative)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def path_glob_match?(pattern, path)
|
|
232
|
+
pattern_parts = pattern.tr("\\", "/").split("/")
|
|
233
|
+
path_parts = path.tr("\\", "/").split("/")
|
|
234
|
+
if Gem.win_platform?
|
|
235
|
+
pattern_parts.map!(&:downcase)
|
|
236
|
+
path_parts.map!(&:downcase)
|
|
222
237
|
end
|
|
238
|
+
|
|
239
|
+
path_glob_parts_match?(pattern_parts, path_parts, {})
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def path_glob_parts_match?(pattern_parts, path_parts, memo)
|
|
243
|
+
key = [pattern_parts.length, path_parts.length]
|
|
244
|
+
return memo[key] if memo.key?(key)
|
|
245
|
+
return memo[key] = path_parts.empty? if pattern_parts.empty?
|
|
246
|
+
|
|
247
|
+
pattern_part = pattern_parts.first
|
|
248
|
+
matched = if pattern_part == "**"
|
|
249
|
+
path_glob_parts_match?(pattern_parts.drop(1), path_parts, memo) ||
|
|
250
|
+
(!path_parts.empty? && path_glob_parts_match?(pattern_parts, path_parts.drop(1), memo))
|
|
251
|
+
else
|
|
252
|
+
!path_parts.empty? &&
|
|
253
|
+
File.fnmatch?(pattern_part, path_parts.first, File::FNM_DOTMATCH | File::FNM_EXTGLOB) &&
|
|
254
|
+
path_glob_parts_match?(pattern_parts.drop(1), path_parts.drop(1), memo)
|
|
255
|
+
end
|
|
256
|
+
memo[key] = matched
|
|
223
257
|
end
|
|
224
258
|
|
|
225
259
|
def relative_candidates(path)
|
|
@@ -229,12 +263,7 @@ module Kettle
|
|
|
229
263
|
end
|
|
230
264
|
|
|
231
265
|
def relative_path(path, root)
|
|
232
|
-
|
|
233
|
-
expanded_root = File.expand_path(root)
|
|
234
|
-
prefix = "#{expanded_root}/"
|
|
235
|
-
return unless expanded_path.start_with?(prefix)
|
|
236
|
-
|
|
237
|
-
expanded_path.delete_prefix(prefix)
|
|
266
|
+
Paths.relative(path, root)
|
|
238
267
|
end
|
|
239
268
|
|
|
240
269
|
class GemfileDependencyCollector
|
|
@@ -7,6 +7,7 @@ require "open3"
|
|
|
7
7
|
require "time"
|
|
8
8
|
|
|
9
9
|
require_relative "concurrency"
|
|
10
|
+
require_relative "paths"
|
|
10
11
|
|
|
11
12
|
module Kettle
|
|
12
13
|
module Family
|
|
@@ -140,7 +141,7 @@ module Kettle
|
|
|
140
141
|
|
|
141
142
|
raise Error, "install local dependency does not exist: #{path}" unless Dir.exist?(expanded)
|
|
142
143
|
|
|
143
|
-
gemspecs =
|
|
144
|
+
gemspecs = Paths.glob(expanded, "*.gemspec")
|
|
144
145
|
raise Error, "no gemspec found for install local dependency: #{path}" if gemspecs.empty?
|
|
145
146
|
raise Error, "multiple gemspecs found for install local dependency: #{path}" if gemspecs.size > 1
|
|
146
147
|
|
|
@@ -163,7 +164,7 @@ module Kettle
|
|
|
163
164
|
canonical = File.join(root, "lib", gem_name.tr("-", "_"), "version.rb")
|
|
164
165
|
return canonical if File.file?(canonical)
|
|
165
166
|
|
|
166
|
-
|
|
167
|
+
Paths.glob(root, "lib", "**", "version.rb").min
|
|
167
168
|
end
|
|
168
169
|
|
|
169
170
|
def required_ruby_version(spec)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "rubygems"
|
|
4
4
|
require "kettle/dev"
|
|
5
5
|
|
|
6
|
+
require_relative "paths"
|
|
7
|
+
|
|
6
8
|
module Kettle
|
|
7
9
|
module Family
|
|
8
10
|
class NomonoBootstrap
|
|
@@ -91,7 +93,7 @@ module Kettle
|
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
def local_gemfile_edits(member)
|
|
94
|
-
|
|
96
|
+
Paths.glob(member.root, "gemfiles/modular/**/*_local.gemfile").sort.flat_map do |path|
|
|
95
97
|
source = File.read(path)
|
|
96
98
|
parse_result = Kettle::Dev::VersionBump.parse_source(source, path)
|
|
97
99
|
Kettle::Dev::VersionBump.each_node(parse_result.value).filter_map do |node|
|
|
@@ -14,7 +14,7 @@ module Kettle
|
|
|
14
14
|
@by_name = members.to_h { |member| [member.name, member] }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def ordered
|
|
17
|
+
def ordered(allow_cycles: false)
|
|
18
18
|
case mode
|
|
19
19
|
when "dependency"
|
|
20
20
|
tsort
|
|
@@ -24,6 +24,9 @@ module Kettle
|
|
|
24
24
|
raise Error, "unknown order mode #{mode.inspect}"
|
|
25
25
|
end
|
|
26
26
|
rescue TSort::Cyclic => error
|
|
27
|
+
# Read-only state inspection must remain available to diagnose cycles.
|
|
28
|
+
return hinted_members if allow_cycles
|
|
29
|
+
|
|
27
30
|
raise Error, "dependency cycle detected: #{error.message}"
|
|
28
31
|
end
|
|
29
32
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "kettle/dev"
|
|
5
|
+
|
|
6
|
+
module Kettle
|
|
7
|
+
module Family
|
|
8
|
+
module Paths
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def canonical(path, base: nil)
|
|
12
|
+
Kettle::Dev::Paths.canonical(path, base: base)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def same?(left, right, base: nil)
|
|
16
|
+
Kettle::Dev::Paths.same?(left, right, base: base)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def within?(path, root, base: nil)
|
|
20
|
+
Kettle::Dev::Paths.within?(path, root, base: base)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Computes a relative path by walking filesystem ancestors. Component
|
|
24
|
+
# string comparisons are insufficient on Windows, where equivalent
|
|
25
|
+
# paths may have different drive-letter casing or filesystem aliases.
|
|
26
|
+
def relative(path, root)
|
|
27
|
+
candidate = canonical(path)
|
|
28
|
+
boundary = canonical(root)
|
|
29
|
+
return "." if same?(candidate, boundary)
|
|
30
|
+
|
|
31
|
+
is_directory = File.directory?(candidate)
|
|
32
|
+
current = is_directory ? candidate : File.dirname(candidate)
|
|
33
|
+
parts = is_directory ? [] : [File.basename(candidate)]
|
|
34
|
+
|
|
35
|
+
until same?(current, boundary)
|
|
36
|
+
parent = File.dirname(current)
|
|
37
|
+
return unless parent != current
|
|
38
|
+
|
|
39
|
+
parts.unshift(File.basename(current))
|
|
40
|
+
current = parent
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
File.join(*parts).tr("\\", "/")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def glob(*parts)
|
|
47
|
+
Dir.glob(File.join(*parts).tr("\\", "/"))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def local_path_remote?(remote)
|
|
51
|
+
text = remote.to_s
|
|
52
|
+
drive_absolute = text.length >= 3 && text[1] == ":" && ["/", "\\"].include?(text[2])
|
|
53
|
+
drive_absolute || text.start_with?("/", "./", "../", ".\\", "..\\") || Pathname.new(text).absolute?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "paths"
|
|
4
|
+
|
|
3
5
|
module Kettle
|
|
4
6
|
module Family
|
|
5
7
|
class ReadinessCheck
|
|
@@ -43,7 +45,8 @@ module Kettle
|
|
|
43
45
|
def missing_required_bins
|
|
44
46
|
required_bins.filter_map do |path|
|
|
45
47
|
full_path = File.join(member.root, path)
|
|
46
|
-
|
|
48
|
+
executable = Gem.win_platform? || File.executable?(full_path)
|
|
49
|
+
next if File.file?(full_path) && executable
|
|
47
50
|
|
|
48
51
|
"missing executable binstub #{path}"
|
|
49
52
|
end
|
|
@@ -100,7 +103,8 @@ module Kettle
|
|
|
100
103
|
return [] unless File.file?(lockfile)
|
|
101
104
|
|
|
102
105
|
File.readlines(lockfile).filter_map.with_index(1) do |line, index|
|
|
103
|
-
next unless line.start_with?(" remote:
|
|
106
|
+
next unless line.start_with?(" remote:")
|
|
107
|
+
next unless Paths.local_path_remote?(line.split("remote:", 2).last.to_s.strip)
|
|
104
108
|
next if allowed_local_path?(line)
|
|
105
109
|
|
|
106
110
|
"release lockfile has local path remote at Gemfile.lock:#{index}"
|
|
@@ -112,17 +116,14 @@ module Kettle
|
|
|
112
116
|
remote_path = normalized_path(remote, base: member.root)
|
|
113
117
|
return false unless remote_path
|
|
114
118
|
|
|
115
|
-
allowed_local_path_roots.any? { |root|
|
|
119
|
+
allowed_local_path_roots.any? { |root| Paths.within?(remote_path, root) }
|
|
116
120
|
end
|
|
117
121
|
|
|
118
122
|
def normalized_path(path, base: nil)
|
|
119
123
|
text = path.to_s
|
|
120
124
|
return nil if text.empty? || text.casecmp("false").zero?
|
|
121
125
|
|
|
122
|
-
|
|
123
|
-
File.realpath(expanded)
|
|
124
|
-
rescue Errno::ENOENT
|
|
125
|
-
expanded
|
|
126
|
+
Paths.canonical(text, base: base)
|
|
126
127
|
end
|
|
127
128
|
|
|
128
129
|
def required_files
|