job-workflow 0.2.0 → 0.3.0

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: 9ac9f67bfd9ba71b60f98643a5f5ed720423239650bdf80a8bb07de594d9b502
4
- data.tar.gz: 6902b370e54d0285721390a40af5ff0a7b459c6ce0a67af6c538dc1877f0df85
3
+ metadata.gz: 2a4a82b69372293b7bf689c33125b60b4cbdbcdf4f2e1f468e205cf9bf0c1d85
4
+ data.tar.gz: e2e6e91167d6127e6cee7e2af0339f79ffee244fe6e768d73e50ebb6c2e7b372
5
5
  SHA512:
6
- metadata.gz: 6c24f513d3c1192513fef902a92d844050a25e095b2739e9f40e774639cde3fb75c4df1a6e2878a50f13583e5df0e665faa6ef803a434e9444d020a81ec2a71b
7
- data.tar.gz: 1c7218d7a1c2d45444a111956bd26d152f16b2e1c6f86fb2959fd255c2ab844444d7929863149f3c7e4322073d520ff40ec3ea02ec29a1595135350723ed4a6a
6
+ metadata.gz: 84356b092c371e103d601576f9fc6ed5cdee9025340948b0d1fe8222b5b1f49f301342f8254a0d858fc89da58e8f828241d7d8ec706103349e63d6f68a426945
7
+ data.tar.gz: b146df16edb3112fa10379437e03ebd6a7f74c46e3a83526bb195320e141e9493319caa96d601622cf1e2f5d8cf18b071cc56c93aac1ad39492b89cfa8e08392
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-03-13
4
+
5
+ ### Added
6
+
7
+ - Add `fetch_job_contexts(job_ids)` to queue adapter interface (`Abstract`, `SolidQueueAdapter`, `NullAdapter`) for fetching sub-job context data without direct `SolidQueue::Job` dependency from domain classes
8
+ - Add `persist_job_context(job)` to queue adapter interface for persisting task outputs back to SolidQueue job records after execution
9
+ - Add `without_query_cache` private helper to `SolidQueueAdapter` to bypass ActiveRecord query cache during polling queries
10
+ - Add `"job_workflow_context"` key to `find_job` return hash for direct access to workflow context data
11
+ - Add `AcceptanceNoDependencyWaitJob` and acceptance tests for `depends_on` without `dependency_wait` (polling-only mode)
12
+ - Add acceptance test for output aggregation verification in async workflows
13
+
14
+ ### Changed
15
+
16
+ - **Breaking (internal):** Replace `Output#update_task_outputs_from_db` and `Output#update_task_outputs_from_jobs` with `Output#update_task_outputs_from_contexts` — callers now pass context data hashes instead of `SolidQueue::Job` objects
17
+ - `Runner#update_task_outputs` now routes through `QueueAdapter.current.fetch_job_contexts` instead of directly querying `SolidQueue::Job`
18
+ - `Runner#run` now calls `QueueAdapter.current.persist_job_context(job)` after both sub-job and workflow execution
19
+ - `WorkflowStatus.from_job_data` now reads `job_workflow_context` from top-level data first, falling back to `arguments.first.dig("job_workflow_context")`
20
+ - `reschedule_solid_queue_job` now saves full serialized job hash (`active_job.serialize.deep_stringify_keys`) instead of only `["arguments"]`
21
+ - Wrap `find_job`, `fetch_job_statuses`, `job_status`, `reschedule_job`, and `fetch_job_contexts` with `without_query_cache` to prevent stale reads under SolidQueue executor
22
+
3
23
  ## [0.2.0] - 2026-03-12
4
24
 
5
25
  ### Added
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # JobWorkflow
2
2
 
3
- > ⚠️ **Early Stage (v0.1.3):** This library is in active development. APIs and features may change in breaking ways without notice. Use in production at your own risk and expect potential breaking changes in future releases.
3
+ > ⚠️ **Early Stage (v0.3.0):** This library is in active development. APIs and features may change in breaking ways without notice. Use in production at your own risk and expect potential breaking changes in future releases.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Production Deployment
2
2
 
3
- > ⚠️ **Early Stage (v0.1.3):** JobWorkflow is still in early development. While this section outlines potential deployment patterns, please thoroughly test in your specific environment and monitor for any issues before relying on JobWorkflow in critical production systems.
3
+ > ⚠️ **Early Stage (v0.3.0):** JobWorkflow is still in early development. While this section outlines potential deployment patterns, please thoroughly test in your specific environment and monitor for any issues before relying on JobWorkflow in critical production systems.
4
4
 
5
5
  This section covers suggested settings and patterns for running JobWorkflow in production-like environments.
6
6
 
data/guides/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # JobWorkflow Guides
2
2
 
3
- > ⚠️ **Early Stage (v0.1.3):** JobWorkflow is in active development. APIs and features may change. The following guides provide patterns and examples for building workflows, but be aware that implementations may need adjustment as the library evolves.
3
+ > ⚠️ **Early Stage (v0.3.0):** JobWorkflow is in active development. APIs and features may change. The following guides provide patterns and examples for building workflows, but be aware that implementations may need adjustment as the library evolves.
4
4
 
5
5
  Welcome to the JobWorkflow documentation! This directory contains comprehensive guides to help you build robust workflows with JobWorkflow.
6
6
 
@@ -50,18 +50,10 @@ module JobWorkflow
50
50
  task_outputs[task_output.task_name][task_output.each_index] = task_output
51
51
  end
52
52
 
53
- #: (Array[String], Workflow) -> void
54
- def update_task_outputs_from_db(job_ids, workflow)
55
- jobs = SolidQueue::Job.where(active_job_id: job_ids)
56
- return if jobs.empty?
57
-
58
- update_task_outputs_from_jobs(jobs.to_a, workflow)
59
- end
60
-
61
- #: (Array[SolidQueue::Job], Workflow) -> void
62
- def update_task_outputs_from_jobs(jobs, workflow)
63
- jobs.each do |job|
64
- context = Context.deserialize(job.arguments["job_workflow_context"].merge("workflow" => workflow))
53
+ #: (Array[Hash[String, untyped]], Workflow) -> void
54
+ def update_task_outputs_from_contexts(context_data_list, workflow)
55
+ context_data_list.each do |context_data|
56
+ context = Context.deserialize(context_data.merge("workflow" => workflow))
65
57
  task_output = context.each_task_output
66
58
  next if task_output.nil?
67
59
 
@@ -77,10 +77,18 @@ module JobWorkflow
77
77
  raise NotImplementedError, "#{self.class}#find_job must be implemented"
78
78
  end
79
79
 
80
+ #: (Array[String]) -> Array[Hash[String, untyped]]
81
+ def fetch_job_contexts(_job_ids)
82
+ raise NotImplementedError, "#{self.class}#fetch_job_contexts must be implemented"
83
+ end
84
+
80
85
  #: (DSL, Numeric) -> bool
81
86
  def reschedule_job(_job, _wait)
82
87
  false
83
88
  end
89
+
90
+ #: (DSL) -> void
91
+ def persist_job_context(_job); end
84
92
  end
85
93
  # rubocop:enable Naming/PredicateMethod
86
94
  end
@@ -90,6 +90,11 @@ module JobWorkflow
90
90
  @stored_jobs[job_id]
91
91
  end
92
92
 
93
+ #: (Array[String]) -> Array[Hash[String, untyped]]
94
+ def fetch_job_contexts(_job_ids)
95
+ []
96
+ end
97
+
93
98
  #: (DSL, Numeric) -> bool
94
99
  def reschedule_job(_job, _wait)
95
100
  false
@@ -2,7 +2,7 @@
2
2
 
3
3
  module JobWorkflow
4
4
  module QueueAdapters
5
- # rubocop:disable Naming/PredicateMethod
5
+ # rubocop:disable Naming/PredicateMethod, Metrics/ClassLength
6
6
  class SolidQueueAdapter < Abstract
7
7
  # @note
8
8
  # - Registry scope: @semaphore_registry is process-scoped (shared across fibers/threads
@@ -74,16 +74,20 @@ module JobWorkflow
74
74
  def fetch_job_statuses(job_ids)
75
75
  return {} unless defined?(SolidQueue::Job)
76
76
 
77
- SolidQueue::Job.where(active_job_id: job_ids).index_by(&:active_job_id)
77
+ without_query_cache do
78
+ SolidQueue::Job.where(active_job_id: job_ids).index_by(&:active_job_id)
79
+ end
78
80
  end
79
81
 
80
82
  #: (untyped) -> Symbol
81
83
  def job_status(job)
82
- return :failed if job.failed?
83
- return :succeeded if job.finished?
84
- return :running if job.claimed?
84
+ without_query_cache do
85
+ return :failed if job.failed?
86
+ return :succeeded if job.finished?
87
+ return :running if job.claimed?
85
88
 
86
- :pending
89
+ :pending
90
+ end
87
91
  end
88
92
 
89
93
  #: () -> bool
@@ -153,23 +157,40 @@ module JobWorkflow
153
157
  def find_job(job_id)
154
158
  return unless defined?(SolidQueue::Job)
155
159
 
156
- job = SolidQueue::Job.find_by(active_job_id: job_id)
160
+ job = without_query_cache { SolidQueue::Job.find_by(active_job_id: job_id) }
157
161
  return if job.nil?
158
162
 
163
+ args = job.arguments
159
164
  {
160
165
  "job_id" => job.active_job_id,
161
166
  "class_name" => job.class_name,
162
167
  "queue_name" => job.queue_name,
163
- "arguments" => job.arguments.is_a?(Hash) ? job.arguments["arguments"] : job.arguments,
168
+ "arguments" => args.is_a?(Hash) ? args["arguments"] : args,
169
+ "job_workflow_context" => args.is_a?(Hash) ? args["job_workflow_context"] : nil,
164
170
  "status" => job_status(job)
165
171
  }
166
172
  end
167
173
 
174
+ # @note
175
+ # - Fetches job_workflow_context hashes for the given job IDs.
176
+ #
177
+ #: (Array[String]) -> Array[Hash[String, untyped]]
178
+ def fetch_job_contexts(job_ids)
179
+ return [] unless defined?(SolidQueue::Job)
180
+ return [] if job_ids.empty?
181
+
182
+ jobs = without_query_cache { SolidQueue::Job.where(active_job_id: job_ids).to_a }
183
+ jobs.filter_map do |job|
184
+ args = job.arguments
185
+ args.is_a?(Hash) ? args["job_workflow_context"] : nil
186
+ end
187
+ end
188
+
168
189
  #: (DSL, Numeric) -> bool
169
190
  def reschedule_job(job, wait)
170
191
  return false unless defined?(SolidQueue::Job)
171
192
 
172
- solid_queue_job = SolidQueue::Job.find_by(active_job_id: job.job_id)
193
+ solid_queue_job = without_query_cache { SolidQueue::Job.find_by(active_job_id: job.job_id) }
173
194
  return false unless solid_queue_job&.claimed?
174
195
 
175
196
  reschedule_solid_queue_job(solid_queue_job, job, wait)
@@ -177,17 +198,44 @@ module JobWorkflow
177
198
  false
178
199
  end
179
200
 
201
+ # @note
202
+ # - Persists the job's updated context (including task outputs) back
203
+ # to the SolidQueue job record after execution completes. Without this,
204
+ # outputs computed during job execution would be lost because
205
+ # SolidQueue does not re-serialize job arguments after perform.
206
+ #
207
+ #: (DSL) -> void
208
+ def persist_job_context(job)
209
+ return unless defined?(SolidQueue::Job)
210
+
211
+ solid_queue_job = SolidQueue::Job.find_by(active_job_id: job.job_id)
212
+ return if solid_queue_job.nil?
213
+
214
+ solid_queue_job.update!(arguments: job.serialize.deep_stringify_keys)
215
+ end
216
+
180
217
  private
181
218
 
182
219
  attr_reader :semaphore_registry #: Hash[Object, ^(SolidQueue::Worker) -> void]
183
220
 
221
+ # @note
222
+ # - Bypasses ActiveRecord query cache for the given block.
223
+ # - When running under SolidQueue's executor, SELECT queries are cached
224
+ # for the entire job execution. Polling queries must bypass this cache
225
+ # to observe status changes made by other threads/processes.
226
+ #
227
+ #: [T] () { () -> T } -> T
228
+ def without_query_cache(&)
229
+ defined?(SolidQueue::Job) ? SolidQueue::Job.uncached(&) : yield
230
+ end
231
+
184
232
  #: (SolidQueue::Job, DSL, Numeric) -> bool
185
233
  def reschedule_solid_queue_job(solid_queue_job, active_job, wait)
186
234
  solid_queue_job.with_lock do
187
235
  solid_queue_job.claimed_execution&.destroy!
188
236
  solid_queue_job.update!(
189
237
  scheduled_at: wait.seconds.from_now,
190
- arguments: active_job.serialize.deep_stringify_keys["arguments"]
238
+ arguments: active_job.serialize.deep_stringify_keys
191
239
  )
192
240
  solid_queue_job.prepare_for_execution
193
241
  end
@@ -219,6 +267,6 @@ module JobWorkflow
219
267
  end
220
268
  end
221
269
  end
222
- # rubocop:enable Naming/PredicateMethod
270
+ # rubocop:enable Naming/PredicateMethod, Metrics/ClassLength
223
271
  end
224
272
  end
@@ -13,9 +13,14 @@ module JobWorkflow
13
13
  #: () -> void
14
14
  def run
15
15
  task = context._task_context.task
16
- return run_task(task) if !task.nil? && context.sub_job?
16
+ if !task.nil? && context.sub_job?
17
+ run_task(task)
18
+ QueueAdapter.current.persist_job_context(job)
19
+ return
20
+ end
17
21
 
18
22
  catch(:rescheduled) { run_workflow }
23
+ QueueAdapter.current.persist_job_context(job)
19
24
  end
20
25
 
21
26
  private
@@ -167,7 +172,8 @@ module JobWorkflow
167
172
  #: (Task) -> void
168
173
  def update_task_outputs(task)
169
174
  finished_job_ids = context.job_status.finished_job_ids(task_name: task.task_name)
170
- context.output.update_task_outputs_from_db(finished_job_ids, context.workflow)
175
+ context_data_list = QueueAdapter.current.fetch_job_contexts(finished_job_ids)
176
+ context.output.update_task_outputs_from_contexts(context_data_list, context.workflow)
171
177
  end
172
178
  end
173
179
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JobWorkflow
4
- VERSION = "0.2.0" # : String
4
+ VERSION = "0.3.0" # : String
5
5
  end
@@ -34,7 +34,7 @@ module JobWorkflow
34
34
  job_class = job_class_name.constantize
35
35
  workflow = job_class._workflow
36
36
 
37
- context_data = data["arguments"].first["job_workflow_context"]
37
+ context_data = data["job_workflow_context"] || data["arguments"]&.first&.dig("job_workflow_context")
38
38
  context = if context_data
39
39
  Context.deserialize(context_data.merge("workflow" => workflow))
40
40
  else
@@ -6,7 +6,7 @@ gems:
6
6
  source:
7
7
  type: git
8
8
  name: ruby/gem_rbs_collection
9
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
9
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
10
10
  remote: https://github.com/ruby/gem_rbs_collection.git
11
11
  repo_dir: gems
12
12
  - name: activerecord
@@ -14,7 +14,7 @@ gems:
14
14
  source:
15
15
  type: git
16
16
  name: ruby/gem_rbs_collection
17
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
17
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
18
18
  remote: https://github.com/ruby/gem_rbs_collection.git
19
19
  repo_dir: gems
20
20
  - name: activesupport
@@ -22,7 +22,7 @@ gems:
22
22
  source:
23
23
  type: git
24
24
  name: ruby/gem_rbs_collection
25
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
25
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
26
26
  remote: https://github.com/ruby/gem_rbs_collection.git
27
27
  repo_dir: gems
28
28
  - name: base64
@@ -34,7 +34,7 @@ gems:
34
34
  source:
35
35
  type: git
36
36
  name: ruby/gem_rbs_collection
37
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
37
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
38
38
  remote: https://github.com/ruby/gem_rbs_collection.git
39
39
  repo_dir: gems
40
40
  - name: concurrent-ruby
@@ -42,7 +42,7 @@ gems:
42
42
  source:
43
43
  type: git
44
44
  name: ruby/gem_rbs_collection
45
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
45
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
46
46
  remote: https://github.com/ruby/gem_rbs_collection.git
47
47
  repo_dir: gems
48
48
  - name: connection_pool
@@ -50,7 +50,7 @@ gems:
50
50
  source:
51
51
  type: git
52
52
  name: ruby/gem_rbs_collection
53
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
53
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
54
54
  remote: https://github.com/ruby/gem_rbs_collection.git
55
55
  repo_dir: gems
56
56
  - name: date
@@ -74,7 +74,7 @@ gems:
74
74
  source:
75
75
  type: git
76
76
  name: ruby/gem_rbs_collection
77
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
77
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
78
78
  remote: https://github.com/ruby/gem_rbs_collection.git
79
79
  repo_dir: gems
80
80
  - name: i18n
@@ -82,7 +82,7 @@ gems:
82
82
  source:
83
83
  type: git
84
84
  name: ruby/gem_rbs_collection
85
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
85
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
86
86
  remote: https://github.com/ruby/gem_rbs_collection.git
87
87
  repo_dir: gems
88
88
  - name: json
@@ -98,7 +98,7 @@ gems:
98
98
  source:
99
99
  type: git
100
100
  name: ruby/gem_rbs_collection
101
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
101
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
102
102
  remote: https://github.com/ruby/gem_rbs_collection.git
103
103
  repo_dir: gems
104
104
  - name: monitor
@@ -138,7 +138,7 @@ gems:
138
138
  source:
139
139
  type: git
140
140
  name: ruby/gem_rbs_collection
141
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
141
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
142
142
  remote: https://github.com/ruby/gem_rbs_collection.git
143
143
  repo_dir: gems
144
144
  - name: stringio
@@ -162,7 +162,7 @@ gems:
162
162
  source:
163
163
  type: git
164
164
  name: ruby/gem_rbs_collection
165
- revision: 2af46cde7fec5e4130f6c84c5b691c0538af8789
165
+ revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
166
166
  remote: https://github.com/ruby/gem_rbs_collection.git
167
167
  repo_dir: gems
168
168
  - name: uri
@@ -23,11 +23,8 @@ module JobWorkflow
23
23
  # : (TaskOutput) -> void
24
24
  def add_task_output: (TaskOutput) -> void
25
25
 
26
- # : (Array[String], Workflow) -> void
27
- def update_task_outputs_from_db: (Array[String], Workflow) -> void
28
-
29
- # : (Array[SolidQueue::Job], Workflow) -> void
30
- def update_task_outputs_from_jobs: (Array[SolidQueue::Job], Workflow) -> void
26
+ # : (Array[Hash[String, untyped]], Workflow) -> void
27
+ def update_task_outputs_from_contexts: (Array[Hash[String, untyped]], Workflow) -> void
31
28
 
32
29
  # : () -> Array[TaskOutput]
33
30
  def flat_task_outputs: () -> Array[TaskOutput]
@@ -49,8 +49,14 @@ module JobWorkflow
49
49
  # : (String) -> Hash[String, untyped]?
50
50
  def find_job: (String) -> Hash[String, untyped]?
51
51
 
52
+ # : (Array[String]) -> Array[Hash[String, untyped]]
53
+ def fetch_job_contexts: (Array[String]) -> Array[Hash[String, untyped]]
54
+
52
55
  # : (DSL, Numeric) -> bool
53
56
  def reschedule_job: (DSL, Numeric) -> bool
57
+
58
+ # : (DSL) -> void
59
+ def persist_job_context: (DSL) -> void
54
60
  end
55
61
  end
56
62
  end
@@ -51,6 +51,9 @@ module JobWorkflow
51
51
  # : (String) -> Hash[String, untyped]?
52
52
  def find_job: (String) -> Hash[String, untyped]?
53
53
 
54
+ # : (Array[String]) -> Array[Hash[String, untyped]]
55
+ def fetch_job_contexts: (Array[String]) -> Array[Hash[String, untyped]]
56
+
54
57
  # : (DSL, Numeric) -> bool
55
58
  def reschedule_job: (DSL, Numeric) -> bool
56
59
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module JobWorkflow
4
4
  module QueueAdapters
5
- # rubocop:disable Naming/PredicateMethod
5
+ # rubocop:disable Naming/PredicateMethod, Metrics/ClassLength
6
6
  class SolidQueueAdapter < Abstract
7
7
  # @note
8
8
  # - Registry scope: @semaphore_registry is process-scoped (shared across fibers/threads
@@ -82,13 +82,37 @@ module JobWorkflow
82
82
  # : (String) -> Hash[String, untyped]?
83
83
  def find_job: (String) -> Hash[String, untyped]?
84
84
 
85
+ # @note
86
+ # - Fetches job_workflow_context hashes for the given job IDs.
87
+ #
88
+ # : (Array[String]) -> Array[Hash[String, untyped]]
89
+ def fetch_job_contexts: (Array[String]) -> Array[Hash[String, untyped]]
90
+
85
91
  # : (DSL, Numeric) -> bool
86
92
  def reschedule_job: (DSL, Numeric) -> bool
87
93
 
94
+ # @note
95
+ # - Persists the job's updated context (including task outputs) back
96
+ # to the SolidQueue job record after execution completes. Without this,
97
+ # outputs computed during job execution would be lost because
98
+ # SolidQueue does not re-serialize job arguments after perform.
99
+ #
100
+ # : (DSL) -> void
101
+ def persist_job_context: (DSL) -> void
102
+
88
103
  private
89
104
 
90
105
  attr_reader semaphore_registry: Hash[Object, ^(SolidQueue::Worker) -> void]
91
106
 
107
+ # @note
108
+ # - Bypasses ActiveRecord query cache for the given block.
109
+ # - When running under SolidQueue's executor, SELECT queries are cached
110
+ # for the entire job execution. Polling queries must bypass this cache
111
+ # to observe status changes made by other threads/processes.
112
+ #
113
+ # : [T] () { () -> T } -> T
114
+ def without_query_cache: [T] () { () -> T } -> T
115
+
92
116
  # : (SolidQueue::Job, DSL, Numeric) -> bool
93
117
  def reschedule_solid_queue_job: (SolidQueue::Job, DSL, Numeric) -> bool
94
118
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: job-workflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - shoma07