howzit 2.1.45 → 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 +4 -4
- data/CHANGELOG.md +41 -0
- data/lib/howzit/buildnote.rb +3 -2
- data/lib/howzit/config.rb +1 -0
- data/lib/howzit/directive.rb +1 -1
- data/lib/howzit/script_support.rb +29 -0
- data/lib/howzit/stringutils.rb +66 -13
- data/lib/howzit/task.rb +50 -9
- data/lib/howzit/topic.rb +36 -14
- data/lib/howzit/version.rb +1 -1
- data/spec/default_metadata_spec.rb +142 -0
- data/spec/shell_variables_spec.rb +177 -0
- metadata +5 -2
- data/nested-topics-for-howzit.md +0 -100
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 477bc94918f7906d52412423ee73992f2d255279d757f36d0feed85f74c1f454
|
|
4
|
+
data.tar.gz: 926727bfe69e059367fa75d4b55b095786663487cdc8f311b13ee92dd44d15e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f4c4228bc522c7608f77d0ab4e7f4f802753e71c7f37d2f41d9f81e01764a8b0b285aaed8bc5bad6a9ccaa0544d83007bde4c7aea7c0e41bc2fc834bb8304d7
|
|
7
|
+
data.tar.gz: a24505c7b3f22f7d4bb52eb91372f44d0ef200285faf4006bb8a93588bdb63318063d26b5571026374f8f67b648678d0bd4edf4223c5561c39ad625ec2a08a69
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
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
|
+
|
|
17
|
+
### 2.1.46
|
|
18
|
+
|
|
19
|
+
2026-09-27 12:52
|
|
20
|
+
|
|
21
|
+
#### CHANGED
|
|
22
|
+
|
|
23
|
+
- **Shell run blocks** (sh, bash, zsh, dash, ksh, or no hashbang) now receive Howzit variables as environment variables instead of having ${VAR} replaced in the script text, so scripts can use their own variables, parameter expansions, and ENV without Howzit clobbering them.
|
|
24
|
+
- **Command-line arguments** after -- are passed to shell run blocks as script arguments, so $1, $#, and "$@" work like any shell script (including inside shell functions).
|
|
25
|
+
|
|
26
|
+
#### NEW
|
|
27
|
+
|
|
28
|
+
- **:shell_variables: config option** (env or substitute) to switch shell run blocks back to text substitution; defaults to env.
|
|
29
|
+
- **$${VAR} escape** passes ${VAR} through to the shell untouched in @run, @copy, and text-substituted blocks.
|
|
30
|
+
|
|
31
|
+
#### IMPROVED
|
|
32
|
+
|
|
33
|
+
- **Howzit-style defaults** (${name:default}) in shell run blocks are converted to the shell's ${name:-default} so they keep working with environment variables.
|
|
34
|
+
|
|
35
|
+
#### FIXED
|
|
36
|
+
|
|
37
|
+
- **Shell parameter expansions** like ${VAR:-default}, ${VAR:=x}, ${VAR:+x}, ${VAR:?x}, and ${VAR:0:3} are no longer mangled into Howzit defaults when VAR isn't a Howzit variable.
|
|
38
|
+
- **Positional placeholders** ($1, ${2}) without a matching argument are left intact instead of being replaced with an empty string, which broke shell functions and awk '{print $1}'.
|
|
39
|
+
- **$@ and $*** are only replaced when arguments were passed after --, instead of always being replaced with an empty string.
|
|
40
|
+
- **Default values containing colons** (e.g. ${url:http://localhost:3000}) are no longer cut off at the second colon.
|
|
41
|
+
|
|
1
42
|
### 2.1.45
|
|
2
43
|
|
|
3
44
|
2026-09-27 12:16
|
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/config.rb
CHANGED
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?
|
|
@@ -153,6 +153,35 @@ module Howzit
|
|
|
153
153
|
end
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
##
|
|
157
|
+
## Whether a run block is a POSIX-style shell script (sh, bash,
|
|
158
|
+
## zsh, dash, ksh). Blocks without a hashbang run with sh.
|
|
159
|
+
##
|
|
160
|
+
## @param script_content [String] The script content
|
|
161
|
+
##
|
|
162
|
+
## @return [Boolean]
|
|
163
|
+
##
|
|
164
|
+
def shell_script?(script_content)
|
|
165
|
+
first_line = script_content.to_s.lines.first&.strip
|
|
166
|
+
return true unless first_line&.start_with?('#!')
|
|
167
|
+
|
|
168
|
+
first_line.match?(%r{\A#!\s*(?:/usr/bin/env\s+)?(?:\S*/)?(?:sh|bash|zsh|dash|ksh)(?:\s|\z)})
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
##
|
|
172
|
+
## Howzit variables as environment variables for shell run blocks
|
|
173
|
+
##
|
|
174
|
+
## @return [Hash] name => string value
|
|
175
|
+
##
|
|
176
|
+
def variables_env
|
|
177
|
+
(Howzit.named_arguments || {}).each_with_object({}) do |(key, value), env|
|
|
178
|
+
name = key.to_s
|
|
179
|
+
next if value.nil? || !name.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
180
|
+
|
|
181
|
+
env[name] = value.to_s
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
156
185
|
##
|
|
157
186
|
## Get the injection line for a given interpreter
|
|
158
187
|
##
|
data/lib/howzit/stringutils.rb
CHANGED
|
@@ -383,29 +383,64 @@ module Howzit
|
|
|
383
383
|
str = dup
|
|
384
384
|
str.render_named_placeholders
|
|
385
385
|
str.render_numeric_placeholders
|
|
386
|
-
|
|
386
|
+
args = Howzit.arguments
|
|
387
|
+
return str if args.nil? || args.empty?
|
|
388
|
+
|
|
389
|
+
str.gsub(/(?<!\$)\$[@*]/, Shellwords.join(args))
|
|
387
390
|
end
|
|
388
391
|
|
|
392
|
+
##
|
|
393
|
+
## Replace ${NAME} and ${NAME:default} with Howzit variable
|
|
394
|
+
## values. Names Howzit doesn't know, shell parameter
|
|
395
|
+
## expansions (${VAR:-x}, ${VAR:=x}, ${VAR:0:3}, etc.), and
|
|
396
|
+
## escaped $${NAME} placeholders are left for the shell.
|
|
397
|
+
##
|
|
389
398
|
def render_named_placeholders
|
|
390
|
-
gsub!(
|
|
399
|
+
gsub!(/(?<!\$)\$\{(?<name>[A-Z0-9_]+)(?<rest>:[^}]*)?\}/i) do
|
|
391
400
|
m = Regexp.last_match
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
401
|
+
original = m[0]
|
|
402
|
+
rest = m['rest'].to_s
|
|
403
|
+
value = placeholder_value(m['name'])
|
|
404
|
+
|
|
405
|
+
if rest.empty?
|
|
406
|
+
value.nil? ? original : value
|
|
407
|
+
elsif rest.start_with?(':-')
|
|
408
|
+
value.nil? || value.empty? || rest.include?('$') ? original : value
|
|
409
|
+
elsif rest.match?(/\A:[=+?\s\d]/)
|
|
410
|
+
original
|
|
397
411
|
else
|
|
398
|
-
|
|
399
|
-
m[0]
|
|
412
|
+
value.nil? ? rest[1..].strip : value
|
|
400
413
|
end
|
|
401
414
|
end
|
|
402
415
|
end
|
|
403
416
|
|
|
404
417
|
def render_numeric_placeholders
|
|
405
|
-
gsub!(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
418
|
+
gsub!(/(?<!\$)\$(?:\{(\d+)\}|(\d+))/) do
|
|
419
|
+
m = Regexp.last_match
|
|
420
|
+
placeholder_value(m[1] || m[2]) || m[0]
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
##
|
|
425
|
+
## Remove the escape from $${NAME} placeholders, leaving
|
|
426
|
+
## ${NAME} for the shell. Applied right before execution.
|
|
427
|
+
##
|
|
428
|
+
## @return [String] unescaped string
|
|
429
|
+
##
|
|
430
|
+
def unescape_placeholders
|
|
431
|
+
gsub(/\$\$(?=\{)/, '$')
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
##
|
|
435
|
+
## Convert Howzit-style ${NAME:default} to the shell's
|
|
436
|
+
## ${NAME:-default} so defaults work when variables are
|
|
437
|
+
## passed to a shell script through the environment
|
|
438
|
+
##
|
|
439
|
+
## @return [String] converted string
|
|
440
|
+
##
|
|
441
|
+
def shellify_defaults
|
|
442
|
+
gsub(/(?<!\$)\$\{([A-Za-z_][A-Za-z0-9_]*|\d+):(?![-=+?\s\d$(])([^}]*)\}/) do
|
|
443
|
+
"${#{Regexp.last_match(1)}:-#{Regexp.last_match(2)}}"
|
|
409
444
|
end
|
|
410
445
|
end
|
|
411
446
|
|
|
@@ -560,6 +595,24 @@ module Howzit
|
|
|
560
595
|
Color.template("\n\n#{title}#{tail}{x}\n\n")
|
|
561
596
|
end
|
|
562
597
|
end
|
|
598
|
+
|
|
599
|
+
private
|
|
600
|
+
|
|
601
|
+
##
|
|
602
|
+
## Look up a placeholder name: digits are positional
|
|
603
|
+
## arguments, anything else is a named variable
|
|
604
|
+
##
|
|
605
|
+
## @return [String, nil] value, or nil if not defined
|
|
606
|
+
##
|
|
607
|
+
def placeholder_value(name)
|
|
608
|
+
if name.match?(/\A\d+\z/)
|
|
609
|
+
idx = name.to_i - 1
|
|
610
|
+
args = Howzit.arguments || []
|
|
611
|
+
return idx >= 0 && idx < args.length ? args[idx].to_s : nil
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
Howzit.named_arguments&.[](name)&.to_s
|
|
615
|
+
end
|
|
563
616
|
end
|
|
564
617
|
end
|
|
565
618
|
|
data/lib/howzit/task.rb
CHANGED
|
@@ -27,12 +27,14 @@ 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
|
|
33
34
|
@parent = attributes[:parent] || nil
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
action = attributes[:action]
|
|
37
|
+
@action = shell_env_block?(action) ? action : action.render_arguments
|
|
36
38
|
@log_level = attributes[:log_level]
|
|
37
39
|
# Get source_file from parent topic if available, or from attributes
|
|
38
40
|
parent_obj = attributes[:parent]
|
|
@@ -61,15 +63,37 @@ module Howzit
|
|
|
61
63
|
@title
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
##
|
|
67
|
+
## Whether this is a shell run block that receives Howzit
|
|
68
|
+
## variables through the environment instead of text substitution
|
|
69
|
+
##
|
|
70
|
+
## @param content [String] The block content
|
|
71
|
+
##
|
|
72
|
+
def shell_env_block?(content = @action)
|
|
73
|
+
@type == :block &&
|
|
74
|
+
Howzit.options[:shell_variables].to_s != 'substitute' &&
|
|
75
|
+
ScriptSupport.shell_script?(content)
|
|
76
|
+
end
|
|
77
|
+
|
|
64
78
|
##
|
|
65
79
|
## Execute a block type
|
|
66
80
|
##
|
|
67
81
|
def run_block
|
|
68
82
|
Howzit.console.info "#{@prefix}{bg}Running block {bw}#{@title}{x}".c if Howzit.options[:log_level] < 2
|
|
69
83
|
block = @action
|
|
84
|
+
env_mode = shell_env_block?(block)
|
|
70
85
|
# Apply variable substitution to block content at execution time
|
|
71
86
|
# (variables from previous run blocks are now available)
|
|
72
|
-
|
|
87
|
+
if env_mode
|
|
88
|
+
block = block.shellify_defaults
|
|
89
|
+
env = ScriptSupport.variables_env
|
|
90
|
+
script_args = Howzit.arguments || []
|
|
91
|
+
else
|
|
92
|
+
block = block.render_arguments if block && !block.empty?
|
|
93
|
+
env = {}
|
|
94
|
+
script_args = []
|
|
95
|
+
end
|
|
96
|
+
block = block.unescape_placeholders
|
|
73
97
|
script = Tempfile.new('howzit_script')
|
|
74
98
|
comm_file = ScriptComm.setup
|
|
75
99
|
old_log_level = apply_log_level
|
|
@@ -117,9 +141,9 @@ module Howzit
|
|
|
117
141
|
cmd = ScriptSupport.execution_command_for(script.path, interpreter)
|
|
118
142
|
# If interpreter is nil, execute directly (will respect hashbang)
|
|
119
143
|
res = if interpreter.nil?
|
|
120
|
-
system(script.path)
|
|
144
|
+
system(env, script.path, *script_args)
|
|
121
145
|
else
|
|
122
|
-
system(cmd)
|
|
146
|
+
system(env, [cmd, *script_args.map { |arg| Shellwords.escape(arg) }].join(' '))
|
|
123
147
|
end
|
|
124
148
|
ensure
|
|
125
149
|
# Restore original directory
|
|
@@ -148,18 +172,35 @@ module Howzit
|
|
|
148
172
|
output = []
|
|
149
173
|
action = @action
|
|
150
174
|
|
|
151
|
-
matches = Howzit.buildnote.find_topic(action)
|
|
175
|
+
matches = Howzit.buildnote.find_topic(action.sub(/ *\[.*?\] *$/, ''))
|
|
152
176
|
raise "Topic not found: #{action}" if matches.empty?
|
|
153
177
|
|
|
154
|
-
topic = matches[0]
|
|
178
|
+
topic = matches[0].with_arguments(@include_args)
|
|
155
179
|
Howzit.console.info("#{@prefix}{by}Running tasks from {bw}#{topic.title}{x}".c)
|
|
156
|
-
output.concat(topic.run(nested: true))
|
|
180
|
+
output.concat(with_positional_arguments(@include_args) { topic.run(nested: true) })
|
|
157
181
|
Howzit.console.info("{by}End include: #{topic.all_tasks.count} tasks{x}".c)
|
|
158
182
|
@last_status = nil
|
|
159
183
|
@include_results = topic.results.slice(:total, :success, :errors)
|
|
160
184
|
[output, @include_results[:total], @include_results[:errors].zero?]
|
|
161
185
|
end
|
|
162
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
|
+
|
|
163
204
|
##
|
|
164
205
|
## Apply log level for this task
|
|
165
206
|
##
|
|
@@ -241,7 +282,7 @@ module Howzit
|
|
|
241
282
|
Dir.chdir(expanded_exec_dir) if expanded_exec_dir != expanded_original
|
|
242
283
|
end
|
|
243
284
|
|
|
244
|
-
res = system(@action)
|
|
285
|
+
res = system(@action.unescape_placeholders)
|
|
245
286
|
ensure
|
|
246
287
|
# Restore original directory
|
|
247
288
|
if exec_dir && Dir.exist?(exec_dir)
|
|
@@ -274,7 +315,7 @@ module Howzit
|
|
|
274
315
|
@title && !@title.empty? ? @title : @action
|
|
275
316
|
end
|
|
276
317
|
Howzit.console.info("#{@prefix}{bg}Copied {bw}#{display_title}{bg} to clipboard{x}".c)
|
|
277
|
-
Util.os_copy(@action)
|
|
318
|
+
Util.os_copy(@action.unescape_placeholders)
|
|
278
319
|
@last_status = 0
|
|
279
320
|
true
|
|
280
321
|
end
|
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)
|
|
@@ -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
|
-
|
|
563
|
+
task_args[:include_args] = args
|
|
542
564
|
title.sub!(/ *\[.*?\] *$/, '')
|
|
543
565
|
end
|
|
544
566
|
# Apply variable substitution to title after bracket processing
|
data/lib/howzit/version.rb
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
describe 'shell variable handling' do
|
|
8
|
+
before do
|
|
9
|
+
Howzit.arguments = []
|
|
10
|
+
Howzit.named_arguments = { 'file' => 'README.md', 'version' => '1.2.3' }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
after do
|
|
14
|
+
Howzit.arguments = []
|
|
15
|
+
Howzit.named_arguments = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe 'String#render_arguments' do
|
|
19
|
+
it 'substitutes Howzit variables' do
|
|
20
|
+
expect('v${version}'.render_arguments).to eq('v1.2.3')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'leaves unknown variables for the shell' do
|
|
24
|
+
expect('echo "${HOME}"'.render_arguments).to eq('echo "${HOME}"')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'uses Howzit-style defaults for undefined variables' do
|
|
28
|
+
expect('${missing:fallback}'.render_arguments).to eq('fallback')
|
|
29
|
+
expect('${url:http://localhost:3000}'.render_arguments).to eq('http://localhost:3000')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'leaves shell parameter expansions alone' do
|
|
33
|
+
%w[${OUT:-/tmp} ${OUT:=/tmp} ${OUT:+set} ${OUT:?missing} ${NAME:0:3} ${#NAME} ${NAME/a/b}].each do |expansion|
|
|
34
|
+
expect(expansion.render_arguments).to eq(expansion)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'substitutes a defined Howzit variable in ${VAR:-default}' do
|
|
39
|
+
expect('${version:-0.0.0}'.render_arguments).to eq('1.2.3')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'leaves positional placeholders without a matching argument' do
|
|
43
|
+
expect('f() { echo "$1 ${2}"; }'.render_arguments).to eq('f() { echo "$1 ${2}"; }')
|
|
44
|
+
expect("awk '{print $1}'".render_arguments).to eq("awk '{print $1}'")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'leaves $@ and $* alone when no arguments were passed' do
|
|
48
|
+
expect('run() { "$@"; }'.render_arguments).to eq('run() { "$@"; }')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'substitutes positional arguments when passed' do
|
|
52
|
+
Howzit.arguments = ['one', 'two words']
|
|
53
|
+
expect('$1 ${2} ${3:three}'.render_arguments).to eq('one two words three')
|
|
54
|
+
expect('cmd $@'.render_arguments).to eq("cmd one two\\ words")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'leaves escaped $${VAR} placeholders until execution' do
|
|
58
|
+
rendered = 'echo $${file}'.render_arguments
|
|
59
|
+
expect(rendered).to eq('echo $${file}')
|
|
60
|
+
expect(rendered.unescape_placeholders).to eq('echo ${file}')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe 'String#shellify_defaults' do
|
|
65
|
+
it 'converts Howzit defaults to shell defaults' do
|
|
66
|
+
expect('${name:world} ${1:first}'.shellify_defaults).to eq('${name:-world} ${1:-first}')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'leaves shell expansions alone' do
|
|
70
|
+
%w[${name} ${name:-x} ${name:0:3} ${name:$offset} ${name: -2}].each do |expansion|
|
|
71
|
+
expect(expansion.shellify_defaults).to eq(expansion)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe 'ScriptSupport.shell_script?' do
|
|
77
|
+
it 'detects shell scripts' do
|
|
78
|
+
expect(Howzit::ScriptSupport.shell_script?("echo hi\n")).to be true
|
|
79
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/bin/bash\necho hi\n")).to be true
|
|
80
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/usr/bin/env zsh\n")).to be true
|
|
81
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/bin/sh -e\n")).to be true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'excludes other interpreters' do
|
|
85
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/usr/bin/env ruby\n")).to be false
|
|
86
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/usr/bin/env fish\n")).to be false
|
|
87
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/usr/bin/env python3\n")).to be false
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe 'running blocks' do
|
|
92
|
+
let(:temp_dir) { File.expand_path(Dir.mktmpdir('howzit_shell_vars_test')) }
|
|
93
|
+
let(:out_file) { File.join(temp_dir, 'out.txt') }
|
|
94
|
+
|
|
95
|
+
before do
|
|
96
|
+
File.write(
|
|
97
|
+
File.join(temp_dir, 'buildnotes.md'),
|
|
98
|
+
<<~NOTE
|
|
99
|
+
# Shell Variables
|
|
100
|
+
|
|
101
|
+
## Shell Block (target:prod)
|
|
102
|
+
|
|
103
|
+
```run
|
|
104
|
+
#!/bin/bash
|
|
105
|
+
greet() { echo "arg=$1"; }
|
|
106
|
+
for file in a.rb; do echo "loop=${file}"; done > "#{out_file}"
|
|
107
|
+
echo "howzit=${target}" >> "#{out_file}"
|
|
108
|
+
echo "default=${undefined_var:fallback}" >> "#{out_file}"
|
|
109
|
+
echo "shell_default=${UNSET_THING:-shellval}" >> "#{out_file}"
|
|
110
|
+
echo "env=${HOWZIT_SPEC_ENV}" >> "#{out_file}"
|
|
111
|
+
greet hello >> "#{out_file}"
|
|
112
|
+
echo "script_arg=${1:-none}" >> "#{out_file}"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Escaped Run
|
|
116
|
+
|
|
117
|
+
@run(echo "$${HOWZIT_SPEC_ENV}" > "#{out_file}") Escaped
|
|
118
|
+
NOTE
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
ENV['HOWZIT_SPEC_ENV'] = 'from_env'
|
|
122
|
+
Dir.chdir(temp_dir)
|
|
123
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
124
|
+
Howzit.options[:stack] = false
|
|
125
|
+
Howzit.options[:include_upstream] = false
|
|
126
|
+
Howzit.options[:log_level] = 3
|
|
127
|
+
Howzit.named_arguments = { 'file' => 'README.md' }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
after do
|
|
131
|
+
ENV.delete('HOWZIT_SPEC_ENV')
|
|
132
|
+
Howzit.options[:shell_variables] = 'env'
|
|
133
|
+
Howzit.options[:log_level] = 1
|
|
134
|
+
Dir.chdir(Dir.tmpdir)
|
|
135
|
+
FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
|
|
136
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def output_lines
|
|
140
|
+
File.read(out_file).lines.map(&:strip)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'passes Howzit variables to shell blocks through the environment' do
|
|
144
|
+
Howzit::BuildNote.new.find_topic('Shell Block')[0].run
|
|
145
|
+
|
|
146
|
+
expect(output_lines).to eq([
|
|
147
|
+
'loop=a.rb',
|
|
148
|
+
'howzit=prod',
|
|
149
|
+
'default=fallback',
|
|
150
|
+
'shell_default=shellval',
|
|
151
|
+
'env=from_env',
|
|
152
|
+
'arg=hello',
|
|
153
|
+
'script_arg=none'
|
|
154
|
+
])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'passes positional arguments to the script' do
|
|
158
|
+
Howzit.arguments = ['cli_arg']
|
|
159
|
+
Howzit::BuildNote.new.find_topic('Shell Block')[0].run
|
|
160
|
+
|
|
161
|
+
expect(output_lines).to include('arg=hello', 'script_arg=cli_arg')
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'uses text substitution when shell_variables is substitute' do
|
|
165
|
+
Howzit.options[:shell_variables] = 'substitute'
|
|
166
|
+
Howzit::BuildNote.new.find_topic('Shell Block')[0].run
|
|
167
|
+
|
|
168
|
+
expect(output_lines).to include('loop=README.md', 'howzit=prod')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'passes escaped placeholders to the shell in @run commands' do
|
|
172
|
+
Howzit::BuildNote.new.find_topic('Escaped Run')[0].run
|
|
173
|
+
|
|
174
|
+
expect(output_lines).to eq(['from_env'])
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
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.47
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -314,13 +314,13 @@ files:
|
|
|
314
314
|
- lib/howzit/topic.rb
|
|
315
315
|
- lib/howzit/util.rb
|
|
316
316
|
- lib/howzit/version.rb
|
|
317
|
-
- nested-topics-for-howzit.md
|
|
318
317
|
- scripts/runtests.sh
|
|
319
318
|
- spec/buildnote_spec.rb
|
|
320
319
|
- spec/cli_spec.rb
|
|
321
320
|
- spec/condition_evaluator_spec.rb
|
|
322
321
|
- spec/conditional_blocks_integration_spec.rb
|
|
323
322
|
- spec/conditional_content_spec.rb
|
|
323
|
+
- spec/default_metadata_spec.rb
|
|
324
324
|
- spec/includes_metadata_spec.rb
|
|
325
325
|
- spec/log_level_spec.rb
|
|
326
326
|
- spec/prompt_spec.rb
|
|
@@ -330,6 +330,7 @@ files:
|
|
|
330
330
|
- spec/script_comm_spec.rb
|
|
331
331
|
- spec/sequential_conditional_spec.rb
|
|
332
332
|
- spec/set_var_spec.rb
|
|
333
|
+
- spec/shell_variables_spec.rb
|
|
333
334
|
- spec/spec_helper.rb
|
|
334
335
|
- spec/stack_mode_spec.rb
|
|
335
336
|
- spec/stringutils_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
|
|
@@ -378,6 +380,7 @@ test_files:
|
|
|
378
380
|
- spec/script_comm_spec.rb
|
|
379
381
|
- spec/sequential_conditional_spec.rb
|
|
380
382
|
- spec/set_var_spec.rb
|
|
383
|
+
- spec/shell_variables_spec.rb
|
|
381
384
|
- spec/spec_helper.rb
|
|
382
385
|
- spec/stack_mode_spec.rb
|
|
383
386
|
- spec/stringutils_spec.rb
|
data/nested-topics-for-howzit.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Nested topics for Howzit
|
|
3
|
-
layout: post
|
|
4
|
-
tags: [howzit, ruby, markdown, cli, automation]
|
|
5
|
-
categories: [Blog,Code]
|
|
6
|
-
post_class: 'code'
|
|
7
|
-
comments: true
|
|
8
|
-
---
|
|
9
|
-
[Howzit](https://github.com/ttscoff/howzit) has always split build notes into topics using Markdown headers. What it didn't do was care *which* header level you used. An `##` and a `###` were both just "a topic," and they all got flattened into one long list. As of version 2.1.44, that's changed: deeper headers now nest under the header above them, and that nesting affects both how notes display and what happens when you run them.
|
|
10
|
-
|
|
11
|
-
### The short version
|
|
12
|
-
|
|
13
|
-
A `###` following a `##` is now a subtopic of that `##`. A `####` under the `###` is a subtopic of the `###`, and so on down. The next header at the same level (or shallower) closes out the parent.
|
|
14
|
-
|
|
15
|
-
```markdown
|
|
16
|
-
## Deploy
|
|
17
|
-
|
|
18
|
-
@run(./scripts/preflight.sh) Preflight checks
|
|
19
|
-
|
|
20
|
-
### Build assets
|
|
21
|
-
|
|
22
|
-
@run(gulp) Build
|
|
23
|
-
|
|
24
|
-
### Upload
|
|
25
|
-
|
|
26
|
-
@run(gulp sync) Sync to server
|
|
27
|
-
|
|
28
|
-
## Package management
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Here "Build assets" and "Upload" belong to "Deploy." "Package management" is back at the top level.
|
|
32
|
-
|
|
33
|
-
### Viewing a parent topic
|
|
34
|
-
|
|
35
|
-
When you pull up a parent topic, you now get the whole thing: the parent, followed by all of its subtopics.
|
|
36
|
-
|
|
37
|
-
```console
|
|
38
|
-
$ howzit deploy
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Subtopic headers get a slightly lighter treatment (a dotted rule and a cyan title) so you can see where the nesting happens without it getting noisy. You can still ask for a subtopic directly:
|
|
42
|
-
|
|
43
|
-
```console
|
|
44
|
-
$ howzit upload
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
...and you'll get just that section.
|
|
48
|
-
|
|
49
|
-
### Running a parent topic
|
|
50
|
-
|
|
51
|
-
This is the part I actually wanted. Running a parent now runs everything under it:
|
|
52
|
-
|
|
53
|
-
```console
|
|
54
|
-
$ howzit -r deploy
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
That runs Deploy's own tasks first, then each subtopic's tasks in order, depth-first. So the example above runs preflight, then the build, then the sync. You get one combined summary at the end instead of a "Ran 1 task" line for every section.
|
|
58
|
-
|
|
59
|
-
A few details worth knowing:
|
|
60
|
-
|
|
61
|
-
- **Errors stop the run.** If a task fails, the remaining subtopics are skipped. Add `--force` if you want it to push through anyway.
|
|
62
|
-
- **`@before` and `@after` still work per topic.** Each topic shows its prerequisites before its own tasks run. A parent's `@after` block shows up once the parent *and* all of its subtopics are done, which is usually where you want that "don't forget to..." reminder.
|
|
63
|
-
- **Parents don't need tasks of their own.** A parent that's just an umbrella for a few subtopics runs fine, and it won't complain about "No @directive found."
|
|
64
|
-
- **Subtopics still run on their own.** `howzit -r upload` runs just the upload step.
|
|
65
|
-
|
|
66
|
-
### Includes get the whole tree too
|
|
67
|
-
|
|
68
|
-
If another topic does `@include(Deploy)`, it now runs Deploy and all of its subtopics, same as running it directly. Task counts in include notes and menus reflect the full tree, so you're not surprised by how much is about to happen.
|
|
69
|
-
|
|
70
|
-
### Listing and "show everything"
|
|
71
|
-
|
|
72
|
-
Running `howzit` with no arguments shows every topic. It now only prints top-level topics, since subtopics come along with their parents. No more seeing the same section twice.
|
|
73
|
-
|
|
74
|
-
`howzit -l` indents subtopics under their parents, so the list reflects the structure of your note:
|
|
75
|
-
|
|
76
|
-
```console
|
|
77
|
-
$ howzit -l
|
|
78
|
-
Topics:
|
|
79
|
-
|
|
80
|
-
- Deploy
|
|
81
|
-
- Build assets
|
|
82
|
-
- Upload
|
|
83
|
-
- Package management
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Shell completions stay flat, so you can still tab-complete straight to a subtopic.
|
|
87
|
-
|
|
88
|
-
### One thing to check in your notes
|
|
89
|
-
|
|
90
|
-
If you've been using `###` headers under `##` purely as visual formatting, those are now real subtopics. Viewing the parent will include them (probably what you wanted anyway), and running the parent will run their tasks too. If you had a `###` section with tasks you *don't* want run as part of its parent, bump it up to `##`.
|
|
91
|
-
|
|
92
|
-
### Grab it
|
|
93
|
-
|
|
94
|
-
It's in 2.1.44:
|
|
95
|
-
|
|
96
|
-
```console
|
|
97
|
-
$ gem install howzit
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
The details are in the [build notes anatomy page](https://github.com/ttscoff/howzit/wiki/Anatomy-of-a-build-notes-file#subtopics) on the wiki. I've already started restructuring a few of my own notes around this. Having a single "Release" topic that fans out into build, test, and publish steps is a lot nicer than keeping them all at the same level and chaining them together with `@include`.
|