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 +4 -4
- data/CHANGELOG.md +33 -0
- data/lib/howzit/buildnote.rb +3 -2
- data/lib/howzit/directive.rb +1 -1
- data/lib/howzit/task.rb +22 -4
- data/lib/howzit/topic.rb +140 -42
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +2 -1
- data/spec/collected_notes_spec.rb +142 -0
- data/spec/default_metadata_spec.rb +142 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a6dffed9d9ce31f4704b3388bbf330f87d86f054067444073ea34b8cba13a54
|
|
4
|
+
data.tar.gz: 87cdb843c109e8f0b88e482634d0f35b676ffcd6e2732009f5a7aa60932f7fff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/howzit/buildnote.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
##
|
data/lib/howzit/directive.rb
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
77
|
-
#
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}#{
|
|
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
|
-
|
|
255
|
+
postreqs = all_postreqs
|
|
256
|
+
return if postreqs.empty?
|
|
207
257
|
|
|
208
|
-
#
|
|
209
|
-
|
|
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
|
-
|
|
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
|
##
|
data/lib/howzit/version.rb
CHANGED
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.
|
|
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
|