howzit 2.1.44 → 2.1.46
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 +48 -0
- data/lib/howzit/buildnote.rb +23 -2
- data/lib/howzit/config.rb +1 -0
- data/lib/howzit/run_report.rb +20 -7
- data/lib/howzit/script_support.rb +29 -0
- data/lib/howzit/stringutils.rb +66 -13
- data/lib/howzit/task.rb +40 -13
- data/lib/howzit/topic.rb +68 -33
- data/lib/howzit/version.rb +1 -1
- data/spec/run_report_accuracy_spec.rb +171 -0
- data/spec/shell_variables_spec.rb +177 -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: daae85713f05d780640619a8ada2291aaa69c0eb3f2d941f6fb9e2964c9247b4
|
|
4
|
+
data.tar.gz: 5773440b0a9718af5dd0b3e66929bfa74023061b99a85c61d65558b76ac57d4d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f688d2e403997a3cf6f3b91c0ebd3746ed51b1d0bc4dfea2be5fd1297a22715d2a7b5672482b2f243dc26b38b56eee2880c9e57df98c55c16178b84eddeb8da
|
|
7
|
+
data.tar.gz: 934e5ef9e7b71bc0323475ae4c54596d7a6813f552d63fd08dd358f51805c6950780d4435b9cf830aed7712392a5a38d52077dfbcceb69293adf02f9b0970794
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
### 2.1.46
|
|
2
|
+
|
|
3
|
+
2026-09-27 12:52
|
|
4
|
+
|
|
5
|
+
#### CHANGED
|
|
6
|
+
|
|
7
|
+
- **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.
|
|
8
|
+
- **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).
|
|
9
|
+
|
|
10
|
+
#### NEW
|
|
11
|
+
|
|
12
|
+
- **:shell_variables: config option** (env or substitute) to switch shell run blocks back to text substitution; defaults to env.
|
|
13
|
+
- **$${VAR} escape** passes ${VAR} through to the shell untouched in @run, @copy, and text-substituted blocks.
|
|
14
|
+
|
|
15
|
+
#### IMPROVED
|
|
16
|
+
|
|
17
|
+
- **Howzit-style defaults** (${name:default}) in shell run blocks are converted to the shell's ${name:-default} so they keep working with environment variables.
|
|
18
|
+
|
|
19
|
+
#### FIXED
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
- **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}'.
|
|
23
|
+
- **$@ and $*** are only replaced when arguments were passed after --, instead of always being replaced with an empty string.
|
|
24
|
+
- **Default values containing colons** (e.g. ${url:http://localhost:3000}) are no longer cut off at the second colon.
|
|
25
|
+
|
|
26
|
+
### 2.1.45
|
|
27
|
+
|
|
28
|
+
2026-09-27 12:16
|
|
29
|
+
|
|
30
|
+
#### CHANGED
|
|
31
|
+
|
|
32
|
+
- **Multi-topic runs** (comma-separated topics or `default:` metadata) now stop after the first topic with a failed task and mark the remaining topics' tasks as skipped; use --force to run every topic.
|
|
33
|
+
|
|
34
|
+
#### NEW
|
|
35
|
+
|
|
36
|
+
- **Skipped tasks** in the run report: tasks that did not run because of an earlier failure are listed with a skipped marker and "(skipped)" label.
|
|
37
|
+
- **Skipped @include tasks** are expanded to the included topic's individual tasks in the run report.
|
|
38
|
+
|
|
39
|
+
#### IMPROVED
|
|
40
|
+
|
|
41
|
+
- **Conditional topics** list remaining unconditional tasks as skipped after a failure; tasks inside unevaluated conditionals are omitted.
|
|
42
|
+
|
|
43
|
+
#### FIXED
|
|
44
|
+
|
|
45
|
+
- **Run report** now includes the task that failed and stopped a run, instead of omitting it and showing only the earlier successes.
|
|
46
|
+
- **@include** directives now fail when a task in the included topic fails, so the including topic stops instead of continuing with its remaining tasks.
|
|
47
|
+
- **Task totals for @include** reflect the included topic's actual successes and errors instead of counting every included task as a single result.
|
|
48
|
+
|
|
1
49
|
### 2.1.44
|
|
2
50
|
|
|
3
51
|
2026-09-27 11:38
|
data/lib/howzit/buildnote.rb
CHANGED
|
@@ -1558,8 +1558,10 @@ module Howzit
|
|
|
1558
1558
|
end
|
|
1559
1559
|
topic_matches.compact! # Remove any nil values from failed matches
|
|
1560
1560
|
# A matched subtopic is already output/run with its matched parent
|
|
1561
|
-
top_level_topics(topic_matches)
|
|
1561
|
+
selected = top_level_topics(topic_matches)
|
|
1562
|
+
selected.each_with_index do |topic_match, idx|
|
|
1562
1563
|
output.push(process_topic(topic_match, Howzit.options[:run], single: true))
|
|
1564
|
+
break if stop_after_failure?(selected[(idx + 1)..])
|
|
1563
1565
|
end
|
|
1564
1566
|
else
|
|
1565
1567
|
top_level_topics.each { |k| output.push(process_topic(k, false, single: false)) }
|
|
@@ -1599,7 +1601,7 @@ module Howzit
|
|
|
1599
1601
|
end
|
|
1600
1602
|
|
|
1601
1603
|
# Run each topic with its specific arguments
|
|
1602
|
-
topic_specs.
|
|
1604
|
+
topic_specs.each_with_index do |(topic_match, args), idx|
|
|
1603
1605
|
# Set arguments if provided, otherwise clear them
|
|
1604
1606
|
Howzit.arguments = if args && !args.empty?
|
|
1605
1607
|
args.split(/ *, */).map(&:render_arguments)
|
|
@@ -1607,10 +1609,29 @@ module Howzit
|
|
|
1607
1609
|
[]
|
|
1608
1610
|
end
|
|
1609
1611
|
output.push(process_topic(topic_match, Howzit.options[:run], single: true))
|
|
1612
|
+
break if stop_after_failure?(topic_specs[(idx + 1)..].map(&:first))
|
|
1610
1613
|
end
|
|
1611
1614
|
finalize_output(output)
|
|
1612
1615
|
end
|
|
1613
1616
|
|
|
1617
|
+
##
|
|
1618
|
+
## After running a topic in a multi-topic run, check whether a task
|
|
1619
|
+
## failed. If so (and --force isn't set), log the remaining topics'
|
|
1620
|
+
## tasks as skipped.
|
|
1621
|
+
##
|
|
1622
|
+
## @param remaining [Array] Topics that haven't run yet
|
|
1623
|
+
##
|
|
1624
|
+
## @return [Boolean] true if the run should stop
|
|
1625
|
+
##
|
|
1626
|
+
def stop_after_failure?(remaining)
|
|
1627
|
+
return false unless Howzit.options[:run]
|
|
1628
|
+
return false if Howzit.options[:force]
|
|
1629
|
+
return false unless Howzit.run_log.any? { |entry| !entry[:success] && !entry[:skipped] }
|
|
1630
|
+
|
|
1631
|
+
remaining.each { |topic| topic.log_skipped_tasks(topic.all_tasks) }
|
|
1632
|
+
true
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1614
1635
|
##
|
|
1615
1636
|
## Parse default metadata value into individual topic specifications
|
|
1616
1637
|
##
|
data/lib/howzit/config.rb
CHANGED
data/lib/howzit/run_report.rb
CHANGED
|
@@ -26,8 +26,20 @@ module Howzit
|
|
|
26
26
|
output_lines.join("\n")
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def status_symbol(entry)
|
|
30
|
+
return '⏭️' if entry[:skipped]
|
|
31
|
+
|
|
32
|
+
entry[:success] ? '✅' : '❌'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def status_reason(entry)
|
|
36
|
+
return 'skipped' if entry[:skipped]
|
|
37
|
+
|
|
38
|
+
entry[:exit_status] ? "exit code #{entry[:exit_status]}" : 'failed'
|
|
39
|
+
end
|
|
40
|
+
|
|
29
41
|
def format_line(entry, prefix_topic)
|
|
30
|
-
symbol = entry
|
|
42
|
+
symbol = status_symbol(entry)
|
|
31
43
|
parts = ["#{symbol} "]
|
|
32
44
|
if prefix_topic && entry[:topic] && !entry[:topic].empty?
|
|
33
45
|
# Escape braces and dollar signs in topic name to prevent color code interpretation
|
|
@@ -36,10 +48,11 @@ module Howzit
|
|
|
36
48
|
end
|
|
37
49
|
# Escape braces and dollar signs in task name to prevent color code interpretation
|
|
38
50
|
task_escaped = entry[:task].gsub(/\{/, '\\{').gsub(/\}/, '\\}').gsub(/\$/, '\\$')
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
parts << "
|
|
51
|
+
if entry[:skipped]
|
|
52
|
+
parts << "{d}#{task_escaped} (skipped){x}"
|
|
53
|
+
else
|
|
54
|
+
parts << "{by}#{task_escaped} {x}"
|
|
55
|
+
parts << " {br}(#{status_reason(entry)}){x}" unless entry[:success]
|
|
43
56
|
end
|
|
44
57
|
parts.join.c
|
|
45
58
|
end
|
|
@@ -76,7 +89,7 @@ module Howzit
|
|
|
76
89
|
def format_row(entry, prefix_topic)
|
|
77
90
|
# Use plain emoji without color codes - the emoji itself provides visual meaning
|
|
78
91
|
# and complex ANSI codes interfere with mdless table rendering
|
|
79
|
-
symbol = entry
|
|
92
|
+
symbol = status_symbol(entry)
|
|
80
93
|
|
|
81
94
|
task_parts = []
|
|
82
95
|
task_parts_plain = []
|
|
@@ -94,7 +107,7 @@ module Howzit
|
|
|
94
107
|
task_parts_plain << entry[:task]
|
|
95
108
|
|
|
96
109
|
unless entry[:success]
|
|
97
|
-
reason = entry
|
|
110
|
+
reason = status_reason(entry)
|
|
98
111
|
task_parts << " {br}(#{reason}){x}"
|
|
99
112
|
task_parts_plain << " (#{reason})"
|
|
100
113
|
end
|
|
@@ -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
|
@@ -5,7 +5,8 @@ require 'English'
|
|
|
5
5
|
module Howzit
|
|
6
6
|
# Task object
|
|
7
7
|
class Task
|
|
8
|
-
attr_reader :type, :title, :action, :arguments, :parent, :optional, :default, :last_status, :log_level, :source_file
|
|
8
|
+
attr_reader :type, :title, :action, :arguments, :parent, :optional, :default, :last_status, :log_level, :source_file,
|
|
9
|
+
:include_results
|
|
9
10
|
|
|
10
11
|
##
|
|
11
12
|
## Initialize a Task object
|
|
@@ -31,7 +32,8 @@ module Howzit
|
|
|
31
32
|
@title = attributes[:title]&.to_s
|
|
32
33
|
@parent = attributes[:parent] || nil
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
action = attributes[:action]
|
|
36
|
+
@action = shell_env_block?(action) ? action : action.render_arguments
|
|
35
37
|
@log_level = attributes[:log_level]
|
|
36
38
|
# Get source_file from parent topic if available, or from attributes
|
|
37
39
|
parent_obj = attributes[:parent]
|
|
@@ -60,15 +62,37 @@ module Howzit
|
|
|
60
62
|
@title
|
|
61
63
|
end
|
|
62
64
|
|
|
65
|
+
##
|
|
66
|
+
## Whether this is a shell run block that receives Howzit
|
|
67
|
+
## variables through the environment instead of text substitution
|
|
68
|
+
##
|
|
69
|
+
## @param content [String] The block content
|
|
70
|
+
##
|
|
71
|
+
def shell_env_block?(content = @action)
|
|
72
|
+
@type == :block &&
|
|
73
|
+
Howzit.options[:shell_variables].to_s != 'substitute' &&
|
|
74
|
+
ScriptSupport.shell_script?(content)
|
|
75
|
+
end
|
|
76
|
+
|
|
63
77
|
##
|
|
64
78
|
## Execute a block type
|
|
65
79
|
##
|
|
66
80
|
def run_block
|
|
67
81
|
Howzit.console.info "#{@prefix}{bg}Running block {bw}#{@title}{x}".c if Howzit.options[:log_level] < 2
|
|
68
82
|
block = @action
|
|
83
|
+
env_mode = shell_env_block?(block)
|
|
69
84
|
# Apply variable substitution to block content at execution time
|
|
70
85
|
# (variables from previous run blocks are now available)
|
|
71
|
-
|
|
86
|
+
if env_mode
|
|
87
|
+
block = block.shellify_defaults
|
|
88
|
+
env = ScriptSupport.variables_env
|
|
89
|
+
script_args = Howzit.arguments || []
|
|
90
|
+
else
|
|
91
|
+
block = block.render_arguments if block && !block.empty?
|
|
92
|
+
env = {}
|
|
93
|
+
script_args = []
|
|
94
|
+
end
|
|
95
|
+
block = block.unescape_placeholders
|
|
72
96
|
script = Tempfile.new('howzit_script')
|
|
73
97
|
comm_file = ScriptComm.setup
|
|
74
98
|
old_log_level = apply_log_level
|
|
@@ -116,9 +140,9 @@ module Howzit
|
|
|
116
140
|
cmd = ScriptSupport.execution_command_for(script.path, interpreter)
|
|
117
141
|
# If interpreter is nil, execute directly (will respect hashbang)
|
|
118
142
|
res = if interpreter.nil?
|
|
119
|
-
system(script.path)
|
|
143
|
+
system(env, script.path, *script_args)
|
|
120
144
|
else
|
|
121
|
-
system(cmd)
|
|
145
|
+
system(env, [cmd, *script_args.map { |arg| Shellwords.escape(arg) }].join(' '))
|
|
122
146
|
end
|
|
123
147
|
ensure
|
|
124
148
|
# Restore original directory
|
|
@@ -141,7 +165,7 @@ module Howzit
|
|
|
141
165
|
##
|
|
142
166
|
## Execute an include task
|
|
143
167
|
##
|
|
144
|
-
## @return [Array] [[Array] output, [Integer] number of tasks executed]
|
|
168
|
+
## @return [Array] [[Array] output, [Integer] number of tasks executed, [Boolean] all tasks succeeded]
|
|
145
169
|
##
|
|
146
170
|
def run_include
|
|
147
171
|
output = []
|
|
@@ -150,11 +174,13 @@ module Howzit
|
|
|
150
174
|
matches = Howzit.buildnote.find_topic(action)
|
|
151
175
|
raise "Topic not found: #{action}" if matches.empty?
|
|
152
176
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
topic = matches[0]
|
|
178
|
+
Howzit.console.info("#{@prefix}{by}Running tasks from {bw}#{topic.title}{x}".c)
|
|
179
|
+
output.concat(topic.run(nested: true))
|
|
180
|
+
Howzit.console.info("{by}End include: #{topic.all_tasks.count} tasks{x}".c)
|
|
156
181
|
@last_status = nil
|
|
157
|
-
|
|
182
|
+
@include_results = topic.results.slice(:total, :success, :errors)
|
|
183
|
+
[output, @include_results[:total], @include_results[:errors].zero?]
|
|
158
184
|
end
|
|
159
185
|
|
|
160
186
|
##
|
|
@@ -238,7 +264,7 @@ module Howzit
|
|
|
238
264
|
Dir.chdir(expanded_exec_dir) if expanded_exec_dir != expanded_original
|
|
239
265
|
end
|
|
240
266
|
|
|
241
|
-
res = system(@action)
|
|
267
|
+
res = system(@action.unescape_placeholders)
|
|
242
268
|
ensure
|
|
243
269
|
# Restore original directory
|
|
244
270
|
if exec_dir && Dir.exist?(exec_dir)
|
|
@@ -271,7 +297,7 @@ module Howzit
|
|
|
271
297
|
@title && !@title.empty? ? @title : @action
|
|
272
298
|
end
|
|
273
299
|
Howzit.console.info("#{@prefix}{bg}Copied {bw}#{display_title}{bg} to clipboard{x}".c)
|
|
274
|
-
Util.os_copy(@action)
|
|
300
|
+
Util.os_copy(@action.unescape_placeholders)
|
|
275
301
|
@last_status = 0
|
|
276
302
|
true
|
|
277
303
|
end
|
|
@@ -287,7 +313,8 @@ module Howzit
|
|
|
287
313
|
else
|
|
288
314
|
case @type
|
|
289
315
|
when :include
|
|
290
|
-
output, tasks = run_include
|
|
316
|
+
output, tasks, include_success = run_include
|
|
317
|
+
include_success
|
|
291
318
|
when :run
|
|
292
319
|
run_run
|
|
293
320
|
when :copy
|
data/lib/howzit/topic.rb
CHANGED
|
@@ -157,7 +157,11 @@ module Howzit
|
|
|
157
157
|
Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
if halted?
|
|
161
|
+
log_skipped_tasks(@subtopics.flat_map(&:all_tasks))
|
|
162
|
+
else
|
|
163
|
+
run_subtopics(output)
|
|
164
|
+
end
|
|
161
165
|
|
|
162
166
|
if @results[:total].positive? || (all_tasks.any? && !sequential?)
|
|
163
167
|
@results[:message] += results_message
|
|
@@ -225,37 +229,76 @@ module Howzit
|
|
|
225
229
|
## Run tasks without conditional evaluation
|
|
226
230
|
##
|
|
227
231
|
def run_tasks(output)
|
|
228
|
-
@tasks.
|
|
232
|
+
@tasks.each_with_index do |task, idx|
|
|
229
233
|
next if (task.optional || Howzit.options[:ask]) && !ask_task(task)
|
|
230
234
|
|
|
231
235
|
run_output, total, success = task.run
|
|
232
236
|
|
|
233
237
|
output.concat(run_output)
|
|
238
|
+
record_task_result(task, total, success)
|
|
239
|
+
next unless halted?
|
|
240
|
+
|
|
241
|
+
log_skipped_tasks(@tasks[(idx + 1)..])
|
|
242
|
+
break
|
|
243
|
+
end
|
|
244
|
+
output
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
##
|
|
248
|
+
## Add a task's results to the topic totals and the run report
|
|
249
|
+
##
|
|
250
|
+
## @param task [Task] The task that ran
|
|
251
|
+
## @param total [Integer] Number of tasks it represents
|
|
252
|
+
## @param success [Boolean] Whether it succeeded
|
|
253
|
+
##
|
|
254
|
+
def record_task_result(task, total, success)
|
|
255
|
+
if task.type == :include && task.include_results
|
|
256
|
+
%i[total success errors].each { |key| @results[key] += task.include_results[key] }
|
|
257
|
+
else
|
|
234
258
|
@results[:total] += total
|
|
259
|
+
@results[success ? :success : :errors] += total
|
|
260
|
+
end
|
|
235
261
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c
|
|
262
|
+
log_task_result(task, success)
|
|
263
|
+
Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c unless success
|
|
264
|
+
end
|
|
240
265
|
|
|
241
|
-
|
|
266
|
+
##
|
|
267
|
+
## Log tasks that won't run because an earlier task failed.
|
|
268
|
+
## Include tasks are expanded to the tasks of the included topic.
|
|
269
|
+
##
|
|
270
|
+
## @param tasks [Array] Task objects
|
|
271
|
+
## @param seen [Array] Titles of included topics already expanded
|
|
272
|
+
##
|
|
273
|
+
def log_skipped_tasks(tasks, seen = [])
|
|
274
|
+
return unless Howzit.options[:run]
|
|
242
275
|
|
|
243
|
-
|
|
276
|
+
Howzit.run_log ||= []
|
|
277
|
+
tasks.each do |task|
|
|
278
|
+
if task.type == :include
|
|
279
|
+
topic = Howzit.buildnote.find_topic(task.action)[0]
|
|
280
|
+
next if topic.nil? || seen.include?(topic.title)
|
|
281
|
+
|
|
282
|
+
topic.log_skipped_tasks(topic.all_tasks, seen + [topic.title])
|
|
283
|
+
next
|
|
244
284
|
end
|
|
245
285
|
|
|
246
|
-
|
|
286
|
+
topic_title = task.parent.is_a?(Topic) ? task.parent.title : @title
|
|
287
|
+
Howzit.run_log << { topic: topic_title, task: task_log_title(task), success: false, skipped: true }
|
|
247
288
|
end
|
|
248
|
-
output
|
|
249
289
|
end
|
|
250
290
|
|
|
251
291
|
##
|
|
252
292
|
## Run each subtopic in order, adding its results to this topic's
|
|
253
293
|
##
|
|
254
294
|
def run_subtopics(output)
|
|
255
|
-
@subtopics.
|
|
295
|
+
@subtopics.each_with_index do |sub, idx|
|
|
256
296
|
output.concat(sub.run(nested: true, as_subtopic: true))
|
|
257
297
|
%i[total success errors].each { |key| @results[key] += sub.results[key] }
|
|
258
|
-
|
|
298
|
+
next unless halted?
|
|
299
|
+
|
|
300
|
+
log_skipped_tasks(@subtopics[(idx + 1)..].flat_map(&:all_tasks))
|
|
301
|
+
break
|
|
259
302
|
end
|
|
260
303
|
output
|
|
261
304
|
end
|
|
@@ -538,22 +581,20 @@ module Howzit
|
|
|
538
581
|
return if task.type == :include
|
|
539
582
|
|
|
540
583
|
Howzit.run_log ||= []
|
|
541
|
-
|
|
542
|
-
title = (task.title || '').strip
|
|
543
|
-
if title.empty?
|
|
544
|
-
action = (task.action || '').strip
|
|
545
|
-
title = action.split(/\n/).first.to_s.strip
|
|
546
|
-
end
|
|
547
|
-
title = task.type.to_s.capitalize if title.nil? || title.empty?
|
|
548
|
-
|
|
549
584
|
Howzit.run_log << {
|
|
550
585
|
topic: @title,
|
|
551
|
-
task:
|
|
586
|
+
task: task_log_title(task),
|
|
552
587
|
success: success ? true : false,
|
|
553
588
|
exit_status: task.last_status
|
|
554
589
|
}
|
|
555
590
|
end
|
|
556
591
|
|
|
592
|
+
def task_log_title(task)
|
|
593
|
+
title = (task.title || '').strip
|
|
594
|
+
title = (task.action || '').strip.split(/\n/).first.to_s.strip if title.empty?
|
|
595
|
+
title.empty? ? task.type.to_s.capitalize : title
|
|
596
|
+
end
|
|
597
|
+
|
|
557
598
|
def gather_tasks
|
|
558
599
|
runnable = []
|
|
559
600
|
# Process conditional blocks first
|
|
@@ -968,20 +1009,14 @@ module Howzit
|
|
|
968
1009
|
run_output, total, success = task.run
|
|
969
1010
|
|
|
970
1011
|
output.concat(run_output)
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
@
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
@results[:errors] += total
|
|
979
|
-
|
|
980
|
-
break unless Howzit.options[:force]
|
|
1012
|
+
record_task_result(task, total, success)
|
|
1013
|
+
if halted?
|
|
1014
|
+
# Tasks inside conditionals are omitted, since their conditions were never evaluated
|
|
1015
|
+
remaining = @directives[directive_index..].select { |d| d.task? && (d.conditional_path || []).empty? }
|
|
1016
|
+
log_skipped_tasks(remaining.filter_map { |d| d.to_task(self) })
|
|
1017
|
+
break
|
|
981
1018
|
end
|
|
982
1019
|
|
|
983
|
-
log_task_result(task, success)
|
|
984
|
-
|
|
985
1020
|
# Re-evaluate all open conditionals after task execution
|
|
986
1021
|
re_evaluate_conditionals(conditional_state, directive_index - 1, context)
|
|
987
1022
|
end
|
data/lib/howzit/version.rb
CHANGED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
describe 'run report accuracy' do
|
|
8
|
+
let(:temp_dir) { File.expand_path(Dir.mktmpdir('howzit_run_report_test')) }
|
|
9
|
+
let(:buildnote) { Howzit::BuildNote.new }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
File.write(
|
|
13
|
+
File.join(temp_dir, 'buildnotes.md'),
|
|
14
|
+
<<~NOTE
|
|
15
|
+
# Run Report Project
|
|
16
|
+
|
|
17
|
+
## Failing
|
|
18
|
+
|
|
19
|
+
@run(true) First
|
|
20
|
+
@run(false) Second
|
|
21
|
+
@run(true) Third
|
|
22
|
+
|
|
23
|
+
## Release
|
|
24
|
+
|
|
25
|
+
@include(Failing)
|
|
26
|
+
@run(true) After include
|
|
27
|
+
|
|
28
|
+
## Early Failure
|
|
29
|
+
|
|
30
|
+
@run(false) Fails first
|
|
31
|
+
@include(Failing)
|
|
32
|
+
|
|
33
|
+
## Conditional
|
|
34
|
+
|
|
35
|
+
@if 1 == 1
|
|
36
|
+
@run(false) Conditional failure
|
|
37
|
+
@run(true) Conditional after
|
|
38
|
+
@end
|
|
39
|
+
@run(true) Unconditional after
|
|
40
|
+
|
|
41
|
+
## Parent
|
|
42
|
+
|
|
43
|
+
@run(true) Parent task
|
|
44
|
+
|
|
45
|
+
### Broken Child
|
|
46
|
+
|
|
47
|
+
@run(false) Child failure
|
|
48
|
+
@run(true) Child after
|
|
49
|
+
|
|
50
|
+
### Later Child
|
|
51
|
+
|
|
52
|
+
@run(true) Later child task
|
|
53
|
+
NOTE
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
Dir.chdir(temp_dir)
|
|
57
|
+
Howzit.instance_variable_set(:@buildnote, buildnote)
|
|
58
|
+
Howzit.options[:stack] = false
|
|
59
|
+
Howzit.options[:include_upstream] = false
|
|
60
|
+
Howzit.options[:run] = true
|
|
61
|
+
Howzit.options[:force] = false
|
|
62
|
+
Howzit.run_log = []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
after do
|
|
66
|
+
Howzit.options[:run] = false
|
|
67
|
+
Howzit.options[:force] = false
|
|
68
|
+
Howzit.run_log = []
|
|
69
|
+
Dir.chdir(Dir.tmpdir)
|
|
70
|
+
FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
|
|
71
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def report
|
|
75
|
+
Howzit.run_log.map do |entry|
|
|
76
|
+
status = if entry[:skipped]
|
|
77
|
+
:skipped
|
|
78
|
+
else
|
|
79
|
+
entry[:success] ? :ok : :failed
|
|
80
|
+
end
|
|
81
|
+
[entry[:task], status]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'logs the failed task and marks the rest as skipped' do
|
|
86
|
+
buildnote.find_topic('Failing')[0].run
|
|
87
|
+
|
|
88
|
+
expect(report).to eq([['First', :ok], ['Second', :failed], ['Third', :skipped]])
|
|
89
|
+
expect(Howzit.run_log[1][:exit_status]).to eq(1)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'treats a failed include as a failure and skips the rest of the including topic' do
|
|
93
|
+
topic = buildnote.find_topic('Release')[0]
|
|
94
|
+
topic.run
|
|
95
|
+
|
|
96
|
+
expect(report).to eq([['First', :ok], ['Second', :failed], ['Third', :skipped], ['After include', :skipped]])
|
|
97
|
+
expect(topic.results).to include(total: 2, success: 1, errors: 1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'expands skipped includes into the included topic tasks' do
|
|
101
|
+
buildnote.find_topic('Early Failure')[0].run
|
|
102
|
+
|
|
103
|
+
expect(report).to eq([['Fails first', :failed], ['First', :skipped], ['Second', :skipped], ['Third', :skipped]])
|
|
104
|
+
expect(Howzit.run_log[1][:topic]).to eq('Failing')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'skips only unconditional tasks after a failure in a conditional topic' do
|
|
108
|
+
buildnote.find_topic('Conditional')[0].run
|
|
109
|
+
|
|
110
|
+
expect(report).to eq([['Conditional failure', :failed], ['Unconditional after', :skipped]])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'skips remaining subtopics after a subtopic fails' do
|
|
114
|
+
buildnote.find_topic('Parent')[0].run
|
|
115
|
+
|
|
116
|
+
expect(report).to eq([
|
|
117
|
+
['Parent task', :ok],
|
|
118
|
+
['Child failure', :failed],
|
|
119
|
+
['Child after', :skipped],
|
|
120
|
+
['Later child task', :skipped]
|
|
121
|
+
])
|
|
122
|
+
expect(Howzit.run_log.last[:topic]).to eq('Later Child')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'continues past failures with --force and logs every result' do
|
|
126
|
+
Howzit.options[:force] = true
|
|
127
|
+
topic = buildnote.find_topic('Release')[0]
|
|
128
|
+
topic.run
|
|
129
|
+
|
|
130
|
+
expect(report).to eq([['First', :ok], ['Second', :failed], ['Third', :ok], ['After include', :ok]])
|
|
131
|
+
expect(topic.results).to include(total: 4, success: 3, errors: 1)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe 'multi-topic runs' do
|
|
135
|
+
before do
|
|
136
|
+
allow(buildnote).to receive(:finalize_output)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
let(:topics) { ['Failing', 'Parent'].map { |title| buildnote.find_topic(title)[0] } }
|
|
140
|
+
|
|
141
|
+
it 'stops after a topic fails and skips the remaining topics' do
|
|
142
|
+
buildnote.send(:process_topic_matches, topics, [])
|
|
143
|
+
|
|
144
|
+
expect(report).to eq([
|
|
145
|
+
['First', :ok],
|
|
146
|
+
['Second', :failed],
|
|
147
|
+
['Third', :skipped],
|
|
148
|
+
['Parent task', :skipped],
|
|
149
|
+
['Child failure', :skipped],
|
|
150
|
+
['Child after', :skipped],
|
|
151
|
+
['Later child task', :skipped]
|
|
152
|
+
])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'runs every topic with --force' do
|
|
156
|
+
Howzit.options[:force] = true
|
|
157
|
+
buildnote.send(:process_topic_matches, topics, [])
|
|
158
|
+
|
|
159
|
+
expect(report.map(&:first)).to include('Parent task', 'Later child task')
|
|
160
|
+
expect(report.map(&:last)).not_to include(:skipped)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'formats skipped tasks in the report' do
|
|
165
|
+
buildnote.find_topic('Failing')[0].run
|
|
166
|
+
plain = Howzit::RunReport.format.uncolor
|
|
167
|
+
|
|
168
|
+
expect(plain).to include('❌ Second (exit code 1)')
|
|
169
|
+
expect(plain).to include('⏭️ Third (skipped)')
|
|
170
|
+
end
|
|
171
|
+
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.46
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -324,10 +324,12 @@ files:
|
|
|
324
324
|
- spec/log_level_spec.rb
|
|
325
325
|
- spec/prompt_spec.rb
|
|
326
326
|
- spec/ruby_gem_spec.rb
|
|
327
|
+
- spec/run_report_accuracy_spec.rb
|
|
327
328
|
- spec/run_report_spec.rb
|
|
328
329
|
- spec/script_comm_spec.rb
|
|
329
330
|
- spec/sequential_conditional_spec.rb
|
|
330
331
|
- spec/set_var_spec.rb
|
|
332
|
+
- spec/shell_variables_spec.rb
|
|
331
333
|
- spec/spec_helper.rb
|
|
332
334
|
- spec/stack_mode_spec.rb
|
|
333
335
|
- spec/stringutils_spec.rb
|
|
@@ -371,10 +373,12 @@ test_files:
|
|
|
371
373
|
- spec/log_level_spec.rb
|
|
372
374
|
- spec/prompt_spec.rb
|
|
373
375
|
- spec/ruby_gem_spec.rb
|
|
376
|
+
- spec/run_report_accuracy_spec.rb
|
|
374
377
|
- spec/run_report_spec.rb
|
|
375
378
|
- spec/script_comm_spec.rb
|
|
376
379
|
- spec/sequential_conditional_spec.rb
|
|
377
380
|
- spec/set_var_spec.rb
|
|
381
|
+
- spec/shell_variables_spec.rb
|
|
378
382
|
- spec/spec_helper.rb
|
|
379
383
|
- spec/stack_mode_spec.rb
|
|
380
384
|
- spec/stringutils_spec.rb
|