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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 881188642b99f4be83a7697440251ec8890e6d847468c3984a72183939196857
4
- data.tar.gz: c622b23318a075aa277a0a197e1ec93d8023e52e99018557e7eb6f8309c76927
3
+ metadata.gz: 3b5d2120c8484147d2d3cb8f8844073ca027e7a54655385749ee721f645d05a5
4
+ data.tar.gz: 3cef9cda18bf1b43b28d9288e9eda81d30b07fc9ac2505dd932e508240abf86b
5
5
  SHA512:
6
- metadata.gz: 219ae789410d1ec44619f3bfb00b7f8b539a33632e9b9d9e778f384205e03c653efa83a7a090d469dd769a2cdda53ae7026e40b495363879e59f24be38da09ae
7
- data.tar.gz: d0c1dd2d03ab11923e93440fb9f04bfdfb0292d63522fd21483a38a7d2ca31c691dc64e4a894063944b586ef1b2af66fd8a4bd55ea132ce743b6e69e7fd812db
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
@@ -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).each do |topic_match|
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.each do |topic_match, args|
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
  ##
@@ -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[:success] ? '✅' : '❌'
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
- parts << "{by}#{task_escaped} {x}"
40
- unless entry[:success]
41
- reason = entry[:exit_status] ? "exit code #{entry[:exit_status]}" : 'failed'
42
- parts << " {br}(#{reason}){x}"
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[:success] ? '✅' : '❌'
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[:exit_status] ? "exit code #{entry[:exit_status]}" : 'failed'
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
- Howzit.console.info("#{@prefix}{by}Running tasks from {bw}#{matches[0].title}{x}".c)
154
- output.concat(matches[0].run(nested: true))
155
- Howzit.console.info("{by}End include: #{matches[0].all_tasks.count} tasks{x}".c)
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
- [output, matches[0].all_tasks.count]
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
- run_subtopics(output) unless halted?
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.each do |task|
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
- if success
237
- @results[:success] += total
238
- else
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
- @results[:errors] += total
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
- break unless Howzit.options[:force]
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
- log_task_result(task, success)
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.each do |sub|
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
- break if halted?
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: title,
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
- @results[:total] += total
972
-
973
- if success
974
- @results[:success] += total
975
- else
976
- Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c
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
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Howzit
5
5
  # Current Howzit version.
6
- VERSION = '2.1.44'
6
+ VERSION = '2.1.45'
7
7
  end
@@ -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.44
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