kettle-family 1.2.29 → 1.2.30
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 +48 -1
- data/README.md +1 -1
- data/lib/kettle/family/cli.rb +83 -8
- data/lib/kettle/family/release_state_check.rb +63 -20
- data/lib/kettle/family/release_state_event_tape.rb +52 -0
- data/lib/kettle/family/report.rb +5 -2
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +169 -18
- data/lib/kettle/family/workflow_progress.rb +47 -7
- data/lib/kettle/family.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +5 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 945e285ec99d1d336abdf5a51c05135c99485cafeb1b47a65b81ab72818a6786
|
|
4
|
+
data.tar.gz: 29bd40256a7b84edaf06dd9adc4f0d21db43a470b07e71b17dc3295e23f9fe4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a25b8669590d61a7a2ecd48fc64bf7d32d7d6090fcea810b13673534827f6609f700ecbeb0ef8b5dded509c4de135f385323a034a236bfd092a9d789c93d178
|
|
7
|
+
data.tar.gz: '0508285a2bc3905833dffe7e0387909613f0ff669d9e600f640d1a052ab53f06fb67d6a7bc64a4bfbae1b8175f4bf5efc268f22e012fe6978802cbc01b4da55b'
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,51 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.2.30] - 2026-08-04
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.2.30][1.2.30t]
|
|
36
|
+
- COVERAGE: 94.10% -- 4944/5254 lines in 32 files
|
|
37
|
+
- BRANCH COVERAGE: 76.51% -- 1964/2567 branches in 32 files
|
|
38
|
+
- 29.86% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Emit per-member NDJSON event tapes while deriving release state.
|
|
43
|
+
|
|
44
|
+
- Render plain state analysis as stable per-member TTY progress rows before the final release-state table.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- Anchor TTY progress redraws and reserve a terminal column to prevent stale or wrapped rows.
|
|
49
|
+
|
|
50
|
+
- Recover template autostashes when generated lockfiles are uncommitted.
|
|
51
|
+
|
|
52
|
+
- Keep release secret-provider notifications on the dedicated TTY line.
|
|
53
|
+
|
|
54
|
+
- Recover autostashes when generated lockfiles cause staged merge conflicts
|
|
55
|
+
|
|
56
|
+
- Recognize nested monorepo lockfiles during template autostash recovery
|
|
57
|
+
|
|
58
|
+
- TTY release progress now anchors redraws at column one so out-of-band notifications cannot concatenate with event tape rows.
|
|
59
|
+
|
|
60
|
+
- Keep transient progress notifications on a dedicated line above the event tape with independent row accounting.
|
|
61
|
+
|
|
62
|
+
- Preserve inherited local-path dependency environments during template bootstrap and lockfile recovery.
|
|
63
|
+
|
|
64
|
+
- Keep secret-provider notifications off the TTY event tape in verbose and debug modes.
|
|
65
|
+
|
|
66
|
+
- Normalize release-state event detail keys before writing NDJSON tapes.
|
|
67
|
+
|
|
68
|
+
- Forward --events so state analysis is streamed as NDJSON instead of falling back to the text report.
|
|
69
|
+
|
|
70
|
+
- Separate state analysis progress from the final release-state report on TTY output.
|
|
71
|
+
|
|
72
|
+
- Restore all generated lockfiles during monorepo template autostash recovery.
|
|
73
|
+
|
|
74
|
+
- Recover nested monorepo autostashes from the repository root.
|
|
75
|
+
|
|
76
|
+
- Restore generated Gemfile.lock files between branch-matrix test and lint targets.
|
|
77
|
+
|
|
33
78
|
## [1.2.29] - 2026-08-03
|
|
34
79
|
|
|
35
80
|
- TAG: [v1.2.29][1.2.29t]
|
|
@@ -1742,7 +1787,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
1742
1787
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
1743
1788
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
1744
1789
|
|
|
1745
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.
|
|
1790
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.30...HEAD
|
|
1791
|
+
[1.2.30]: https://github.com/kettle-dev/kettle-family/compare/v1.2.29...v1.2.30
|
|
1792
|
+
[1.2.30t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.30
|
|
1746
1793
|
[1.2.29]: https://github.com/kettle-dev/kettle-family/compare/v1.2.28...v1.2.29
|
|
1747
1794
|
[1.2.29t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.29
|
|
1748
1795
|
[1.2.28]: https://github.com/kettle-dev/kettle-family/compare/v1.2.27...v1.2.28
|
data/README.md
CHANGED
|
@@ -828,7 +828,7 @@ Thanks for RTFM. ☺️
|
|
|
828
828
|
[📌gitmoji]: https://gitmoji.dev
|
|
829
829
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
830
830
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
831
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-5.
|
|
831
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-5.254-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
832
832
|
[🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
|
|
833
833
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
834
834
|
[📄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
|
@@ -21,6 +21,7 @@ module Kettle
|
|
|
21
21
|
option :config, value: {type: String, usage: "PATH"}, desc: "Family config path"
|
|
22
22
|
option :json, desc: "Print JSON report to stdout"
|
|
23
23
|
option :report, value: {type: String, usage: "PATH"}, desc: "Write JSON report to PATH"
|
|
24
|
+
option :events, desc: "Stream release-state analysis as NDJSON"
|
|
24
25
|
|
|
25
26
|
def self.call(argv, out: $stdout, err: $stderr)
|
|
26
27
|
main(argv, stdout: out, stderr: err)
|
|
@@ -32,6 +33,7 @@ module Kettle
|
|
|
32
33
|
base.option :config, value: {type: String, usage: "PATH"}, desc: "Family config path"
|
|
33
34
|
base.option :json, desc: "Print JSON report to stdout"
|
|
34
35
|
base.option :report, value: {type: String, usage: "PATH"}, desc: "Write JSON report to PATH"
|
|
36
|
+
base.option :events, desc: "Stream release-state analysis as NDJSON"
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -136,6 +138,7 @@ module Kettle
|
|
|
136
138
|
exclude: options[:exclude],
|
|
137
139
|
start_at: options[:start_at],
|
|
138
140
|
json: truthy_option?(:json),
|
|
141
|
+
events: truthy_option?(:events),
|
|
139
142
|
report: options[:report],
|
|
140
143
|
execute: truthy_option?(:execute),
|
|
141
144
|
debug: truthy_option?(:debug),
|
|
@@ -613,7 +616,11 @@ module Kettle
|
|
|
613
616
|
def run_command(command, options)
|
|
614
617
|
report = build_report(command, options)
|
|
615
618
|
write_report(report, options)
|
|
616
|
-
|
|
619
|
+
if options[:events] && command == "release-state"
|
|
620
|
+
stdout.puts(JSON.generate("event_version" => 1, "type" => "summary", "report" => report.to_h))
|
|
621
|
+
else
|
|
622
|
+
stdout.puts(options[:json] ? report.to_json : report.to_text)
|
|
623
|
+
end
|
|
617
624
|
report.success? ? 0 : 1
|
|
618
625
|
rescue Error, OptionParser::ParseError => error
|
|
619
626
|
stderr.puts("kettle-family: #{error.message}")
|
|
@@ -638,6 +645,7 @@ module Kettle
|
|
|
638
645
|
else
|
|
639
646
|
Orderer.new(members: members, mode: config.order_mode, hints: config.order_hints).ordered
|
|
640
647
|
end
|
|
648
|
+
state_event_tape = release_state_event_tape(command: command, config: config, options: options)
|
|
641
649
|
release_state_results = release_state_results_for_selection(config: config, members: ordered, only: effective_only)
|
|
642
650
|
selected = Selection.new(members: ordered, release_state_results: release_state_results).apply(only: effective_only, exclude: options[:exclude], start_at: start_at.member)
|
|
643
651
|
result_members = selected
|
|
@@ -645,7 +653,22 @@ module Kettle
|
|
|
645
653
|
display_selected_members = display_members_for(command: command, config: config, members: selected, selected_members: selected)
|
|
646
654
|
print_execution_intent(command: command, config: config, members: display_selected_members, options: options, start_at: start_at)
|
|
647
655
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
648
|
-
|
|
656
|
+
state_progress = release_state_progress(command: command, members: result_members, options: options)
|
|
657
|
+
state_progress&.start
|
|
658
|
+
state_event_handler = release_state_event_handler(
|
|
659
|
+
event_tape: state_event_tape,
|
|
660
|
+
progress: state_progress,
|
|
661
|
+
members: result_members
|
|
662
|
+
)
|
|
663
|
+
results = command_results(
|
|
664
|
+
command: command,
|
|
665
|
+
config: config,
|
|
666
|
+
members: result_members,
|
|
667
|
+
options: options,
|
|
668
|
+
start_at: start_at,
|
|
669
|
+
state_event_handler: state_event_handler
|
|
670
|
+
)
|
|
671
|
+
state_progress&.finish
|
|
649
672
|
elapsed_seconds = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
650
673
|
Report.new(
|
|
651
674
|
family_name: config.family_name,
|
|
@@ -661,17 +684,18 @@ module Kettle
|
|
|
661
684
|
command: command,
|
|
662
685
|
results: results,
|
|
663
686
|
warnings: discovery.warnings,
|
|
687
|
+
event_log_dirs: state_event_tape ? [state_event_tape.directory] : [],
|
|
664
688
|
elapsed_seconds: elapsed_seconds
|
|
665
689
|
)
|
|
666
690
|
end
|
|
667
691
|
|
|
668
692
|
StartAt = Struct.new(:member, :branch)
|
|
669
693
|
|
|
670
|
-
def command_results(command:, config:, members:, options:, start_at:)
|
|
694
|
+
def command_results(command:, config:, members:, options:, start_at:, state_event_handler: nil)
|
|
671
695
|
return branch_target_command_results(command: command, config: config, members: members, options: options, start_at: start_at) if branch_target_command?(command, config)
|
|
672
696
|
return member_local_branch_target_command_results(command: command, config: config, members: members, options: options, start_at: start_at) if member_local_branch_target_command?(command, config, members)
|
|
673
697
|
|
|
674
|
-
command_results_for_current_branch(command: command, config: config, members: members, options: options, start_at: start_at)
|
|
698
|
+
command_results_for_current_branch(command: command, config: config, members: members, options: options, start_at: start_at, state_event_handler: state_event_handler)
|
|
675
699
|
end
|
|
676
700
|
|
|
677
701
|
def release_state_results_for_selection(config:, members:, only:)
|
|
@@ -712,14 +736,14 @@ module Kettle
|
|
|
712
736
|
members
|
|
713
737
|
end
|
|
714
738
|
|
|
715
|
-
def command_results_for_current_branch(command:, config:, members:, options:, start_at: StartAt.new(nil, nil))
|
|
739
|
+
def command_results_for_current_branch(command:, config:, members:, options:, start_at: StartAt.new(nil, nil), state_event_handler: nil)
|
|
716
740
|
return mise_trust_results(config: config, members: members) if command == "mise-trust"
|
|
717
741
|
return bump_version_results(members: members, options: options, phase: command) if %w[bump bump-version].include?(command)
|
|
718
742
|
return add_changelog_results(members: members, options: options) if command == "add-changelog"
|
|
719
743
|
return clean_unreleased_results(config: config, members: members, options: options) if command == "clean-unreleased"
|
|
720
744
|
return reconcile_release_results(config: config, members: members, options: options) if command == "reconcile-releases"
|
|
721
745
|
return branch_lane_results(config: config, members: members) if command == "branch-lanes"
|
|
722
|
-
return release_state_results(config: config, members: members) if command == "release-state"
|
|
746
|
+
return release_state_results(config: config, members: members, event_handler: state_event_handler) if command == "release-state"
|
|
723
747
|
return install_results(config: config, members: members, options: options) if command == "install"
|
|
724
748
|
return [] unless WORKFLOW_COMMANDS.include?(command)
|
|
725
749
|
|
|
@@ -777,6 +801,49 @@ module Kettle
|
|
|
777
801
|
stdout
|
|
778
802
|
end
|
|
779
803
|
|
|
804
|
+
def release_state_progress(command:, members:, options:)
|
|
805
|
+
return nil unless command == "release-state"
|
|
806
|
+
return nil if options[:events] || options[:json]
|
|
807
|
+
return nil unless stdout.respond_to?(:tty?) && stdout.tty?
|
|
808
|
+
|
|
809
|
+
WorkflowProgress.new(
|
|
810
|
+
io: stdout,
|
|
811
|
+
label: "release state",
|
|
812
|
+
total: members.length,
|
|
813
|
+
jobs: [options[:jobs].to_i, 1].max,
|
|
814
|
+
members: members,
|
|
815
|
+
heading: "release state #{members.length} member#{"s" unless members.length == 1}:"
|
|
816
|
+
)
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def release_state_event_handler(event_tape:, progress:, members:)
|
|
820
|
+
return nil unless event_tape || progress
|
|
821
|
+
|
|
822
|
+
members_by_name = members.to_h { |member| [member.name, member] }
|
|
823
|
+
lambda do |event|
|
|
824
|
+
event_tape&.call(event)
|
|
825
|
+
next unless progress
|
|
826
|
+
|
|
827
|
+
member = members_by_name[event["member"]]
|
|
828
|
+
next unless member
|
|
829
|
+
|
|
830
|
+
action = event["action"].to_s
|
|
831
|
+
status = event["status"].to_s
|
|
832
|
+
case action
|
|
833
|
+
when "member_start"
|
|
834
|
+
progress.start_member(member, total: 5, status: "release_state")
|
|
835
|
+
when "member_complete"
|
|
836
|
+
progress.finish_member(member, success: status == "ok", status: status)
|
|
837
|
+
when "changelog_command", "computed_booleans", "git_state", "github_release", "transfer_changelog"
|
|
838
|
+
if status == "running"
|
|
839
|
+
progress.update(member, status: action)
|
|
840
|
+
elsif status == "ok" || status == "failed"
|
|
841
|
+
progress.advance(member, status: action, success: status == "ok")
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
end
|
|
846
|
+
|
|
780
847
|
def print_execution_intent(command:, config:, members:, options:, start_at:)
|
|
781
848
|
return unless command == "release"
|
|
782
849
|
return unless options[:execute]
|
|
@@ -1022,8 +1089,16 @@ module Kettle
|
|
|
1022
1089
|
LocalInstall.new(config: config, members: members, execute: options[:execute], jobs: options[:jobs]).results
|
|
1023
1090
|
end
|
|
1024
1091
|
|
|
1025
|
-
def release_state_results(config:, members:)
|
|
1026
|
-
ReleaseStateCheck.new(config: config, members: members).results
|
|
1092
|
+
def release_state_results(config:, members:, event_handler: nil)
|
|
1093
|
+
ReleaseStateCheck.new(config: config, members: members).results(event_handler: event_handler)
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
def release_state_event_tape(command:, config:, options:)
|
|
1097
|
+
return unless command == "release-state"
|
|
1098
|
+
|
|
1099
|
+
tape = ReleaseStateEventTape.new(root: config.root, stream: options[:events] ? stdout : nil)
|
|
1100
|
+
stderr.puts("release state event tapes: #{tape.directory}") unless options[:events]
|
|
1101
|
+
tape
|
|
1027
1102
|
end
|
|
1028
1103
|
|
|
1029
1104
|
def release_mode(command:, options:)
|
|
@@ -28,16 +28,16 @@ module Kettle
|
|
|
28
28
|
@transfer_changelog_total_count = TRANSFER_CHANGELOG_TOTAL_UNSET
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def results
|
|
32
|
-
return branch_results unless release_target_branches.empty?
|
|
33
|
-
return members.map { |member| check_shared_changelog_member_or_local(member) } if shared_changelog?
|
|
31
|
+
def results(event_handler: nil)
|
|
32
|
+
return branch_results(event_handler: event_handler) unless release_target_branches.empty?
|
|
33
|
+
return members.map { |member| check_shared_changelog_member_or_local(member, event_handler: event_handler) } if shared_changelog?
|
|
34
34
|
|
|
35
35
|
members.each_with_object([]) do |member, memo|
|
|
36
|
-
member_branch_results = member_local_branch_results(member)
|
|
36
|
+
member_branch_results = member_local_branch_results(member, event_handler: event_handler)
|
|
37
37
|
if member_branch_results
|
|
38
38
|
memo.concat(member_branch_results)
|
|
39
39
|
else
|
|
40
|
-
memo << check_member(member)
|
|
40
|
+
memo << check_member(member, event_handler: event_handler)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
end
|
|
@@ -46,63 +46,87 @@ module Kettle
|
|
|
46
46
|
|
|
47
47
|
attr_reader :members, :config
|
|
48
48
|
|
|
49
|
-
def branch_results
|
|
49
|
+
def branch_results(event_handler: nil)
|
|
50
50
|
root = git_root
|
|
51
51
|
selected_names = members.map(&:name)
|
|
52
52
|
release_target_branches.each_with_object([]) do |branch, memo|
|
|
53
53
|
with_branch_worktree(root: root, branch: branch) do |worktree_root|
|
|
54
54
|
branch_members = discover_branch_members(worktree_root: worktree_root, selected_names: selected_names)
|
|
55
55
|
if shared_changelog?
|
|
56
|
-
memo.concat(branch_members.map { |member| check_shared_changelog_member_or_local(member, branch: branch) })
|
|
56
|
+
memo.concat(branch_members.map { |member| check_shared_changelog_member_or_local(member, branch: branch, event_handler: event_handler) })
|
|
57
57
|
next
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
memo.concat(branch_members.map { |member| check_member(member, branch: branch) })
|
|
60
|
+
memo.concat(branch_members.map { |member| check_member(member, branch: branch, event_handler: event_handler) })
|
|
61
61
|
end
|
|
62
62
|
rescue Error => error
|
|
63
63
|
memo << error_result(branch: branch, error: error)
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def check_member(member, branch: nil)
|
|
67
|
+
def check_member(member, branch: nil, event_handler: nil)
|
|
68
68
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
69
69
|
command = release_state_command
|
|
70
|
+
emit_event(event_handler, member: member, branch: branch, action: "member_start", status: "running", command: command)
|
|
71
|
+
emit_event(event_handler, member: member, branch: branch, action: "changelog_command", status: "running", command: command)
|
|
70
72
|
stdout, stderr, status = Open3.capture3(release_state_env(member), *command, chdir: release_state_workdir(member))
|
|
71
73
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
72
74
|
success = status.success?
|
|
75
|
+
emit_event(event_handler, member: member, branch: branch, action: "changelog_command", status: success ? "ok" : "failed", elapsed_seconds: elapsed, status_code: status.exitstatus)
|
|
73
76
|
state = success ? JSON.parse(stdout) : {}
|
|
77
|
+
emit_event(event_handler, member: member, branch: branch, action: "computed_booleans", status: "running") if success
|
|
74
78
|
state = branch_filtered_state(member, state, branch) if success && branch
|
|
75
79
|
state = state_with_computed_booleans(state) if success
|
|
80
|
+
emit_event(event_handler, member: member, branch: branch, action: "computed_booleans", status: "ok") if success
|
|
81
|
+
emit_event(event_handler, member: member, branch: branch, action: "git_state", status: "running") if success
|
|
76
82
|
state = enrich_git_state(member.root, state, branch: branch) if success
|
|
83
|
+
emit_event(event_handler, member: member, branch: branch, action: "git_state", status: "ok") if success
|
|
84
|
+
emit_event(event_handler, member: member, branch: branch, action: "github_release", status: "running") if success
|
|
77
85
|
state = enrich_github_release(member.root, state, branch: branch) if success
|
|
86
|
+
emit_event(event_handler, member: member, branch: branch, action: "github_release", status: "ok") if success
|
|
87
|
+
emit_event(event_handler, member: member, branch: branch, action: "transfer_changelog", status: "running") if success
|
|
78
88
|
state = enrich_transfer_changelog_lag(member.root, state) if success
|
|
79
|
-
|
|
89
|
+
emit_event(event_handler, member: member, branch: branch, action: "transfer_changelog", status: "ok") if success
|
|
90
|
+
result = result(member: member, command: command, stdout: stdout, stderr: stderr, status: status.exitstatus, elapsed: elapsed, success: success, state: state, branch: branch)
|
|
91
|
+
emit_event(event_handler, member: member, branch: branch, action: "member_complete", status: success ? "ok" : "failed", elapsed_seconds: elapsed, status_code: status.exitstatus)
|
|
92
|
+
result
|
|
80
93
|
rescue JSON::ParserError => error
|
|
81
|
-
result(member: member, command: command || release_state_command, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}", branch: branch)
|
|
94
|
+
result = result(member: member, command: command || release_state_command, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}", branch: branch)
|
|
95
|
+
emit_event(event_handler, member: member, branch: branch, action: "member_complete", status: "failed", elapsed_seconds: elapsed || 0.0, status_code: 1, reason: result.reason)
|
|
96
|
+
result
|
|
82
97
|
end
|
|
83
98
|
|
|
84
99
|
def release_state_command
|
|
85
100
|
[RbConfig.ruby, "-S", "kettle-changelog", "--release-state", "--json"]
|
|
86
101
|
end
|
|
87
102
|
|
|
88
|
-
def check_shared_changelog_member_or_local(member, branch: nil)
|
|
89
|
-
return check_member(member, branch: branch) if member_local_changelog?(member)
|
|
103
|
+
def check_shared_changelog_member_or_local(member, branch: nil, event_handler: nil)
|
|
104
|
+
return check_member(member, branch: branch, event_handler: event_handler) if member_local_changelog?(member)
|
|
90
105
|
|
|
91
|
-
check_shared_changelog_member(member, branch: branch)
|
|
106
|
+
check_shared_changelog_member(member, branch: branch, event_handler: event_handler)
|
|
92
107
|
end
|
|
93
108
|
|
|
94
|
-
def check_shared_changelog_member(member, branch: nil)
|
|
109
|
+
def check_shared_changelog_member(member, branch: nil, event_handler: nil)
|
|
95
110
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
96
111
|
command = release_state_command
|
|
112
|
+
emit_event(event_handler, member: member, branch: branch, action: "member_start", status: "running", command: command)
|
|
113
|
+
emit_event(event_handler, member: member, branch: branch, action: "changelog_command", status: "running", command: command)
|
|
97
114
|
stdout, stderr, status = Open3.capture3(shared_changelog_env(member), *command, chdir: config.root)
|
|
98
115
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
99
116
|
success = status.success?
|
|
117
|
+
emit_event(event_handler, member: member, branch: branch, action: "changelog_command", status: success ? "ok" : "failed", elapsed_seconds: elapsed, status_code: status.exitstatus)
|
|
100
118
|
state = success ? JSON.parse(stdout) : {}
|
|
101
119
|
state = branch_filtered_state(member, state, branch) if success && branch
|
|
120
|
+
emit_event(event_handler, member: member, branch: branch, action: "git_state", status: "running") if success
|
|
102
121
|
state = enrich_git_state(member.root, state, branch: branch) if success
|
|
122
|
+
emit_event(event_handler, member: member, branch: branch, action: "git_state", status: "ok") if success
|
|
123
|
+
emit_event(event_handler, member: member, branch: branch, action: "github_release", status: "running") if success
|
|
103
124
|
state = enrich_github_release(member.root, state, branch: branch) if success
|
|
125
|
+
emit_event(event_handler, member: member, branch: branch, action: "github_release", status: "ok") if success
|
|
126
|
+
emit_event(event_handler, member: member, branch: branch, action: "transfer_changelog", status: "running") if success
|
|
104
127
|
state = enrich_transfer_changelog_lag(member.root, state) if success
|
|
105
|
-
|
|
128
|
+
emit_event(event_handler, member: member, branch: branch, action: "transfer_changelog", status: "ok") if success
|
|
129
|
+
result = result(
|
|
106
130
|
member: member,
|
|
107
131
|
command: command,
|
|
108
132
|
stdout: stdout,
|
|
@@ -113,10 +137,14 @@ module Kettle
|
|
|
113
137
|
state: state,
|
|
114
138
|
branch: branch
|
|
115
139
|
)
|
|
140
|
+
emit_event(event_handler, member: member, branch: branch, action: "member_complete", status: success ? "ok" : "failed", elapsed_seconds: elapsed, status_code: status.exitstatus)
|
|
141
|
+
result
|
|
116
142
|
rescue JSON::ParserError => error
|
|
117
|
-
result(member: member, command: command || release_state_command, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}", branch: branch)
|
|
143
|
+
result = result(member: member, command: command || release_state_command, stdout: stdout, stderr: stderr, status: 1, elapsed: elapsed || 0.0, success: false, state: {}, reason: "invalid release-state JSON: #{error.message}", branch: branch)
|
|
144
|
+
emit_event(event_handler, member: member, branch: branch, action: "member_complete", status: "failed", elapsed_seconds: elapsed || 0.0, status_code: 1, reason: result.reason)
|
|
145
|
+
result
|
|
118
146
|
rescue Error => error
|
|
119
|
-
result(
|
|
147
|
+
result = result(
|
|
120
148
|
member: member,
|
|
121
149
|
command: command || release_state_command,
|
|
122
150
|
stdout: "",
|
|
@@ -128,6 +156,8 @@ module Kettle
|
|
|
128
156
|
reason: "release state check failed",
|
|
129
157
|
branch: branch
|
|
130
158
|
)
|
|
159
|
+
emit_event(event_handler, member: member, branch: branch, action: "member_complete", status: "failed", elapsed_seconds: elapsed || 0.0, status_code: 1, reason: result.reason)
|
|
160
|
+
result
|
|
131
161
|
end
|
|
132
162
|
|
|
133
163
|
def release_state_workdir(member)
|
|
@@ -626,11 +656,24 @@ module Kettle
|
|
|
626
656
|
config.release_target_branches
|
|
627
657
|
end
|
|
628
658
|
|
|
629
|
-
def member_local_branch_results(member)
|
|
659
|
+
def member_local_branch_results(member, event_handler: nil)
|
|
630
660
|
member_config = member_local_release_config(member)
|
|
631
661
|
return unless member_config
|
|
632
662
|
|
|
633
|
-
self.class.new(config: member_config, members: [member]).results
|
|
663
|
+
self.class.new(config: member_config, members: [member]).results(event_handler: event_handler)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def emit_event(event_handler, member:, branch:, action:, status:, **details)
|
|
667
|
+
return unless event_handler
|
|
668
|
+
|
|
669
|
+
event_handler.call({
|
|
670
|
+
"member" => member.name.to_s,
|
|
671
|
+
"branch" => branch,
|
|
672
|
+
"action" => action,
|
|
673
|
+
"status" => status
|
|
674
|
+
}.merge(details.each_with_object({}) { |(key, value), memo| memo[key.to_s] = value }))
|
|
675
|
+
rescue
|
|
676
|
+
nil
|
|
634
677
|
end
|
|
635
678
|
|
|
636
679
|
def member_local_release_config(member)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Kettle
|
|
8
|
+
module Family
|
|
9
|
+
class ReleaseStateEventTape
|
|
10
|
+
attr_reader :directory
|
|
11
|
+
|
|
12
|
+
def initialize(root:, stream: nil, clock: nil, wall_clock: nil)
|
|
13
|
+
@directory = File.join(root, "tmp", "kettle-family", "release-state-#{Time.now.strftime("%Y%m%d-%H%M%S")}-#{Process.pid}")
|
|
14
|
+
FileUtils.mkdir_p(@directory)
|
|
15
|
+
@stream = stream
|
|
16
|
+
@clock = clock || lambda { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
17
|
+
@wall_clock = wall_clock || lambda { Time.now.utc.iso8601 }
|
|
18
|
+
@started_at = @clock.call
|
|
19
|
+
@sequence_by_member = Hash.new(0)
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(event)
|
|
24
|
+
payload = event.merge(
|
|
25
|
+
"event_version" => 1,
|
|
26
|
+
"type" => "release_state",
|
|
27
|
+
"timestamp" => @wall_clock.call,
|
|
28
|
+
"elapsed_seconds" => @clock.call - @started_at
|
|
29
|
+
)
|
|
30
|
+
member = payload.fetch("member", "family").to_s
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
@sequence_by_member[member] += 1
|
|
33
|
+
payload["sequence"] = @sequence_by_member.fetch(member)
|
|
34
|
+
line = JSON.generate(payload)
|
|
35
|
+
File.open(path_for(member), "a") { |file| file.puts(line) }
|
|
36
|
+
@stream.puts(line) if @stream
|
|
37
|
+
@stream.flush if @stream&.respond_to?(:flush)
|
|
38
|
+
end
|
|
39
|
+
payload
|
|
40
|
+
rescue
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def path_for(member)
|
|
47
|
+
filename = member.gsub(/[^A-Za-z0-9_.-]+/, "_")
|
|
48
|
+
File.join(@directory, "#{filename}.ndjson")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/kettle/family/report.rb
CHANGED
|
@@ -30,9 +30,9 @@ module Kettle
|
|
|
30
30
|
up
|
|
31
31
|
].freeze
|
|
32
32
|
|
|
33
|
-
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
|
+
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, :event_log_dirs
|
|
34
34
|
|
|
35
|
-
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
|
+
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: [], event_log_dirs: [], elapsed_seconds: nil)
|
|
36
36
|
@family_name = family_name
|
|
37
37
|
@family_mode = family_mode
|
|
38
38
|
@order_mode = order_mode
|
|
@@ -46,6 +46,7 @@ module Kettle
|
|
|
46
46
|
@member_release_target_branches = member_release_target_branches
|
|
47
47
|
@release_mode = release_mode
|
|
48
48
|
@warnings = warnings
|
|
49
|
+
@event_log_dirs = event_log_dirs
|
|
49
50
|
@elapsed_seconds = elapsed_seconds
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -62,6 +63,7 @@ module Kettle
|
|
|
62
63
|
"member_release_target_branches" => member_release_target_branches,
|
|
63
64
|
"release_mode" => release_mode,
|
|
64
65
|
"warnings" => warnings,
|
|
66
|
+
"event_log_dirs" => event_log_dirs,
|
|
65
67
|
"command" => command,
|
|
66
68
|
"results" => results.map(&:to_h),
|
|
67
69
|
"summary" => summary,
|
|
@@ -135,6 +137,7 @@ module Kettle
|
|
|
135
137
|
lines << "summary:"
|
|
136
138
|
lines << " outcome: #{data.fetch("outcome")}"
|
|
137
139
|
lines << " elapsed: #{format_elapsed(data.fetch("elapsed_seconds"))}"
|
|
140
|
+
lines << " events: #{event_log_dirs.join(", ")}" unless event_log_dirs.empty?
|
|
138
141
|
lines << " logs: #{data.fetch("release_log_dirs").join(", ")}" unless data.fetch("release_log_dirs").empty?
|
|
139
142
|
lines << " selected: #{data.fetch("selected_count")}"
|
|
140
143
|
lines << " results: #{data.fetch("result_count")}"
|
|
@@ -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.30"
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
|
@@ -238,21 +238,78 @@ module Kettle
|
|
|
238
238
|
phase: "template_autostash_restore",
|
|
239
239
|
command: ["git", "stash", "pop", stash.fetch(:ref)]
|
|
240
240
|
)
|
|
241
|
-
next restore if restore.ok?
|
|
241
|
+
next restore if restore.ok?
|
|
242
|
+
|
|
243
|
+
if template_generated_lockfile_restore_refusal?(stash.fetch(:member), restore)
|
|
244
|
+
next runner.call(
|
|
245
|
+
member: stash.fetch(:member),
|
|
246
|
+
phase: "template_autostash_generated_lockfile_recovery",
|
|
247
|
+
command: template_generated_lockfile_restore_command(stash)
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
next restore if !template_generated_lockfile_only_conflict?(stash.fetch(:member))
|
|
242
252
|
|
|
243
253
|
runner.call(
|
|
244
254
|
member: stash.fetch(:member),
|
|
245
255
|
phase: "template_autostash_generated_lockfile_recovery",
|
|
246
256
|
command: [
|
|
247
257
|
"sh", "-lc",
|
|
248
|
-
"
|
|
249
|
-
"git
|
|
258
|
+
"set -eu; cd \"$(git rev-parse --show-toplevel)\"; " \
|
|
259
|
+
"paths=$(git diff --name-only --diff-filter=U); " \
|
|
260
|
+
"for path in $paths; do " \
|
|
261
|
+
"case \"$path\" in " \
|
|
262
|
+
"Gemfile.lock|*/Gemfile.lock|Appraisal.root.gemfile.lock|*/Appraisal.root.gemfile.lock|.structuredmerge/kettle-jem.lock|*/.structuredmerge/kettle-jem.lock) " \
|
|
263
|
+
"git restore --ours -- \"$path\"; git add -- \"$path\" ;; " \
|
|
264
|
+
"*) printf 'unresolved non-generated conflict: %s\\n' \"$path\" >&2; exit 1 ;; " \
|
|
265
|
+
"esac; " \
|
|
266
|
+
"done; " \
|
|
267
|
+
"test -z \"$(git diff --name-only --diff-filter=U)\"; " \
|
|
250
268
|
"git stash drop #{Shellwords.escape(stash.fetch(:ref))}"
|
|
251
269
|
]
|
|
252
270
|
)
|
|
253
271
|
end
|
|
254
272
|
end
|
|
255
273
|
|
|
274
|
+
def template_generated_lockfile_restore_refusal?(member, restore)
|
|
275
|
+
output = [restore.stdout, restore.stderr].compact.join("\n")
|
|
276
|
+
return false unless output.include?("would be overwritten by merge")
|
|
277
|
+
|
|
278
|
+
dirty_paths = GitStatus.dirty_paths(member.root)
|
|
279
|
+
dirty_paths.any? && dirty_paths.all? { |path| template_generated_lockfile_path?(path) }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def template_generated_lockfile_restore_command(stash)
|
|
283
|
+
member = stash.fetch(:member)
|
|
284
|
+
generated_paths = GitStatus.dirty_paths(member.root).filter_map do |status_line|
|
|
285
|
+
path = status_line.to_s.sub(/\A.../, "").strip
|
|
286
|
+
path if template_generated_lockfile_path?(status_line)
|
|
287
|
+
end.uniq
|
|
288
|
+
reset_commands = generated_paths.map do |path|
|
|
289
|
+
escaped_path = Shellwords.escape(path)
|
|
290
|
+
"if git ls-files --error-unmatch #{escaped_path} >/dev/null 2>&1; then " \
|
|
291
|
+
"git restore --source=HEAD --staged --worktree -- #{escaped_path}; " \
|
|
292
|
+
"else rm -f -- #{escaped_path}; fi"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
[
|
|
296
|
+
"sh",
|
|
297
|
+
"-lc",
|
|
298
|
+
"set -eu; cd \"$(git rev-parse --show-toplevel)\"; " \
|
|
299
|
+
"#{reset_commands.join("; ")}; git stash pop #{Shellwords.escape(stash.fetch(:ref))}"
|
|
300
|
+
]
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def template_generated_lockfile_path?(status_line)
|
|
304
|
+
path = status_line.to_s.sub(/\A.../, "").strip
|
|
305
|
+
[
|
|
306
|
+
"Gemfile.lock",
|
|
307
|
+
"Appraisal.root.gemfile.lock",
|
|
308
|
+
".structuredmerge/kettle-jem.lock"
|
|
309
|
+
].include?(path) ||
|
|
310
|
+
path.end_with?("/Gemfile.lock", "/Appraisal.root.gemfile.lock", "/.structuredmerge/kettle-jem.lock")
|
|
311
|
+
end
|
|
312
|
+
|
|
256
313
|
def rollback_failed_template_worktrees(stashes, workflow_results, runner:)
|
|
257
314
|
failed_members = workflow_results.reject(&:ok?).map(&:member_name).compact.uniq
|
|
258
315
|
stashes.filter_map do |stash|
|
|
@@ -276,7 +333,7 @@ module Kettle
|
|
|
276
333
|
def template_generated_lockfile_only_conflict?(member)
|
|
277
334
|
stdout, _stderr, status = Open3.capture3("git", "diff", "--name-only", "--diff-filter=U", chdir: member.root)
|
|
278
335
|
conflicts = stdout.lines.map(&:strip).reject(&:empty?).sort
|
|
279
|
-
status.success? && !conflicts.empty? && conflicts.all? { |path|
|
|
336
|
+
status.success? && !conflicts.empty? && conflicts.all? { |path| template_generated_lockfile_path?(" M #{path}") }
|
|
280
337
|
end
|
|
281
338
|
|
|
282
339
|
def template_branch_sync_results(branch_members, runner:)
|
|
@@ -561,12 +618,12 @@ module Kettle
|
|
|
561
618
|
def template_bootstrap_dependency_env(member)
|
|
562
619
|
env = release_lockfile_local_path_env_overrides(member).merge(workflow_env)
|
|
563
620
|
env.each_key do |key|
|
|
564
|
-
env[key] = "false" if key.end_with?("_DEV") && !
|
|
621
|
+
env[key] = "false" if key.end_with?("_DEV") && !local_path_env_requested?(key)
|
|
565
622
|
end
|
|
566
623
|
env["K_JEM_TEMPLATING"] = "false"
|
|
567
624
|
env["BUNDLE_DISABLE_CHECKSUM_VALIDATION"] = "true"
|
|
568
625
|
family_env_name = config.family_local_path_env_name
|
|
569
|
-
env[family_env_name] = "false" if family_env_name && !
|
|
626
|
+
env[family_env_name] = "false" if family_env_name && !local_path_env_requested?(family_env_name)
|
|
570
627
|
env
|
|
571
628
|
end
|
|
572
629
|
|
|
@@ -632,6 +689,7 @@ module Kettle
|
|
|
632
689
|
|
|
633
690
|
branch_members = rediscovered_selected_members(selected_names)
|
|
634
691
|
branch_members = members if branch_members.empty?
|
|
692
|
+
dirty_paths_before = branch_generated_lockfile_dirt(branch_members)
|
|
635
693
|
if command == "template" && execute
|
|
636
694
|
memo.concat(template_branch_sync_results(branch_members, runner: runner))
|
|
637
695
|
break memo unless memo.last&.ok?
|
|
@@ -639,6 +697,16 @@ module Kettle
|
|
|
639
697
|
branch_results = current_branch_results(branch_members)
|
|
640
698
|
tag_branch_results(branch_results, branch)
|
|
641
699
|
memo.concat(branch_results)
|
|
700
|
+
|
|
701
|
+
if branch_generated_lockfile_cleanup_required?
|
|
702
|
+
cleanup_branch_generated_lockfiles(
|
|
703
|
+
branch_members: branch_members,
|
|
704
|
+
dirty_paths_before: dirty_paths_before,
|
|
705
|
+
runner: runner,
|
|
706
|
+
memo: memo,
|
|
707
|
+
branch: branch
|
|
708
|
+
)
|
|
709
|
+
end
|
|
642
710
|
break memo unless memo.last&.ok?
|
|
643
711
|
|
|
644
712
|
commit_normalized_lockfiles(branch_members: branch_members, runner: runner, memo: memo)
|
|
@@ -661,6 +729,57 @@ module Kettle
|
|
|
661
729
|
end
|
|
662
730
|
end
|
|
663
731
|
|
|
732
|
+
def branch_generated_lockfile_cleanup_required?
|
|
733
|
+
execute && %w[test lint].include?(command)
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def branch_generated_lockfile_dirt(branch_members)
|
|
737
|
+
branch_members.each_with_object({}) do |member, memo|
|
|
738
|
+
root = git_root_for(member)
|
|
739
|
+
memo[root] ||= GitStatus.dirty_paths(root)
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def cleanup_branch_generated_lockfiles(branch_members:, dirty_paths_before:, runner:, memo:, branch:)
|
|
744
|
+
processed_roots = []
|
|
745
|
+
branch_members.each do |member|
|
|
746
|
+
root = git_root_for(member)
|
|
747
|
+
next if processed_roots.include?(root)
|
|
748
|
+
|
|
749
|
+
processed_roots << root
|
|
750
|
+
before = dirty_paths_before.fetch(root, []).map { |path| normalized_dirty_path(path) }
|
|
751
|
+
generated_paths = GitStatus.dirty_paths(root).filter_map do |status_line|
|
|
752
|
+
path = normalized_dirty_path(status_line)
|
|
753
|
+
path unless before.include?(path) || !template_generated_lockfile_path?(status_line)
|
|
754
|
+
end.uniq
|
|
755
|
+
next if generated_paths.empty?
|
|
756
|
+
|
|
757
|
+
result = runner.call(
|
|
758
|
+
member: member,
|
|
759
|
+
phase: "branch_generated_lockfile_recovery",
|
|
760
|
+
command: branch_generated_lockfile_restore_command(generated_paths)
|
|
761
|
+
)
|
|
762
|
+
result.branch = branch if result.respond_to?(:branch=)
|
|
763
|
+
memo << result
|
|
764
|
+
break unless result.ok?
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def normalized_dirty_path(status_line)
|
|
769
|
+
status_line.to_s.sub(/\A.../, "").strip
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def branch_generated_lockfile_restore_command(paths)
|
|
773
|
+
reset_commands = paths.map do |path|
|
|
774
|
+
escaped_path = Shellwords.escape(path)
|
|
775
|
+
"if git ls-files --error-unmatch #{escaped_path} >/dev/null 2>&1; then " \
|
|
776
|
+
"git restore --source=HEAD --staged --worktree -- #{escaped_path}; " \
|
|
777
|
+
"else rm -f -- #{escaped_path}; fi"
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
["sh", "-lc", "set -eu; #{reset_commands.join("; ")}"]
|
|
781
|
+
end
|
|
782
|
+
|
|
664
783
|
def release_preflight_results
|
|
665
784
|
phases = release_preflight_phases
|
|
666
785
|
progress = start_release_preflight_progress(phases)
|
|
@@ -1082,15 +1201,10 @@ module Kettle
|
|
|
1082
1201
|
return unless progress
|
|
1083
1202
|
|
|
1084
1203
|
member = release_progress_member_for(member_name)
|
|
1204
|
+
if progress_io && suppress_release_secret_provider_event?(event, progress: progress)
|
|
1205
|
+
return
|
|
1206
|
+
end
|
|
1085
1207
|
if progress_io
|
|
1086
|
-
if event["type"] == "secret_provider"
|
|
1087
|
-
case event["action"].to_s
|
|
1088
|
-
when "prompt_request", "manual_prompt"
|
|
1089
|
-
progress.notification(secret_provider_notification_label(event))
|
|
1090
|
-
when "prompt_response"
|
|
1091
|
-
progress.notification("")
|
|
1092
|
-
end
|
|
1093
|
-
end
|
|
1094
1208
|
if verbose || debug
|
|
1095
1209
|
emit_release_event_progress(member, event)
|
|
1096
1210
|
elsif progress.tty?
|
|
@@ -2113,7 +2227,9 @@ module Kettle
|
|
|
2113
2227
|
def workflow_family_local_path_env
|
|
2114
2228
|
return {} if implicit_single_member_template_root?
|
|
2115
2229
|
|
|
2116
|
-
config.family_local_path_env
|
|
2230
|
+
config.family_local_path_env.each_with_object({}) do |(name, default), env|
|
|
2231
|
+
env[name] = ENV.key?(name) ? ENV.fetch(name) : default
|
|
2232
|
+
end
|
|
2117
2233
|
end
|
|
2118
2234
|
|
|
2119
2235
|
def implicit_single_member_template_root?
|
|
@@ -2372,7 +2488,9 @@ module Kettle
|
|
|
2372
2488
|
next false unless event
|
|
2373
2489
|
|
|
2374
2490
|
if progress_io
|
|
2375
|
-
if
|
|
2491
|
+
if suppress_release_secret_provider_event?(event, progress: progress)
|
|
2492
|
+
next true
|
|
2493
|
+
elsif verbose || debug
|
|
2376
2494
|
emit_release_event_progress(member, event)
|
|
2377
2495
|
elsif progress&.tty?
|
|
2378
2496
|
emit_release_event_status(member, event, progress: progress)
|
|
@@ -2382,6 +2500,26 @@ module Kettle
|
|
|
2382
2500
|
end
|
|
2383
2501
|
end
|
|
2384
2502
|
|
|
2503
|
+
def suppress_release_secret_provider_event?(event, progress:)
|
|
2504
|
+
return false unless event["type"] == "secret_provider"
|
|
2505
|
+
return false unless progress
|
|
2506
|
+
|
|
2507
|
+
case event["action"].to_s
|
|
2508
|
+
when "prompt_request", "manual_prompt"
|
|
2509
|
+
progress.notification(secret_provider_notification_label(event))
|
|
2510
|
+
when "prompt_response"
|
|
2511
|
+
progress.notification("")
|
|
2512
|
+
end
|
|
2513
|
+
|
|
2514
|
+
# Prompt and provider status are transient operator notifications, not
|
|
2515
|
+
# tape events. Keeping them out of the member row prevents external
|
|
2516
|
+
# prompt output from competing with the fixed-width event tape.
|
|
2517
|
+
# A TTY always has a dedicated notification line. Prompt/provider
|
|
2518
|
+
# events must never fall through to the member-row event tape, even
|
|
2519
|
+
# when verbose or debug output is enabled.
|
|
2520
|
+
progress.tty?
|
|
2521
|
+
end
|
|
2522
|
+
|
|
2385
2523
|
def parse_template_event(line)
|
|
2386
2524
|
payload = JSON.parse(line.to_s)
|
|
2387
2525
|
(payload.is_a?(Hash) && payload["event_version"]) ? payload : nil
|
|
@@ -2822,7 +2960,16 @@ module Kettle
|
|
|
2822
2960
|
end
|
|
2823
2961
|
|
|
2824
2962
|
def template_lockfile_recovery_env(member)
|
|
2825
|
-
release_lockfile_env(member).merge(
|
|
2963
|
+
env = release_lockfile_env(member).merge(workflow_env)
|
|
2964
|
+
ENV.each do |key, value|
|
|
2965
|
+
env[key] = value if key.end_with?("_DEV", "_LOCAL") && local_path_env_requested?(key)
|
|
2966
|
+
end
|
|
2967
|
+
env.merge!(env_overrides)
|
|
2968
|
+
env.merge!(
|
|
2969
|
+
"K_JEM_TEMPLATING" => "false",
|
|
2970
|
+
"BUNDLE_DISABLE_CHECKSUM_VALIDATION" => "true"
|
|
2971
|
+
)
|
|
2972
|
+
env
|
|
2826
2973
|
end
|
|
2827
2974
|
|
|
2828
2975
|
def normalize_lockfiles_command(member:, phase:, skip_checksum_option: false)
|
|
@@ -3027,10 +3174,14 @@ module Kettle
|
|
|
3027
3174
|
def template_prepare_env
|
|
3028
3175
|
env = workflow_env
|
|
3029
3176
|
family_env_name = config.family_local_path_env_name
|
|
3030
|
-
env[family_env_name] = "false" if family_env_name && !
|
|
3177
|
+
env[family_env_name] = "false" if family_env_name && !local_path_env_requested?(family_env_name)
|
|
3031
3178
|
env
|
|
3032
3179
|
end
|
|
3033
3180
|
|
|
3181
|
+
def local_path_env_requested?(name)
|
|
3182
|
+
env_overrides.key?(name) || ENV.key?(name)
|
|
3183
|
+
end
|
|
3184
|
+
|
|
3034
3185
|
def template_thread_worker_budget
|
|
3035
3186
|
member_jobs = template_jobs(members)
|
|
3036
3187
|
[1, (Etc.nprocessors / member_jobs) - 1].max
|
|
@@ -36,7 +36,8 @@ module Kettle
|
|
|
36
36
|
@mutex = Mutex.new
|
|
37
37
|
@tty = @enabled && io.respond_to?(:tty?) && io.tty?
|
|
38
38
|
@tty_rendered = false
|
|
39
|
-
@
|
|
39
|
+
@tty_block_rows = 0
|
|
40
|
+
@tty_tape_rows = 0
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def start
|
|
@@ -122,6 +123,21 @@ module Kettle
|
|
|
122
123
|
end
|
|
123
124
|
end
|
|
124
125
|
|
|
126
|
+
# Finish a TTY progress block before the caller prints a second report.
|
|
127
|
+
# The blank line keeps the final report physically separate from the
|
|
128
|
+
# redraw block instead of making the terminal cursor its first row.
|
|
129
|
+
def finish
|
|
130
|
+
return unless @enabled
|
|
131
|
+
|
|
132
|
+
synchronize do
|
|
133
|
+
next if @stopped
|
|
134
|
+
|
|
135
|
+
render_tty_block if @tty && @started && !@line_order.empty?
|
|
136
|
+
write_line("") if @tty && @started
|
|
137
|
+
@stopped = true
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
125
141
|
def tty?
|
|
126
142
|
@tty
|
|
127
143
|
end
|
|
@@ -174,15 +190,31 @@ module Kettle
|
|
|
174
190
|
return if rows.empty?
|
|
175
191
|
|
|
176
192
|
output = +""
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
output << "\e[
|
|
193
|
+
if @tty_rendered && @tty_block_rows.positive?
|
|
194
|
+
# Restore the bottom-of-block cursor before moving to the first row.
|
|
195
|
+
# This keeps unrelated prompt output from changing the redraw anchor.
|
|
196
|
+
output << "\e[u\e[#{@tty_block_rows}A"
|
|
181
197
|
end
|
|
198
|
+
# The notification line is deliberately separate from the event tape.
|
|
199
|
+
# It is transient operator guidance, not a member row or an event.
|
|
200
|
+
output << render_tty_notification_line
|
|
201
|
+
output << render_tty_tape_rows(rows)
|
|
202
|
+
# LF moves down without necessarily returning to column 1. Reset the
|
|
203
|
+
# anchor explicitly so external prompt output starts on a fresh line.
|
|
204
|
+
output << "\e[1G\e[s"
|
|
182
205
|
@io.write(output)
|
|
183
206
|
@io.flush if @io.respond_to?(:flush)
|
|
184
207
|
@tty_rendered = true
|
|
185
|
-
@
|
|
208
|
+
@tty_tape_rows = rows.length
|
|
209
|
+
@tty_block_rows = @tty_tape_rows + 1
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def render_tty_notification_line
|
|
213
|
+
"\e[1G\e[2K#{truncate_display(@notification, render_width)}\n"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def render_tty_tape_rows(rows)
|
|
217
|
+
rows.map { |line| "\e[1G\e[2K#{line}\n" }.join
|
|
186
218
|
end
|
|
187
219
|
|
|
188
220
|
def tty_line(member_name)
|
|
@@ -278,7 +310,15 @@ module Kettle
|
|
|
278
310
|
end
|
|
279
311
|
|
|
280
312
|
def status_width
|
|
281
|
-
[
|
|
313
|
+
available = [render_width - MEMBER_WIDTH - 1 - PROGRESS_WIDTH - 1 - ELAPSED_WIDTH - 1 - EVENT_WIDTH - 1, 1].max
|
|
314
|
+
[available, MIN_STATUS_WIDTH].max.clamp(1, available)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def render_width
|
|
318
|
+
# Leave the last terminal column unused. Writing into it can trigger
|
|
319
|
+
# an automatic wrap before the explicit newline, which invalidates the
|
|
320
|
+
# fixed physical-row cursor accounting used by the redrawer.
|
|
321
|
+
[terminal_width - 1, 1].max
|
|
282
322
|
end
|
|
283
323
|
|
|
284
324
|
def terminal_width
|
data/lib/kettle/family.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "family/branch_lane_audit"
|
|
|
11
11
|
require_relative "family/config"
|
|
12
12
|
require_relative "family/secrets"
|
|
13
13
|
require_relative "family/branch_target_config"
|
|
14
|
+
require_relative "family/release_state_event_tape"
|
|
14
15
|
require_relative "family/release_state_check"
|
|
15
16
|
require_relative "family/release_reconciler"
|
|
16
17
|
require_relative "family/unreleased_gem_cleanup"
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kettle-family
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.30
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -342,6 +342,7 @@ files:
|
|
|
342
342
|
- lib/kettle/family/readiness_check.rb
|
|
343
343
|
- lib/kettle/family/release_reconciler.rb
|
|
344
344
|
- lib/kettle/family/release_state_check.rb
|
|
345
|
+
- lib/kettle/family/release_state_event_tape.rb
|
|
345
346
|
- lib/kettle/family/release_state_result.rb
|
|
346
347
|
- lib/kettle/family/release_waves.rb
|
|
347
348
|
- lib/kettle/family/report.rb
|
|
@@ -359,10 +360,10 @@ licenses:
|
|
|
359
360
|
- AGPL-3.0-only
|
|
360
361
|
metadata:
|
|
361
362
|
homepage_uri: https://kettle-family.galtzo.com
|
|
362
|
-
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.
|
|
363
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.
|
|
363
|
+
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.30
|
|
364
|
+
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.30/CHANGELOG.md
|
|
364
365
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
|
|
365
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.
|
|
366
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.30
|
|
366
367
|
funding_uri: https://github.com/sponsors/pboling
|
|
367
368
|
wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
|
|
368
369
|
news_uri: https://www.railsbling.com/tags/kettle-family
|
metadata.gz.sig
CHANGED
|
Binary file
|