taski 0.8.1 → 0.8.3

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: 7c841d7a7a8e632045f42b090be2c6b10a9ef1510c9b9298bc50737f89486f28
4
- data.tar.gz: 540f4595d667f191ac2a36464529e324fd56c5a5466dbcc2580b5227030501b5
3
+ metadata.gz: cddae184fb1f55c69b42995e51c3bf5711d31f978ec383a00a73d2ce3531ca15
4
+ data.tar.gz: 3e79ca2bf3a6cd163272bbff98ff3b980f3a3b69293607dd45879501d50904c4
5
5
  SHA512:
6
- metadata.gz: c4b2064e55edde2f504fd8c857efff572faf6de8466983c0081c2ca902c63784c75d121c482691cf24e1b8f80a863c97c2268e358271692967adc5612933da28
7
- data.tar.gz: b56270d6324127635290a906bb6d7361a8c9609ad071ea307b26dcd3b16489c073be909e744680b52a202ab99019c611d9a0112cf2b18fc180f8113fb3fcef5a
6
+ metadata.gz: 9310e8b52be98d9087de88b4b7f5d105896edd85137c2f43e6c968c8daaf00dc6b4843ff9d1f228a089dcd7ceb379715c2b640d3c547ce173659dc7130cd1768
7
+ data.tar.gz: 65bade2024691c9c7527f6249294d01714563ff35c211472e6d984e82ba5acd0767251d33e05d8ade4208eabc8b1326eb47fc4f932b4699bf0b986e208fffcea
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.3] - 2026-01-26
11
+
12
+ ### Fixed
13
+ - Improve progress display accuracy for section candidates ([#136](https://github.com/ahogappa/taski/pull/136))
14
+
15
+ ### Changed
16
+ - Extract helper methods for improved readability ([#136](https://github.com/ahogappa/taski/pull/136))
17
+
18
+ ## [0.8.2] - 2026-01-26
19
+
20
+ ### Fixed
21
+ - Queue `Taski.message` output until progress display stops to prevent interleaved output ([#133](https://github.com/ahogappa/taski/pull/133))
22
+ - Correct task count display in SimpleProgressDisplay ([#132](https://github.com/ahogappa/taski/pull/132))
23
+
24
+ ### Changed
25
+ - Consolidate examples from 15 to 8 files for better maintainability ([#131](https://github.com/ahogappa/taski/pull/131))
26
+
10
27
  ## [0.8.1] - 2026-01-26
11
28
 
12
29
  ### Added
data/README.md CHANGED
@@ -262,6 +262,34 @@ end
262
262
 
263
263
  When `TaskAbortException` is raised, no new tasks will start. Already running tasks will complete, then execution stops.
264
264
 
265
+ ### Lifecycle Management
266
+
267
+ Define `clean` methods for resource cleanup. Clean runs in reverse dependency order:
268
+
269
+ ```ruby
270
+ class DatabaseSetup < Taski::Task
271
+ exports :connection
272
+
273
+ def run
274
+ @connection = connect_to_database
275
+ end
276
+
277
+ def clean
278
+ @connection&.close
279
+ end
280
+ end
281
+
282
+ # Run then clean in one call
283
+ DatabaseSetup.run_and_clean
284
+
285
+ # Or separately
286
+ DatabaseSetup.run
287
+ # ... do work ...
288
+ DatabaseSetup.clean
289
+ ```
290
+
291
+ See [docs/guide.md](docs/guide.md#lifecycle-management) for details.
292
+
265
293
  ### Progress Display
266
294
 
267
295
  Tree-based progress visualization is enabled by default:
@@ -281,14 +309,27 @@ WebServer (Task)
281
309
  ✓ [5/5] All tasks completed (1234ms)
282
310
  ```
283
311
 
312
+ **Plain mode** provides text output without escape codes (for CI/logs):
313
+
314
+ ```
315
+ [START] DatabaseSetup
316
+ [DONE] DatabaseSetup (45.2ms)
317
+ [START] WebServer
318
+ [DONE] WebServer (120.5ms)
319
+ [TASKI] Completed: 2/2 tasks (165ms)
320
+ ```
321
+
284
322
  **Configuration:**
285
323
 
286
324
  ```ruby
287
325
  # Via API
288
- Taski.progress_mode = :simple # or :tree (default)
326
+ Taski.progress_mode = :tree # Tree display (default)
327
+ Taski.progress_mode = :simple # Single-line display
328
+ Taski.progress_mode = :plain # Plain text (CI/logs)
289
329
 
290
330
  # Via environment variable
291
331
  TASKI_PROGRESS_MODE=simple ruby your_script.rb
332
+ TASKI_PROGRESS_MODE=plain ruby your_script.rb
292
333
  ```
293
334
 
294
335
  To disable: `TASKI_PROGRESS_DISABLE=1 ruby your_script.rb`
data/docs/GUIDE.md CHANGED
@@ -324,20 +324,34 @@ On failure:
324
324
  ✗ [3/5] DeployTask failed: Connection refused
325
325
  ```
326
326
 
327
+ #### Plain Mode
328
+
329
+ Plain text output without escape codes, designed for CI/logs:
330
+
331
+ ```
332
+ [START] DatabaseSetup
333
+ [DONE] DatabaseSetup (45.2ms)
334
+ [START] WebServer
335
+ [DONE] WebServer (120.5ms)
336
+ [TASKI] Completed: 2/2 tasks (165ms)
337
+ ```
338
+
327
339
  ### Configuring Progress Mode
328
340
 
329
341
  **Via API:**
330
342
 
331
343
  ```ruby
332
- Taski.progress_mode = :simple # Use simple mode
333
- Taski.progress_mode = :tree # Use tree mode (default)
344
+ Taski.progress_mode = :tree # Tree display (default)
345
+ Taski.progress_mode = :simple # Single-line display
346
+ Taski.progress_mode = :plain # Plain text (CI/logs)
334
347
  ```
335
348
 
336
349
  **Via environment variable:**
337
350
 
338
351
  ```bash
339
- TASKI_PROGRESS_MODE=simple ruby your_script.rb
340
352
  TASKI_PROGRESS_MODE=tree ruby your_script.rb
353
+ TASKI_PROGRESS_MODE=simple ruby your_script.rb
354
+ TASKI_PROGRESS_MODE=plain ruby your_script.rb
341
355
  ```
342
356
 
343
357
  ### Disabling Progress Display
@@ -363,7 +377,7 @@ ruby build.rb > build.log 2>&1
363
377
  | Variable | Purpose |
364
378
  |----------|---------|
365
379
  | `TASKI_PROGRESS_DISABLE=1` | Disable progress display |
366
- | `TASKI_PROGRESS_MODE=simple\|tree` | Set progress display mode (default: tree) |
380
+ | `TASKI_PROGRESS_MODE=tree\|simple\|plain` | Set progress display mode (default: tree) |
367
381
  | `TASKI_DEBUG=1` | Enable debug output |
368
382
 
369
383
  ### Dependency Tree Visualization
data/examples/README.md CHANGED
@@ -67,78 +67,67 @@ TASKI_PROGRESS_DISABLE=1 ruby examples/reexecution_demo.rb
67
67
 
68
68
  ---
69
69
 
70
- ### 5. data_pipeline_demo.rb - Real-World Pipeline
70
+ ### 5. clean_demo.rb - Lifecycle Management
71
71
 
72
- A realistic ETL pipeline with parallel data fetching.
73
-
74
- ```bash
75
- ruby examples/data_pipeline_demo.rb
76
- ```
77
-
78
- **Covers:**
79
- - Multiple data sources in parallel
80
- - Data transformation stages
81
- - Aggregation and reporting
82
-
83
- ---
84
-
85
- ### 6. parallel_progress_demo.rb - Progress Display
86
-
87
- Real-time progress visualization during parallel execution.
72
+ Demonstrates resource cleanup with clean methods.
88
73
 
89
74
  ```bash
90
- ruby examples/parallel_progress_demo.rb
75
+ ruby examples/clean_demo.rb
91
76
  ```
92
77
 
93
78
  **Covers:**
94
- - Parallel task execution
95
- - Progress display with spinners
96
- - Execution timing
79
+ - Defining `clean` methods for resource cleanup
80
+ - Reverse dependency order execution
81
+ - `run_and_clean` combined operation
97
82
 
98
83
  ---
99
84
 
100
- ### 7. clean_demo.rb - Lifecycle Management
85
+ ### 6. group_demo.rb - Task Output Grouping
101
86
 
102
- Demonstrates resource cleanup with clean methods.
87
+ Organize task output into logical phases with groups.
103
88
 
104
89
  ```bash
105
- ruby examples/clean_demo.rb
90
+ ruby examples/group_demo.rb
106
91
  ```
107
92
 
108
93
  **Covers:**
109
- - Defining `clean` methods for resource cleanup
110
- - Reverse dependency order execution
111
- - `run_and_clean` combined operation
94
+ - `group("label") { ... }` for organizing output
95
+ - Groups displayed as children in progress tree
96
+ - Multiple groups within a single task
112
97
 
113
98
  ---
114
99
 
115
- ### 8. system_call_demo.rb - Subprocess Output
100
+ ### 7. message_demo.rb - User-Facing Messages
116
101
 
117
- Capture subprocess output in progress display.
102
+ Output messages that bypass the progress display capture.
118
103
 
119
104
  ```bash
120
- ruby examples/system_call_demo.rb
105
+ ruby examples/message_demo.rb
121
106
  ```
122
107
 
123
108
  **Covers:**
124
- - `system()` output capture
125
- - Streaming output display
126
- - Parallel subprocess execution
109
+ - `Taski.message(text)` for user-facing output
110
+ - Messages queued during progress and shown after completion
111
+ - Difference between `puts` (captured) and `Taski.message` (bypassed)
127
112
 
128
113
  ---
129
114
 
130
- ### 9. nested_section_demo.rb - Nested Sections
115
+ ### 8. progress_demo.rb - Progress Display Modes
131
116
 
132
- Sections that depend on other tasks for implementation selection.
117
+ Real-time progress visualization during parallel execution.
133
118
 
134
119
  ```bash
135
- TASKI_PROGRESS_DISABLE=1 ruby examples/nested_section_demo.rb
120
+ ruby examples/progress_demo.rb # Tree mode
121
+ TASKI_PROGRESS_MODE=simple ruby examples/progress_demo.rb # Simple mode
122
+ TASKI_PROGRESS_DISABLE=1 ruby examples/progress_demo.rb # Disabled
136
123
  ```
137
124
 
138
125
  **Covers:**
139
- - Section inside Section
140
- - Dynamic implementation selection
141
- - Complex dependency hierarchies
126
+ - Tree progress display (default)
127
+ - Simple one-line progress display
128
+ - Parallel task execution
129
+ - Task output capture and streaming
130
+ - system() output integration
142
131
 
143
132
  ---
144
133
 
@@ -148,13 +137,12 @@ TASKI_PROGRESS_DISABLE=1 ruby examples/nested_section_demo.rb
148
137
  |---------|---------|------------|
149
138
  | quick_start | Exports API | Basic |
150
139
  | section_demo | Section API | Intermediate |
151
- | args_demo | Args API | Intermediate |
140
+ | args_demo | Args/Env API | Intermediate |
152
141
  | reexecution_demo | Scope-Based Execution | Intermediate |
153
- | data_pipeline_demo | ETL Pipeline | Advanced |
154
- | parallel_progress_demo | Progress Display | Advanced |
155
142
  | clean_demo | Lifecycle Management | Intermediate |
156
- | system_call_demo | Subprocess Output | Advanced |
157
- | nested_section_demo | Nested Sections | Advanced |
143
+ | group_demo | Output Grouping | Intermediate |
144
+ | message_demo | User Messages | Basic |
145
+ | progress_demo | Progress Display | Advanced |
158
146
 
159
147
  ## Running All Examples
160
148
 
@@ -163,7 +151,7 @@ TASKI_PROGRESS_DISABLE=1 ruby examples/nested_section_demo.rb
163
151
  for f in examples/*.rb; do echo "=== $f ===" && ruby "$f" && echo; done
164
152
 
165
153
  # Disable progress display if needed
166
- TASKI_PROGRESS_DISABLE=1 ruby examples/parallel_progress_demo.rb
154
+ TASKI_PROGRESS_DISABLE=1 ruby examples/progress_demo.rb
167
155
  ```
168
156
 
169
157
  ## Next Steps
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Taski Progress Display Demo
5
+ #
6
+ # This example demonstrates the progress display modes:
7
+ # - Tree mode (default): Shows task hierarchy with real-time updates
8
+ # - Simple mode: One-line spinner with current task name
9
+ #
10
+ # Run:
11
+ # ruby examples/progress_demo.rb # Tree mode (default)
12
+ # TASKI_PROGRESS_MODE=simple ruby examples/progress_demo.rb # Simple mode
13
+ # TASKI_PROGRESS_DISABLE=1 ruby examples/progress_demo.rb # No progress
14
+ #
15
+ # Covers:
16
+ # - Parallel task execution with progress display
17
+ # - Tree vs simple progress modes
18
+ # - Task output capture and display
19
+ # - system() output streaming
20
+
21
+ require_relative "../lib/taski"
22
+
23
+ # Configuration section with multiple implementations
24
+ class DatabaseSection < Taski::Section
25
+ interfaces :connection_string
26
+
27
+ def impl
28
+ (ENV["USE_PROD_DB"] == "1") ? ProductionDB : DevelopmentDB
29
+ end
30
+
31
+ class ProductionDB < Taski::Task
32
+ def run
33
+ puts "Connecting to production database..."
34
+ sleep(0.4)
35
+ @connection_string = "postgresql://prod-server:5432/myapp"
36
+ end
37
+ end
38
+
39
+ class DevelopmentDB < Taski::Task
40
+ def run
41
+ puts "Connecting to development database..."
42
+ sleep(0.3)
43
+ @connection_string = "postgresql://localhost:5432/myapp_dev"
44
+ end
45
+ end
46
+ end
47
+
48
+ # Parallel download tasks (executed concurrently)
49
+ class DownloadLayer1 < Taski::Task
50
+ exports :layer1_data
51
+
52
+ def run
53
+ puts "Downloading base image..."
54
+ sleep(0.8)
55
+ puts "Base image complete"
56
+ @layer1_data = "Layer 1 (base)"
57
+ end
58
+ end
59
+
60
+ class DownloadLayer2 < Taski::Task
61
+ exports :layer2_data
62
+
63
+ def run
64
+ puts "Downloading dependencies..."
65
+ sleep(1.2)
66
+ puts "Dependencies complete"
67
+ @layer2_data = "Layer 2 (deps)"
68
+ end
69
+ end
70
+
71
+ class DownloadLayer3 < Taski::Task
72
+ exports :layer3_data
73
+
74
+ def run
75
+ puts "Downloading application..."
76
+ sleep(0.4)
77
+ puts "Application complete"
78
+ @layer3_data = "Layer 3 (app)"
79
+ end
80
+ end
81
+
82
+ # Task that depends on all downloads (waits for parallel completion)
83
+ class ExtractLayers < Taski::Task
84
+ exports :extracted_data
85
+
86
+ def run
87
+ layer1 = DownloadLayer1.layer1_data
88
+ layer2 = DownloadLayer2.layer2_data
89
+ layer3 = DownloadLayer3.layer3_data
90
+
91
+ puts "Extracting layers..."
92
+ sleep(0.3)
93
+ @extracted_data = [layer1, layer2, layer3]
94
+ end
95
+ end
96
+
97
+ # Task demonstrating system() output capture
98
+ class RunSystemCommand < Taski::Task
99
+ exports :command_result
100
+
101
+ def run
102
+ puts "Running system command..."
103
+ @command_result = system("echo 'Step 1: Preparing...' && sleep 0.2 && echo 'Step 2: Processing...' && sleep 0.2 && echo 'Step 3: Done'")
104
+ end
105
+ end
106
+
107
+ # Final task combining all dependencies
108
+ class BuildApplication < Taski::Task
109
+ exports :result
110
+
111
+ def run
112
+ db = DatabaseSection.connection_string
113
+ layers = ExtractLayers.extracted_data
114
+ RunSystemCommand.command_result
115
+
116
+ puts "Building application..."
117
+ sleep(0.3)
118
+ puts "Finalizing build..."
119
+ sleep(0.2)
120
+
121
+ @result = {
122
+ database: db,
123
+ layers: layers,
124
+ status: "success"
125
+ }
126
+ end
127
+ end
128
+
129
+ # Main execution
130
+ puts "Taski Progress Display Demo"
131
+ puts "=" * 50
132
+ puts "Progress mode: #{Taski.progress_mode || "tree (default)"}"
133
+ puts
134
+
135
+ puts "Task Tree Structure:"
136
+ puts "-" * 50
137
+ puts BuildApplication.tree
138
+ puts "-" * 50
139
+ puts
140
+
141
+ # Reset for fresh execution
142
+ BuildApplication.reset!
143
+
144
+ # Execute (progress display is automatic)
145
+ result = BuildApplication.result
146
+
147
+ puts
148
+ puts "=" * 50
149
+ puts "Execution completed!"
150
+ puts "Result: #{result.inspect}"
151
+ puts
152
+ puts "Try different modes:"
153
+ puts " TASKI_PROGRESS_MODE=simple ruby examples/progress_demo.rb"
154
+ puts " TASKI_PROGRESS_DISABLE=1 ruby examples/progress_demo.rb"
@@ -85,6 +85,7 @@ module Taski
85
85
  @root_task_class = nil
86
86
  @output_capture = nil
87
87
  @start_time = nil
88
+ @message_queue = []
88
89
  end
89
90
 
90
91
  # Set the output capture for getting task output
@@ -199,6 +200,14 @@ module Taski
199
200
  return unless should_stop
200
201
 
201
202
  on_stop
203
+ flush_queued_messages
204
+ end
205
+
206
+ # Queue a message to be displayed after progress display stops.
207
+ # Thread-safe for concurrent task execution.
208
+ # @param text [String] The message text to queue
209
+ def queue_message(text)
210
+ @monitor.synchronize { @message_queue << text }
202
211
  end
203
212
 
204
213
  protected
@@ -281,15 +290,47 @@ module Taski
281
290
  @output.tty?
282
291
  end
283
292
 
293
+ # Collect all dependencies of a task class recursively
294
+ # Useful for determining which tasks are needed by a selected implementation
295
+ # @param task_class [Class] The task class to collect dependencies for
296
+ # @return [Set<Class>] Set of all dependency task classes (including the task itself)
297
+ def collect_all_dependencies(task_class)
298
+ deps = Set.new
299
+ collect_dependencies_recursive(task_class, deps)
300
+ deps
301
+ end
302
+
284
303
  private
285
304
 
305
+ # Recursively collect dependencies into the given set
306
+ # @param task_class [Class] The task class
307
+ # @param collected [Set<Class>] Accumulated dependencies
308
+ def collect_dependencies_recursive(task_class, collected)
309
+ return if collected.include?(task_class)
310
+ collected.add(task_class)
311
+
312
+ task_class.cached_dependencies.each do |dep|
313
+ collect_dependencies_recursive(dep, collected)
314
+ end
315
+ end
316
+
317
+ # Flush all queued messages to output.
318
+ # Called when progress display stops.
319
+ def flush_queued_messages
320
+ messages = @monitor.synchronize { @message_queue.dup.tap { @message_queue.clear } }
321
+ messages.each { |msg| @output.puts(msg) }
322
+ end
323
+
286
324
  # Apply state transition to TaskProgress
325
+ # Note: Once a task reaches :completed or :failed, it cannot go back to :running.
326
+ # This prevents progress count from decreasing when nested executors re-execute tasks.
287
327
  def apply_state_transition(progress, state, duration, error)
288
328
  case state
289
329
  # Run lifecycle states
290
330
  when :pending
291
331
  progress.run_state = :pending
292
332
  when :running
333
+ return if run_state_finalized?(progress)
293
334
  progress.run_state = :running
294
335
  progress.run_start_time = Time.now
295
336
  when :completed
@@ -315,6 +356,10 @@ module Taski
315
356
  end
316
357
  end
317
358
 
359
+ def run_state_finalized?(progress)
360
+ progress.run_state == :completed || progress.run_state == :failed
361
+ end
362
+
318
363
  # Apply state transition to GroupProgress
319
364
  def apply_group_state_transition(progress, group_name, state, duration, error)
320
365
  case state
@@ -81,7 +81,6 @@ module Taski
81
81
  @output_capture = nil
82
82
  @original_stdout = nil
83
83
  @runtime_dependencies = {}
84
- @message_queue = []
85
84
  end
86
85
 
87
86
  # Check if output capture is already active.
@@ -90,23 +89,6 @@ module Taski
90
89
  @monitor.synchronize { !@output_capture.nil? }
91
90
  end
92
91
 
93
- # Queue a message to be displayed after execution completes.
94
- # Thread-safe for access from worker threads.
95
- #
96
- # @param text [String] The message text to queue
97
- def queue_message(text)
98
- @monitor.synchronize { @message_queue << text }
99
- end
100
-
101
- # Flush all queued messages to the given output.
102
- # Clears the queue after flushing.
103
- #
104
- # @param output [IO] The output stream to write messages to
105
- def flush_messages(output)
106
- messages = @monitor.synchronize { @message_queue.dup.tap { @message_queue.clear } }
107
- messages.each { |msg| output.puts(msg) }
108
- end
109
-
110
92
  # Get the original stdout before output capture was set up.
111
93
  # Thread-safe accessor.
112
94
  #
@@ -123,7 +105,7 @@ module Taski
123
105
  def setup_output_capture(output_io)
124
106
  @monitor.synchronize do
125
107
  @original_stdout = output_io
126
- @output_capture = TaskOutputRouter.new(@original_stdout)
108
+ @output_capture = TaskOutputRouter.new(@original_stdout, self)
127
109
  @output_capture.start_polling
128
110
  $stdout = @output_capture
129
111
  end
@@ -321,8 +303,7 @@ module Taski
321
303
  dispatch(:start)
322
304
  end
323
305
 
324
- ##
325
- # Notify registered observers that execution has stopped.
306
+ # Notify observers to stop.
326
307
  def notify_stop
327
308
  dispatch(:stop)
328
309
  end
@@ -418,17 +418,9 @@ module Taski
418
418
  ensure
419
419
  stop_progress_display
420
420
  @saved_output_capture = @execution_context.output_capture
421
- flush_queued_messages if should_teardown_capture
422
421
  teardown_output_capture if should_teardown_capture
423
422
  end
424
423
 
425
- # Flush queued messages from ExecutionContext to original stdout.
426
- # Called after progress display stops to show user messages.
427
- def flush_queued_messages
428
- output = @execution_context.original_stdout || $stdout
429
- @execution_context.flush_messages(output)
430
- end
431
-
432
424
  def create_default_execution_context
433
425
  context = ExecutionContext.new
434
426
  progress = Taski.progress_display