kettle-family 1.3.1 → 1.3.3
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 +89 -1
- data/README.md +1 -1
- data/lib/kettle/family/branch_target_config.rb +9 -6
- data/lib/kettle/family/command_runner.rb +105 -5
- data/lib/kettle/family/config.rb +1 -3
- data/lib/kettle/family/discovery.rb +41 -14
- data/lib/kettle/family/local_install.rb +3 -2
- data/lib/kettle/family/nomono_bootstrap.rb +3 -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 +18 -2
- 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 +213 -64
- 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: 2fd15138050afedb68be9d6d294f7e3a01dc1554949c8607b1176ddf8d061581
|
|
4
|
+
data.tar.gz: 272997658ef8c5a96b14fbc1dcc8d124b545abe50d5bc0ca423823c8dffc3ad8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5bac59bea82eec800e67069ae00f8810c54a29a7fe3119b4c9ef93f94924af677b0da411635928841d9c0df47d4bfb11853d622cccaf1c72faf6f6419a552d6
|
|
7
|
+
data.tar.gz: c4d9b06b6fed1f0822bdc62ec6d22b3cb379bc1fe7b49e388038e58780c567ecbe3f589e11faa7845e92b2b6b7d365499271f17d0ad8e942636d632f25dbf950
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,90 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.3.3] - 2026-09-28
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.3.3][1.3.3t]
|
|
36
|
+
- COVERAGE: 93.25% -- 6538/7011 lines in 37 files
|
|
37
|
+
- BRANCH COVERAGE: 77.76% -- 2660/3421 branches in 37 files
|
|
38
|
+
- 33.57% documented
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Compare filesystem paths by canonical identity throughout family workflows.
|
|
43
|
+
|
|
44
|
+
## [1.3.2] - 2026-09-28
|
|
45
|
+
|
|
46
|
+
- TAG: [v1.3.2][1.3.2t]
|
|
47
|
+
- COVERAGE: 93.25% -- 6537/7010 lines in 37 files
|
|
48
|
+
- BRANCH COVERAGE: 77.77% -- 2659/3419 branches in 37 files
|
|
49
|
+
- 33.57% documented
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- kettle-jem-template-20260913-001 - Templating now also surfaces a review
|
|
54
|
+
entry in `dependency_conflicts.resolve` when a direct development
|
|
55
|
+
dependency doesn't support one or more of this project's declared
|
|
56
|
+
`engines:` and has no template-managed modular home (e.g. `sqlite3` on
|
|
57
|
+
`jruby`). Review each entry and pick a resolution per the project's own
|
|
58
|
+
engine support needs.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- [kc] kettle-jem/prepare: updated 13 project files:
|
|
63
|
+
- dependencies (13)
|
|
64
|
+
|
|
65
|
+
- [kc] kettle-jem/template: updated 14 project files:
|
|
66
|
+
- code and tests (2)
|
|
67
|
+
- dependencies (2)
|
|
68
|
+
- other (1)
|
|
69
|
+
- workflows (9)
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- Run local Kettle Jem through a prepared member bundle so local StructuredMerge dependencies are activated during templating.
|
|
74
|
+
|
|
75
|
+
- Pin bupb to an installed stable Bundler version, including its managed child updates.
|
|
76
|
+
|
|
77
|
+
- [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.
|
|
78
|
+
|
|
79
|
+
- Recover template lockfiles with Bundler's valid bundle lock --update GEM syntax instead of passing gem names as invalid positional arguments.
|
|
80
|
+
|
|
81
|
+
- 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.
|
|
82
|
+
|
|
83
|
+
- Refresh dependent lockfiles before committing newly raised family dependency floors.
|
|
84
|
+
|
|
85
|
+
- Handle macOS canonical temp paths, Unix socket limits, and PTY EOF during family workflows.
|
|
86
|
+
|
|
87
|
+
- Canonicalize Windows system temporary paths for Git-based specs, launch the Mise integration fixture on Windows, and include WEBrick for local registry specs.
|
|
88
|
+
|
|
89
|
+
- Readiness checks accept existing binstub files on Windows, where Unix executable-bit checks are not portable.
|
|
90
|
+
|
|
91
|
+
- Make family specs portable on Windows and use valid generated gemspec fixtures.
|
|
92
|
+
|
|
93
|
+
- Support Windows pipe-driven interactive commands, absolute local-path lockfile remotes, and equivalent filesystem path spellings in family workflows.
|
|
94
|
+
|
|
95
|
+
- Apply workflow environment overrides portably after mise and normalize Windows filesystem paths in discovery and branch configuration.
|
|
96
|
+
|
|
97
|
+
- Normalize filesystem glob separators across supported platforms so family discovery, installs, and workflow asset lookup find paths on Windows.
|
|
98
|
+
|
|
99
|
+
- Normalize Windows path aliases and subprocess line endings in family workflows.
|
|
100
|
+
|
|
101
|
+
- Close the child input stream when a configured OTP provider returns no code, preventing interactive release commands from hanging.
|
|
102
|
+
|
|
103
|
+
- Start the Windows environment wrapper without inherited RUBYOPT so Bundler cannot activate the parent bundle before wrapper overrides are applied.
|
|
104
|
+
|
|
105
|
+
- 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.
|
|
106
|
+
|
|
107
|
+
- Windows family workflow specs now preserve command arguments and handle case-insensitive checkout paths.
|
|
108
|
+
|
|
109
|
+
- Detect Windows drive-absolute local paths when preparing release lockfile normalization.
|
|
110
|
+
|
|
111
|
+
- Delegate canonical, identity, and containment path checks to kettle-dev so family workflows consistently handle symlink aliases and platform path semantics.
|
|
112
|
+
|
|
113
|
+
- Windows family discovery now applies exclusion globs across path separators, and branch-target config lookup uses shared path normalization.
|
|
114
|
+
|
|
115
|
+
- Resolve family-relative paths by filesystem identity so discovery exclusions and branch-stack configs work with Windows path aliases.
|
|
116
|
+
|
|
33
117
|
## [1.3.1] - 2026-09-10
|
|
34
118
|
|
|
35
119
|
- TAG: [v1.3.1][1.3.1t]
|
|
@@ -2746,7 +2830,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
2746
2830
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
2747
2831
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
2748
2832
|
|
|
2749
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.
|
|
2833
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.3.3...HEAD
|
|
2834
|
+
[1.3.3]: https://github.com/kettle-dev/kettle-family/compare/v1.3.2...v1.3.3
|
|
2835
|
+
[1.3.3t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.3
|
|
2836
|
+
[1.3.2]: https://github.com/kettle-dev/kettle-family/compare/v1.3.1...v1.3.2
|
|
2837
|
+
[1.3.2t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.2
|
|
2750
2838
|
[1.3.1]: https://github.com/kettle-dev/kettle-family/compare/v1.3.0...v1.3.1
|
|
2751
2839
|
[1.3.1t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.3.1
|
|
2752
2840
|
[1.3.0]: https://github.com/kettle-dev/kettle-family/compare/v1.2.100...v1.3.0
|
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.011-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
|
|
@@ -54,7 +55,7 @@ module Kettle
|
|
|
54
55
|
return false if left.to_s.empty? || right.to_s.empty?
|
|
55
56
|
return false unless File.file?(left) && File.file?(right)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
Paths.same?(left, right)
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
def member_local_release_config_from_branch(member)
|
|
@@ -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|
|
|
@@ -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
|
|
|
@@ -94,9 +95,8 @@ module Kettle
|
|
|
94
95
|
|
|
95
96
|
# Selection limits evaluation, not membership in the configuration.
|
|
96
97
|
roots = configured.map(&:root) + config.configured_member_roots + config.explicit_members.map { |entry| entry.fetch("root") }
|
|
97
|
-
configured_roots = roots.map { |root| normalized_path(root) }
|
|
98
98
|
discovered.each do |member|
|
|
99
|
-
next if
|
|
99
|
+
next if roots.any? { |root| same_directory?(root, member.root) }
|
|
100
100
|
|
|
101
101
|
warnings << {
|
|
102
102
|
"kind" => "unlisted_discovered_member",
|
|
@@ -108,7 +108,11 @@ module Kettle
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def normalized_path(path)
|
|
111
|
-
|
|
111
|
+
Paths.canonical(path).tr("\\", "/")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def same_directory?(left, right)
|
|
115
|
+
Paths.same?(left, right)
|
|
112
116
|
end
|
|
113
117
|
|
|
114
118
|
def member_from_gemspec(path)
|
|
@@ -172,7 +176,7 @@ module Kettle
|
|
|
172
176
|
canonical = File.join(root, "lib", gem_name.tr("-", "_"), "version.rb")
|
|
173
177
|
return canonical if File.file?(canonical)
|
|
174
178
|
|
|
175
|
-
candidates =
|
|
179
|
+
candidates = Paths.glob(root, "lib", "**", "version.rb")
|
|
176
180
|
candidates.min
|
|
177
181
|
end
|
|
178
182
|
|
|
@@ -220,8 +224,36 @@ module Kettle
|
|
|
220
224
|
|
|
221
225
|
def path_matches_pattern?(path, pattern)
|
|
222
226
|
relative_candidates(path).any? do |relative|
|
|
223
|
-
|
|
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)
|
|
224
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
|
|
225
257
|
end
|
|
226
258
|
|
|
227
259
|
def relative_candidates(path)
|
|
@@ -231,12 +263,7 @@ module Kettle
|
|
|
231
263
|
end
|
|
232
264
|
|
|
233
265
|
def relative_path(path, root)
|
|
234
|
-
|
|
235
|
-
expanded_root = File.expand_path(root)
|
|
236
|
-
prefix = "#{expanded_root}/"
|
|
237
|
-
return unless expanded_path.start_with?(prefix)
|
|
238
|
-
|
|
239
|
-
expanded_path.delete_prefix(prefix)
|
|
266
|
+
Paths.relative(path, root)
|
|
240
267
|
end
|
|
241
268
|
|
|
242
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|
|
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "paths"
|
|
4
5
|
|
|
5
6
|
module Kettle
|
|
6
7
|
module Family
|
|
@@ -57,7 +58,7 @@ module Kettle
|
|
|
57
58
|
raise Error, "#{name} release graph requires a local path selector" if selector_env.empty?
|
|
58
59
|
|
|
59
60
|
selector_env.each_value do |value|
|
|
60
|
-
next if local_path_roots.any? { |root|
|
|
61
|
+
next if local_path_roots.any? { |root| Paths.within?(value, root) }
|
|
61
62
|
|
|
62
63
|
raise Error, "#{name} selector #{value.inspect} is outside its declared local path roots"
|
|
63
64
|
end
|
|
@@ -66,17 +67,14 @@ module Kettle
|
|
|
66
67
|
|
|
67
68
|
raise Error, "monorepo_ci_local release graph requires a CI root" if ci_root.to_s.empty?
|
|
68
69
|
local_path_roots.each do |path|
|
|
69
|
-
next if
|
|
70
|
+
next if Paths.within?(path, ci_root)
|
|
70
71
|
|
|
71
72
|
raise Error, "monorepo_ci_local path #{path.inspect} is outside CI root #{ci_root.inspect}"
|
|
72
73
|
end
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
def canonical_path(path)
|
|
76
|
-
|
|
77
|
-
File.realpath(expanded)
|
|
78
|
-
rescue Errno::ENOENT
|
|
79
|
-
expanded
|
|
77
|
+
Paths.canonical(path)
|
|
80
78
|
end
|
|
81
79
|
end
|
|
82
80
|
end
|