howzit 2.1.46 → 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: daae85713f05d780640619a8ada2291aaa69c0eb3f2d941f6fb9e2964c9247b4
4
- data.tar.gz: 5773440b0a9718af5dd0b3e66929bfa74023061b99a85c61d65558b76ac57d4d
3
+ metadata.gz: 2a6dffed9d9ce31f4704b3388bbf330f87d86f054067444073ea34b8cba13a54
4
+ data.tar.gz: 87cdb843c109e8f0b88e482634d0f35b676ffcd6e2732009f5a7aa60932f7fff
5
5
  SHA512:
6
- metadata.gz: 1f688d2e403997a3cf6f3b91c0ebd3746ed51b1d0bc4dfea2be5fd1297a22715d2a7b5672482b2f243dc26b38b56eee2880c9e57df98c55c16178b84eddeb8da
7
- data.tar.gz: 934e5ef9e7b71bc0323475ae4c54596d7a6813f552d63fd08dd358f51805c6950780d4435b9cf830aed7712392a5a38d52077dfbcceb69293adf02f9b0970794
6
+ metadata.gz: 4e278c055f9647bb784cfac2f1fe00616dde57023a5cd08e714097ccc3f32181ad67898da2507fe23c5496a235cdee5143c29918149a0a15d1e70574e3d535d8
7
+ data.tar.gz: 409df1fe83a7e46ba317d2c9cceb8346ea94c7578376f1836b7d9bb6cb17b83b51020955470d1371a27abb46150ec6af49da1e5b15ac8108eff9b9fccb625cc9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
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
+
18
+ ### 2.1.47
19
+
20
+ 2026-09-27 13:20
21
+
22
+ #### IMPROVED
23
+
24
+ - **Included topics** receive their bracketed arguments as positional arguments ($1, $2) in shell run blocks for the duration of the include
25
+
26
+ #### FIXED
27
+
28
+ - **default: metadata** now splits topics only on commas outside of brackets, so `default: Build, Deploy[prod, fast], Test` passes both arguments to Deploy
29
+ - **Bracketed arguments in default:** (e.g. `Deploy[prod]`) are now bound to the topic's title parameters, so `${target}` and friends render in @run commands instead of coming out empty
30
+ - **@include bracketed arguments** (e.g. `@include(Deploy [staging])`) now bind to the included topic's parameters, and including the same topic multiple times with different arguments works
31
+ - **Unspecified bracketed arguments** fall back to the topic's parameter defaults
32
+ - **Topics that use @include with arguments** no longer lose their own argument definitions
33
+
1
34
  ### 2.1.46
2
35
 
3
36
  2026-09-27 12:52
@@ -1608,7 +1608,7 @@ module Howzit
1608
1608
  else
1609
1609
  []
1610
1610
  end
1611
- output.push(process_topic(topic_match, Howzit.options[:run], single: true))
1611
+ output.push(process_topic(topic_match.with_arguments(Howzit.arguments), Howzit.options[:run], single: true))
1612
1612
  break if stop_after_failure?(topic_specs[(idx + 1)..].map(&:first))
1613
1613
  end
1614
1614
  finalize_output(output)
@@ -1640,7 +1640,8 @@ module Howzit
1640
1640
  ## @return [Array] Array of topic specification strings
1641
1641
  ##
1642
1642
  def parse_default_metadata(default_value)
1643
- default_value.strip.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
1643
+ # Commas inside [bracketed arguments] separate arguments, not topics
1644
+ default_value.strip.split(/\s*,\s*(?![^\[]*\])/).map(&:strip).reject(&:empty?)
1644
1645
  end
1645
1646
 
1646
1647
  ##
@@ -138,7 +138,7 @@ module Howzit
138
138
  if title =~ /\[(.*?)\] *$/
139
139
  args = Regexp.last_match(1).split(/ *, */).map(&:render_arguments)
140
140
  Howzit.arguments = args
141
- parent.arguments
141
+ task_data[:include_args] = args
142
142
  title.sub!(/ *\[.*?\] *$/, '')
143
143
  end
144
144
  title = title.render_arguments if title && !title.empty?
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
@@ -27,6 +27,7 @@ module Howzit
27
27
  @prefix = "{bw}\u{25B7}\u{25B7} {x}"
28
28
  # arrow = "{bw}\u{279F}{x}"
29
29
  @arguments = attributes[:arguments] || []
30
+ @include_args = attributes[:include_args]
30
31
 
31
32
  @type = attributes[:type] || :run
32
33
  @title = attributes[:title]&.to_s
@@ -171,18 +172,35 @@ module Howzit
171
172
  output = []
172
173
  action = @action
173
174
 
174
- matches = Howzit.buildnote.find_topic(action)
175
+ matches = Howzit.buildnote.find_topic(action.sub(/ *\[.*?\] *$/, ''))
175
176
  raise "Topic not found: #{action}" if matches.empty?
176
177
 
177
- topic = matches[0]
178
+ topic = matches[0].with_arguments(@include_args)
178
179
  Howzit.console.info("#{@prefix}{by}Running tasks from {bw}#{topic.title}{x}".c)
179
- output.concat(topic.run(nested: true))
180
+ output.concat(with_positional_arguments(@include_args) { topic.run(nested: true) })
180
181
  Howzit.console.info("{by}End include: #{topic.all_tasks.count} tasks{x}".c)
181
182
  @last_status = nil
182
183
  @include_results = topic.results.slice(:total, :success, :errors)
183
184
  [output, @include_results[:total], @include_results[:errors].zero?]
184
185
  end
185
186
 
187
+ ##
188
+ ## Temporarily set Howzit.arguments (e.g. $1 in included shell blocks)
189
+ ##
190
+ ## @param args [Array] Positional values, or nil to leave unchanged
191
+ ##
192
+ def with_positional_arguments(args)
193
+ return yield if args.nil? || args.empty?
194
+
195
+ previous = Howzit.arguments
196
+ Howzit.arguments = args
197
+ begin
198
+ yield
199
+ ensure
200
+ Howzit.arguments = previous
201
+ end
202
+ end
203
+
186
204
  ##
187
205
  ## Apply log level for this task
188
206
  ##
data/lib/howzit/topic.rb CHANGED
@@ -18,8 +18,11 @@ module Howzit
18
18
  ## @param metadata [Hash] Optional metadata hash
19
19
  ## @param source_file [String] Optional path to the build note file this topic came from
20
20
  ## @param level [Integer] Markdown header level (2 for ##, 3 for ###, etc.)
21
+ ## @param positional [Array] Values to bind to the title's (arguments),
22
+ ## overriding CLI arguments
21
23
  ##
22
- def initialize(title, content, metadata = nil, source_file: nil, level: 2)
24
+ def initialize(title, content, metadata = nil, source_file: nil, level: 2, positional: nil)
25
+ @raw_title = title
23
26
  @title = title
24
27
  @content = content
25
28
  @parent = nil
@@ -30,7 +33,7 @@ module Howzit
30
33
  @named_args = {}
31
34
  @metadata = metadata
32
35
  @source_file = source_file
33
- arguments(from_cli_snapshot: true)
36
+ arguments(from_cli_snapshot: positional.nil?, positional: positional)
34
37
 
35
38
  @directives = parse_directives_with_conditionals
36
39
  @tasks = gather_tasks
@@ -71,24 +74,43 @@ module Howzit
71
74
  list
72
75
  end
73
76
 
77
+ ##
78
+ ## A copy of this topic with bracketed arguments (e.g.
79
+ ## `@include(Deploy [prod])` or `default: Deploy[prod]`) bound to the
80
+ ## title's (arguments). Tasks are rebuilt so commands render with them.
81
+ ##
82
+ ## @param args [Array] Positional values
83
+ ##
84
+ ## @return [Topic] a new Topic, or self if no args given
85
+ ##
86
+ def with_arguments(args)
87
+ return self if args.nil? || args.empty?
88
+
89
+ topic = Topic.new(@raw_title, @content, @metadata, source_file: @source_file, level: @level, positional: args)
90
+ topic.parent = @parent
91
+ topic.parent_topic = @parent_topic
92
+ topic.subtopics.concat(@subtopics)
93
+ topic
94
+ end
95
+
74
96
  # Get named arguments from title
75
97
  # from_cli_snapshot: use Howzit.cli_topic_positional_args (argv after `--`) so earlier
76
- # topics' gather_tasks cannot clobber positional binding. Re-entrant @include [a,b]
77
- # calls pass false to use live Howzit.arguments.
78
- def arguments(from_cli_snapshot: false)
98
+ # topics' gather_tasks cannot clobber positional binding. positional: binds
99
+ # bracketed arguments from @include [a,b] and default: (see #with_arguments).
100
+ def arguments(from_cli_snapshot: false, positional: nil)
79
101
  @arg_definitions = []
80
102
  return unless @title =~ /\(.*?\) *$/
81
103
 
82
- positional = if from_cli_snapshot
83
- # Specs / non-CLI: leave unset to keep using Howzit.arguments
84
- if Howzit.cli_topic_positional_args.nil?
85
- Howzit.arguments || []
104
+ positional ||= if from_cli_snapshot
105
+ # Specs / non-CLI: leave unset to keep using Howzit.arguments
106
+ if Howzit.cli_topic_positional_args.nil?
107
+ Howzit.arguments || []
108
+ else
109
+ Howzit.cli_topic_positional_args
110
+ end
86
111
  else
87
- Howzit.cli_topic_positional_args
112
+ Howzit.arguments || []
88
113
  end
89
- else
90
- Howzit.arguments || []
91
- end
92
114
 
93
115
  a = @title.match(/\((?<args>.*?)\) *$/)
94
116
  args = a['args'].split(/ *, */).each(&:strip)
@@ -143,34 +165,58 @@ module Howzit
143
165
  ## @return [Array] output lines
144
166
  ##
145
167
  def run(nested: false, as_subtopic: false)
146
- @results = { total: 0, success: 0, errors: 0, message: ''.c }
147
- output = []
148
- cols = check_cols
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
149
170
 
150
- confirm_prereqs(cols) if !@prereqs.empty? && (all_tasks.any? || sequential?)
151
-
152
- if sequential?
153
- run_sequential(output: output)
154
- elsif @tasks.any?
155
- run_tasks(output)
156
- elsif all_tasks.empty? && !as_subtopic && !directives?
157
- Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c
158
- end
159
-
160
- if halted?
161
- log_skipped_tasks(@subtopics.flat_map(&:all_tasks))
162
- else
163
- 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
164
180
  end
181
+ end
165
182
 
166
- if @results[:total].positive? || (all_tasks.any? && !sequential?)
167
- @results[:message] += results_message
168
- output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
169
- 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
170
192
 
171
- 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
172
202
 
173
- 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) }
174
220
  end
175
221
 
176
222
  ##
@@ -193,8 +239,11 @@ module Howzit
193
239
  end
194
240
 
195
241
  def confirm_prereqs(cols)
242
+ prereqs = all_prereqs
243
+ return if prereqs.empty?
244
+
196
245
  begin
197
- 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)
198
247
  rescue Errno::EPIPE
199
248
  # Pipe closed, ignore
200
249
  end
@@ -203,11 +252,11 @@ module Howzit
203
252
  end
204
253
 
205
254
  def show_postreqs(cols)
206
- return if @postreqs.empty?
255
+ postreqs = all_postreqs
256
+ return if postreqs.empty?
207
257
 
208
- # Apply variable substitution to postreqs content, then wrap each line individually to preserve structure
209
- postreqs_content = @postreqs.join("\n\n").render_arguments
210
- 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")
211
260
  puts TTY::Box.frame("{bw}#{wrapped_content}{x}".c, width: cols)
212
261
  rescue Errno::EPIPE
213
262
  # Pipe closed, ignore
@@ -538,7 +587,7 @@ module Howzit
538
587
  if title =~ /\[(.*?)\] *$/
539
588
  args = Regexp.last_match(1).split(/ *, */).map(&:render_arguments)
540
589
  Howzit.arguments = args
541
- arguments
590
+ task_args[:include_args] = args
542
591
  title.sub!(/ *\[.*?\] *$/, '')
543
592
  end
544
593
  # Apply variable substitution to title after bracket processing
@@ -571,6 +620,55 @@ module Howzit
571
620
 
572
621
  private
573
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
+
574
672
  ##
575
673
  ## Collect all directives in the topic content
576
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.46'
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
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+
7
+ describe 'default: metadata' do
8
+ let(:temp_dir) { File.expand_path(Dir.mktmpdir('howzit_default_test')) }
9
+ let(:out_file) { File.join(temp_dir, 'out.txt') }
10
+ let(:default_line) { 'default: Build, Deploy[prod, fast], Test' }
11
+ let(:buildnote) do
12
+ note = Howzit::BuildNote.new
13
+ allow(note).to receive(:finalize_output)
14
+ note
15
+ end
16
+
17
+ before do
18
+ File.write(
19
+ File.join(temp_dir, 'buildnotes.md'),
20
+ <<~NOTE
21
+ #{default_line}
22
+
23
+ # Default Project
24
+
25
+ ## Build
26
+
27
+ @run(echo "build" >> "#{out_file}") Build it
28
+
29
+ ## Deploy (target, speed:slow)
30
+
31
+ @run(echo "deploy ${target} ${speed}" >> "#{out_file}") Deploy it
32
+
33
+ ## Test
34
+
35
+ @run(echo "test" >> "#{out_file}") Test it
36
+
37
+ ## Broken
38
+
39
+ @run(false) Fail
40
+
41
+ ## Release
42
+
43
+ @include(Deploy [staging])
44
+ @include(Deploy [prod, fast])
45
+ NOTE
46
+ )
47
+
48
+ Dir.chdir(temp_dir)
49
+ Howzit.instance_variable_set(:@buildnote, buildnote)
50
+ Howzit.options[:stack] = false
51
+ Howzit.options[:include_upstream] = false
52
+ Howzit.options[:run] = true
53
+ Howzit.options[:force] = false
54
+ Howzit.options[:log_level] = 3
55
+ Howzit.cli_args = []
56
+ Howzit.run_log = []
57
+ end
58
+
59
+ after do
60
+ Howzit.options[:run] = false
61
+ Howzit.options[:log_level] = 1
62
+ Howzit.cli_args = []
63
+ Howzit.run_log = []
64
+ Dir.chdir(Dir.tmpdir)
65
+ FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
66
+ Howzit.instance_variable_set(:@buildnote, nil)
67
+ end
68
+
69
+ def output_lines
70
+ File.read(out_file).lines.map(&:strip)
71
+ end
72
+
73
+ describe 'parsing' do
74
+ it 'splits topics on commas outside of brackets' do
75
+ expect(buildnote.send(:parse_default_metadata, 'Build, Deploy[prod, fast],Test')).to eq(
76
+ ['Build', 'Deploy[prod, fast]', 'Test']
77
+ )
78
+ end
79
+
80
+ it 'separates bracketed arguments from the topic name' do
81
+ expect(buildnote.send(:parse_topic_with_args, 'Deploy[prod, fast]')).to eq(['Deploy', 'prod, fast'])
82
+ expect(buildnote.send(:parse_topic_with_args, 'Build')).to eq(['Build', nil])
83
+ end
84
+ end
85
+
86
+ describe 'running' do
87
+ it 'runs the default topics in order with howzit -r' do
88
+ buildnote.run
89
+
90
+ expect(output_lines).to eq(['build', 'deploy prod fast', 'test'])
91
+ end
92
+
93
+ it 'runs the default topics with howzit -r default' do
94
+ Howzit.cli_args = ['default']
95
+ buildnote.run
96
+
97
+ expect(output_lines).to eq(['build', 'deploy prod fast', 'test'])
98
+ end
99
+
100
+ context 'with fewer arguments than the topic accepts' do
101
+ let(:default_line) { 'default: Deploy[prod]' }
102
+
103
+ it 'uses the topic defaults for the rest' do
104
+ buildnote.run
105
+
106
+ expect(output_lines).to eq(['deploy prod slow'])
107
+ end
108
+ end
109
+
110
+ context 'with a missing topic' do
111
+ let(:default_line) { 'default: Build, Nonexistent, Test' }
112
+
113
+ it 'runs the topics that match' do
114
+ buildnote.run
115
+
116
+ expect(output_lines).to eq(%w[build test])
117
+ end
118
+ end
119
+
120
+ context 'with a failing topic' do
121
+ let(:default_line) { 'default: Build, Broken, Test' }
122
+
123
+ it 'stops after the failure and skips the remaining topics' do
124
+ buildnote.run
125
+
126
+ expect(output_lines).to eq(['build'])
127
+ expect(Howzit.run_log.map { |e| [e[:task], e[:skipped] ? :skipped : e[:success]] }).to eq(
128
+ [['Build it', true], ['Fail', false], ['Test it', :skipped]]
129
+ )
130
+ end
131
+ end
132
+ end
133
+
134
+ describe '@include with bracketed arguments' do
135
+ it 'binds the arguments to the included topic for each include' do
136
+ Howzit.cli_args = ['release']
137
+ buildnote.run
138
+
139
+ expect(output_lines).to eq(['deploy staging slow', 'deploy prod fast'])
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.46
4
+ version: 2.1.48
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -317,9 +317,11 @@ 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
324
+ - spec/default_metadata_spec.rb
323
325
  - spec/includes_metadata_spec.rb
324
326
  - spec/log_level_spec.rb
325
327
  - spec/prompt_spec.rb
@@ -366,9 +368,11 @@ summary: Provides a way to access Markdown project notes by topic with query cap
366
368
  test_files:
367
369
  - spec/buildnote_spec.rb
368
370
  - spec/cli_spec.rb
371
+ - spec/collected_notes_spec.rb
369
372
  - spec/condition_evaluator_spec.rb
370
373
  - spec/conditional_blocks_integration_spec.rb
371
374
  - spec/conditional_content_spec.rb
375
+ - spec/default_metadata_spec.rb
372
376
  - spec/includes_metadata_spec.rb
373
377
  - spec/log_level_spec.rb
374
378
  - spec/prompt_spec.rb