howzit 2.1.46 → 2.1.47

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: 477bc94918f7906d52412423ee73992f2d255279d757f36d0feed85f74c1f454
4
+ data.tar.gz: 926727bfe69e059367fa75d4b55b095786663487cdc8f311b13ee92dd44d15e0
5
5
  SHA512:
6
- metadata.gz: 1f688d2e403997a3cf6f3b91c0ebd3746ed51b1d0bc4dfea2be5fd1297a22715d2a7b5672482b2f243dc26b38b56eee2880c9e57df98c55c16178b84eddeb8da
7
- data.tar.gz: 934e5ef9e7b71bc0323475ae4c54596d7a6813f552d63fd08dd358f51805c6950780d4435b9cf830aed7712392a5a38d52077dfbcceb69293adf02f9b0970794
6
+ metadata.gz: 8f4c4228bc522c7608f77d0ab4e7f4f802753e71c7f37d2f41d9f81e01764a8b0b285aaed8bc5bad6a9ccaa0544d83007bde4c7aea7c0e41bc2fc834bb8304d7
7
+ data.tar.gz: a24505c7b3f22f7d4bb52eb91372f44d0ef200285faf4006bb8a93588bdb63318063d26b5571026374f8f67b648678d0bd4edf4223c5561c39ad625ec2a08a69
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ### 2.1.47
2
+
3
+ 2026-09-27 13:20
4
+
5
+ #### IMPROVED
6
+
7
+ - **Included topics** receive their bracketed arguments as positional arguments ($1, $2) in shell run blocks for the duration of the include
8
+
9
+ #### FIXED
10
+
11
+ - **default: metadata** now splits topics only on commas outside of brackets, so `default: Build, Deploy[prod, fast], Test` passes both arguments to Deploy
12
+ - **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
13
+ - **@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
14
+ - **Unspecified bracketed arguments** fall back to the topic's parameter defaults
15
+ - **Topics that use @include with arguments** no longer lose their own argument definitions
16
+
1
17
  ### 2.1.46
2
18
 
3
19
  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
@@ -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)
@@ -538,7 +560,7 @@ module Howzit
538
560
  if title =~ /\[(.*?)\] *$/
539
561
  args = Regexp.last_match(1).split(/ *, */).map(&:render_arguments)
540
562
  Howzit.arguments = args
541
- arguments
563
+ task_args[:include_args] = args
542
564
  title.sub!(/ *\[.*?\] *$/, '')
543
565
  end
544
566
  # Apply variable substitution to title after bracket processing
@@ -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.47'
7
7
  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.47
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -320,6 +320,7 @@ files:
320
320
  - spec/condition_evaluator_spec.rb
321
321
  - spec/conditional_blocks_integration_spec.rb
322
322
  - spec/conditional_content_spec.rb
323
+ - spec/default_metadata_spec.rb
323
324
  - spec/includes_metadata_spec.rb
324
325
  - spec/log_level_spec.rb
325
326
  - spec/prompt_spec.rb
@@ -369,6 +370,7 @@ test_files:
369
370
  - spec/condition_evaluator_spec.rb
370
371
  - spec/conditional_blocks_integration_spec.rb
371
372
  - spec/conditional_content_spec.rb
373
+ - spec/default_metadata_spec.rb
372
374
  - spec/includes_metadata_spec.rb
373
375
  - spec/log_level_spec.rb
374
376
  - spec/prompt_spec.rb