howzit 2.1.47 → 2.1.48

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 477bc94918f7906d52412423ee73992f2d255279d757f36d0feed85f74c1f454
4
- data.tar.gz: 926727bfe69e059367fa75d4b55b095786663487cdc8f311b13ee92dd44d15e0
3
+ metadata.gz: 2a6dffed9d9ce31f4704b3388bbf330f87d86f054067444073ea34b8cba13a54
4
+ data.tar.gz: 87cdb843c109e8f0b88e482634d0f35b676ffcd6e2732009f5a7aa60932f7fff
5
5
  SHA512:
6
- metadata.gz: 8f4c4228bc522c7608f77d0ab4e7f4f802753e71c7f37d2f41d9f81e01764a8b0b285aaed8bc5bad6a9ccaa0544d83007bde4c7aea7c0e41bc2fc834bb8304d7
7
- data.tar.gz: a24505c7b3f22f7d4bb52eb91372f44d0ef200285faf4006bb8a93588bdb63318063d26b5571026374f8f67b648678d0bd4edf4223c5561c39ad625ec2a08a69
6
+ metadata.gz: 4e278c055f9647bb784cfac2f1fe00616dde57023a5cd08e714097ccc3f32181ad67898da2507fe23c5496a235cdee5143c29918149a0a15d1e70574e3d535d8
7
+ data.tar.gz: 409df1fe83a7e46ba317d2c9cceb8346ea94c7578376f1836b7d9bb6cb17b83b51020955470d1371a27abb46150ec6af49da1e5b15ac8108eff9b9fccb625cc9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ### 2.1.48
2
+
3
+ 2026-10-09 07:20
4
+
5
+ #### CHANGED
6
+
7
+ - **@before blocks** from a topic, its subtopics, and any included topics are shown together in one box before anything runs, with a single confirmation prompt instead of one prompt per topic
8
+ - **@after blocks** from a topic, its subtopics, and any included topics are shown together in one box at the end of the run instead of after each topic finishes
9
+
10
+ #### IMPROVED
11
+
12
+ - **Collected notes** appear in document order: the topic's own block, then included topics in @include order, then subtopics
13
+ - **@after notes from subtopics skipped after a failure** are still shown in the final box
14
+ - **Notes from included topics** render variables with the bracketed arguments passed via @include(Topic [args])
15
+ - **@before blocks** now render ${variables}, matching @after
16
+ - **Topics included more than once** only contribute their notes once
17
+
1
18
  ### 2.1.47
2
19
 
3
20
  2026-09-27 13:20
data/lib/howzit/task.rb CHANGED
@@ -6,7 +6,7 @@ module Howzit
6
6
  # Task object
7
7
  class Task
8
8
  attr_reader :type, :title, :action, :arguments, :parent, :optional, :default, :last_status, :log_level, :source_file,
9
- :include_results
9
+ :include_results, :include_args
10
10
 
11
11
  ##
12
12
  ## Initialize a Task object
data/lib/howzit/topic.rb CHANGED
@@ -165,34 +165,58 @@ module Howzit
165
165
  ## @return [Array] output lines
166
166
  ##
167
167
  def run(nested: false, as_subtopic: false)
168
- @results = { total: 0, success: 0, errors: 0, message: ''.c }
169
- output = []
170
- cols = check_cols
171
-
172
- confirm_prereqs(cols) if !@prereqs.empty? && (all_tasks.any? || sequential?)
173
-
174
- if sequential?
175
- run_sequential(output: output)
176
- elsif @tasks.any?
177
- run_tasks(output)
178
- elsif all_tasks.empty? && !as_subtopic && !directives?
179
- Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c
180
- end
168
+ # The outermost run shows @before/@after notes for the whole tree
169
+ return run_topic(nested: nested, as_subtopic: as_subtopic) if Howzit.topic_run_active
181
170
 
182
- if halted?
183
- log_skipped_tasks(@subtopics.flat_map(&:all_tasks))
184
- else
185
- run_subtopics(output)
171
+ Howzit.topic_run_active = true
172
+ begin
173
+ cols = check_cols
174
+ confirm_prereqs(cols) if all_tasks.any? || sequential?
175
+ output = run_topic(nested: nested, as_subtopic: as_subtopic)
176
+ show_postreqs(cols)
177
+ output
178
+ ensure
179
+ Howzit.topic_run_active = false
186
180
  end
181
+ end
187
182
 
188
- if @results[:total].positive? || (all_tasks.any? && !sequential?)
189
- @results[:message] += results_message
190
- output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
191
- end
183
+ ##
184
+ ## @before notes for this topic, the topics it includes, and its
185
+ ## subtopics, in document order
186
+ ##
187
+ ## @return [Array] rendered notes
188
+ ##
189
+ def all_prereqs
190
+ collect_notes(:prereqs)
191
+ end
192
192
 
193
- show_postreqs(cols)
193
+ ##
194
+ ## @after notes for this topic, the topics it includes, and its
195
+ ## subtopics, in document order
196
+ ##
197
+ ## @return [Array] rendered notes
198
+ ##
199
+ def all_postreqs
200
+ collect_notes(:postreqs)
201
+ end
194
202
 
195
- output
203
+ ##
204
+ ## Gather notes recursively through @include tasks and subtopics
205
+ ##
206
+ ## @param kind [Symbol] :prereqs or :postreqs
207
+ ## @param args [Array] Bracketed arguments from an @include
208
+ ## @param seen [Array] Titles of topics already collected
209
+ ##
210
+ def collect_notes(kind, args = nil, seen = [])
211
+ return [] if seen.include?(@title)
212
+
213
+ seen << @title
214
+ notes = render_notes(kind == :prereqs ? @prereqs : @postreqs, args)
215
+ @tasks.select { |task| task.type == :include }.each do |task|
216
+ topic = Howzit.buildnote.find_topic(task.action.sub(/ *\[.*?\] *$/, ''))[0]
217
+ notes.concat(topic.collect_notes(kind, task.include_args, seen)) if topic
218
+ end
219
+ notes + @subtopics.flat_map { |sub| sub.collect_notes(kind, nil, seen) }
196
220
  end
197
221
 
198
222
  ##
@@ -215,8 +239,11 @@ module Howzit
215
239
  end
216
240
 
217
241
  def confirm_prereqs(cols)
242
+ prereqs = all_prereqs
243
+ return if prereqs.empty?
244
+
218
245
  begin
219
- puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
246
+ puts TTY::Box.frame("{by}#{prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
220
247
  rescue Errno::EPIPE
221
248
  # Pipe closed, ignore
222
249
  end
@@ -225,11 +252,11 @@ module Howzit
225
252
  end
226
253
 
227
254
  def show_postreqs(cols)
228
- return if @postreqs.empty?
255
+ postreqs = all_postreqs
256
+ return if postreqs.empty?
229
257
 
230
- # Apply variable substitution to postreqs content, then wrap each line individually to preserve structure
231
- postreqs_content = @postreqs.join("\n\n").render_arguments
232
- wrapped_content = postreqs_content.split(/\n/).map { |line| line.wrap(cols - 4) }.join("\n")
258
+ # Wrap each line individually to preserve structure
259
+ wrapped_content = postreqs.join("\n\n").split(/\n/).map { |line| line.wrap(cols - 4) }.join("\n")
233
260
  puts TTY::Box.frame("{bw}#{wrapped_content}{x}".c, width: cols)
234
261
  rescue Errno::EPIPE
235
262
  # Pipe closed, ignore
@@ -593,6 +620,55 @@ module Howzit
593
620
 
594
621
  private
595
622
 
623
+ def run_topic(nested:, as_subtopic:)
624
+ @results = { total: 0, success: 0, errors: 0, message: ''.c }
625
+ output = []
626
+
627
+ if sequential?
628
+ run_sequential(output: output)
629
+ elsif @tasks.any?
630
+ run_tasks(output)
631
+ elsif all_tasks.empty? && !as_subtopic && !directives?
632
+ Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c
633
+ end
634
+
635
+ if halted?
636
+ log_skipped_tasks(@subtopics.flat_map(&:all_tasks))
637
+ else
638
+ run_subtopics(output)
639
+ end
640
+
641
+ if @results[:total].positive? || (all_tasks.any? && !sequential?)
642
+ @results[:message] += results_message
643
+ output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
644
+ end
645
+
646
+ output
647
+ end
648
+
649
+ ##
650
+ ## Render notes with this topic's variables, plus any bracketed
651
+ ## @include arguments bound to its title parameters
652
+ ##
653
+ def render_notes(notes, args)
654
+ return [] if notes.empty?
655
+
656
+ previous = Howzit.named_arguments
657
+ begin
658
+ Howzit.named_arguments = (previous || {}).merge(@named_args.compact).merge(bound_arguments(args))
659
+ notes.map(&:render_arguments)
660
+ ensure
661
+ Howzit.named_arguments = previous
662
+ end
663
+ end
664
+
665
+ def bound_arguments(args)
666
+ return {} if args.nil? || args.empty?
667
+
668
+ names = @arg_definitions.map { |definition| definition.split(':', 2).first }
669
+ names.zip(args).to_h.compact
670
+ end
671
+
596
672
  ##
597
673
  ## Collect all directives in the topic content
598
674
  ##
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Howzit
5
5
  # Current Howzit version.
6
- VERSION = '2.1.47'
6
+ VERSION = '2.1.48'
7
7
  end
data/lib/howzit.rb CHANGED
@@ -60,7 +60,8 @@ require 'tty/box'
60
60
  # Main module for howzit
61
61
  module Howzit
62
62
  class << self
63
- attr_accessor :arguments, :named_arguments, :cli_topic_positional_args, :cli_args, :run_log, :multi_topic_run
63
+ attr_accessor :arguments, :named_arguments, :cli_topic_positional_args, :cli_args, :run_log, :multi_topic_run,
64
+ :topic_run_active
64
65
 
65
66
  ##
66
67
  ## Holds a Configuration object with methods and a @settings hash
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+ require 'stringio'
7
+
8
+ describe 'collected @before/@after notes' do
9
+ let(:temp_dir) { File.expand_path(Dir.mktmpdir('howzit_notes_test')) }
10
+ let(:out_file) { File.join(temp_dir, 'out.txt') }
11
+ let(:child_two_command) { 'false' }
12
+ let(:buildnote) do
13
+ note = Howzit::BuildNote.new
14
+ allow(note).to receive(:finalize_output)
15
+ note
16
+ end
17
+
18
+ before do
19
+ File.write(
20
+ File.join(temp_dir, 'buildnotes.md'),
21
+ <<~NOTE
22
+ # Notes Project
23
+
24
+ ## Parent
25
+
26
+ @before
27
+ Parent before
28
+ @end
29
+
30
+ @run(echo "parent" >> "#{out_file}") Parent task
31
+
32
+ @include(Shared [prod])
33
+
34
+ @after
35
+ Parent after
36
+ @end
37
+
38
+ ### Child One
39
+
40
+ @before
41
+ Child before
42
+ @end
43
+
44
+ @run(echo "child" >> "#{out_file}") Child task
45
+
46
+ @after
47
+ Child after
48
+ @end
49
+
50
+ ### Child Two
51
+
52
+ @run(#{child_two_command}) Child two task
53
+
54
+ ### Child Three
55
+
56
+ @run(echo "three" >> "#{out_file}") Three task
57
+
58
+ @after
59
+ Three after
60
+ @end
61
+
62
+ ## Shared (target)
63
+
64
+ @run(echo "shared ${target}" >> "#{out_file}") Shared task
65
+
66
+ @after
67
+ Shared after ${target}
68
+ @end
69
+ NOTE
70
+ )
71
+
72
+ Dir.chdir(temp_dir)
73
+ Howzit.instance_variable_set(:@buildnote, buildnote)
74
+ Howzit.options[:stack] = false
75
+ Howzit.options[:include_upstream] = false
76
+ Howzit.options[:run] = true
77
+ Howzit.options[:force] = false
78
+ Howzit.options[:log_level] = 3
79
+ Howzit.cli_args = ['parent']
80
+ Howzit.run_log = []
81
+ allow(Howzit::Prompt).to receive(:yn).and_return(true)
82
+ end
83
+
84
+ after do
85
+ Howzit.options[:run] = false
86
+ Howzit.options[:log_level] = 1
87
+ Howzit.cli_args = []
88
+ Howzit.run_log = []
89
+ Dir.chdir(Dir.tmpdir)
90
+ FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
91
+ Howzit.instance_variable_set(:@buildnote, nil)
92
+ end
93
+
94
+ def run_and_capture
95
+ original = $stdout
96
+ $stdout = StringIO.new
97
+ buildnote.run
98
+ $stdout.string.gsub(/\e\[[\d;]*m/, '')
99
+ ensure
100
+ $stdout = original
101
+ end
102
+
103
+ def positions(text, notes)
104
+ notes.map { |note| text.index(note) }
105
+ end
106
+
107
+ it 'asks once for all prerequisites before running anything' do
108
+ text = run_and_capture
109
+
110
+ expect(Howzit::Prompt).to have_received(:yn).once
111
+ expect(positions(text, ['Parent before', 'Child before'])).to all(be_a(Integer))
112
+ expect(text.scan('Parent before').count).to eq 1
113
+ end
114
+
115
+ it 'shows all @after notes together in document order, including included topics' do
116
+ text = run_and_capture
117
+
118
+ notes = ['Parent after', 'Shared after prod', 'Child after', 'Three after']
119
+ found = positions(text, notes)
120
+ expect(found).to all(be_a(Integer))
121
+ expect(found).to eq(found.sort)
122
+ expect(text.scan('Child after').count).to eq 1
123
+ end
124
+
125
+ it 'shows notes from subtopics skipped after a failure' do
126
+ text = run_and_capture
127
+
128
+ expect(File.read(out_file).lines.map(&:strip)).to eq(['parent', 'shared prod', 'child'])
129
+ expect(text).to include('Three after')
130
+ end
131
+
132
+ context 'when nothing fails' do
133
+ let(:child_two_command) { 'true' }
134
+
135
+ it 'shows the combined @after box after all tasks have run' do
136
+ text = run_and_capture
137
+
138
+ expect(File.read(out_file).lines.map(&:strip)).to eq(['parent', 'shared prod', 'child', 'three'])
139
+ expect(text.scan('Parent after').count).to eq 1
140
+ end
141
+ end
142
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howzit
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.47
4
+ version: 2.1.48
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -317,6 +317,7 @@ files:
317
317
  - scripts/runtests.sh
318
318
  - spec/buildnote_spec.rb
319
319
  - spec/cli_spec.rb
320
+ - spec/collected_notes_spec.rb
320
321
  - spec/condition_evaluator_spec.rb
321
322
  - spec/conditional_blocks_integration_spec.rb
322
323
  - spec/conditional_content_spec.rb
@@ -367,6 +368,7 @@ summary: Provides a way to access Markdown project notes by topic with query cap
367
368
  test_files:
368
369
  - spec/buildnote_spec.rb
369
370
  - spec/cli_spec.rb
371
+ - spec/collected_notes_spec.rb
370
372
  - spec/condition_evaluator_spec.rb
371
373
  - spec/conditional_blocks_integration_spec.rb
372
374
  - spec/conditional_content_spec.rb