howzit 2.1.44 → 2.1.45
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 +23 -0
- data/lib/howzit/buildnote.rb +23 -2
- data/lib/howzit/run_report.rb +20 -7
- data/lib/howzit/task.rb +11 -7
- data/lib/howzit/topic.rb +68 -33
- data/lib/howzit/version.rb +1 -1
- data/nested-topics-for-howzit.md +100 -0
- data/spec/run_report_accuracy_spec.rb +171 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b5d2120c8484147d2d3cb8f8844073ca027e7a54655385749ee721f645d05a5
|
|
4
|
+
data.tar.gz: 3cef9cda18bf1b43b28d9288e9eda81d30b07fc9ac2505dd932e508240abf86b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4dc4af7fce8a375a33a3dfee3ed2e6f9d914ea3069601aa3426ae552f4c5488d5aafa332522c4eef0a6b4318bd63afef20eda2d638e9c6fd09ce9784c5e6a434
|
|
7
|
+
data.tar.gz: fe60529771944221e4f7acd4bd9883669782f413c5a367ae3c03c8c7d8ce0a0e0413fcf4e086e37584868c58bbea10bb4e24ab49c28583521d901166404146be
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
### 2.1.45
|
|
2
|
+
|
|
3
|
+
2026-09-27 12:16
|
|
4
|
+
|
|
5
|
+
#### CHANGED
|
|
6
|
+
|
|
7
|
+
- **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.
|
|
8
|
+
|
|
9
|
+
#### NEW
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
- **Skipped @include tasks** are expanded to the included topic's individual tasks in the run report.
|
|
13
|
+
|
|
14
|
+
#### IMPROVED
|
|
15
|
+
|
|
16
|
+
- **Conditional topics** list remaining unconditional tasks as skipped after a failure; tasks inside unevaluated conditionals are omitted.
|
|
17
|
+
|
|
18
|
+
#### FIXED
|
|
19
|
+
|
|
20
|
+
- **Run report** now includes the task that failed and stopped a run, instead of omitting it and showing only the earlier successes.
|
|
21
|
+
- **@include** directives now fail when a task in the included topic fails, so the including topic stops instead of continuing with its remaining tasks.
|
|
22
|
+
- **Task totals for @include** reflect the included topic's actual successes and errors instead of counting every included task as a single result.
|
|
23
|
+
|
|
1
24
|
### 2.1.44
|
|
2
25
|
|
|
3
26
|
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/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
|
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
|
|
@@ -141,7 +142,7 @@ module Howzit
|
|
|
141
142
|
##
|
|
142
143
|
## Execute an include task
|
|
143
144
|
##
|
|
144
|
-
## @return [Array] [[Array] output, [Integer] number of tasks executed]
|
|
145
|
+
## @return [Array] [[Array] output, [Integer] number of tasks executed, [Boolean] all tasks succeeded]
|
|
145
146
|
##
|
|
146
147
|
def run_include
|
|
147
148
|
output = []
|
|
@@ -150,11 +151,13 @@ module Howzit
|
|
|
150
151
|
matches = Howzit.buildnote.find_topic(action)
|
|
151
152
|
raise "Topic not found: #{action}" if matches.empty?
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
topic = matches[0]
|
|
155
|
+
Howzit.console.info("#{@prefix}{by}Running tasks from {bw}#{topic.title}{x}".c)
|
|
156
|
+
output.concat(topic.run(nested: true))
|
|
157
|
+
Howzit.console.info("{by}End include: #{topic.all_tasks.count} tasks{x}".c)
|
|
156
158
|
@last_status = nil
|
|
157
|
-
|
|
159
|
+
@include_results = topic.results.slice(:total, :success, :errors)
|
|
160
|
+
[output, @include_results[:total], @include_results[:errors].zero?]
|
|
158
161
|
end
|
|
159
162
|
|
|
160
163
|
##
|
|
@@ -287,7 +290,8 @@ module Howzit
|
|
|
287
290
|
else
|
|
288
291
|
case @type
|
|
289
292
|
when :include
|
|
290
|
-
output, tasks = run_include
|
|
293
|
+
output, tasks, include_success = run_include
|
|
294
|
+
include_success
|
|
291
295
|
when :run
|
|
292
296
|
run_run
|
|
293
297
|
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,100 @@
|
|
|
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`.
|
|
@@ -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
|
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.45
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -314,6 +314,7 @@ 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
|
|
317
318
|
- scripts/runtests.sh
|
|
318
319
|
- spec/buildnote_spec.rb
|
|
319
320
|
- spec/cli_spec.rb
|
|
@@ -324,6 +325,7 @@ files:
|
|
|
324
325
|
- spec/log_level_spec.rb
|
|
325
326
|
- spec/prompt_spec.rb
|
|
326
327
|
- spec/ruby_gem_spec.rb
|
|
328
|
+
- spec/run_report_accuracy_spec.rb
|
|
327
329
|
- spec/run_report_spec.rb
|
|
328
330
|
- spec/script_comm_spec.rb
|
|
329
331
|
- spec/sequential_conditional_spec.rb
|
|
@@ -371,6 +373,7 @@ 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
|