geneva_drive 0.7.0 → 0.8.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/MANUAL.md +84 -1
  4. data/Rakefile +14 -1
  5. data/bin/matrix +76 -0
  6. data/bin/md2html +114 -0
  7. data/lib/generators/geneva_drive/install/templates/add_resumable_step_support.rb +5 -9
  8. data/lib/generators/geneva_drive/install/templates/add_signals_support.rb +5 -10
  9. data/lib/geneva_drive/executor.rb +18 -0
  10. data/lib/geneva_drive/json_column.rb +72 -0
  11. data/lib/geneva_drive/migration_helpers.rb +27 -0
  12. data/lib/geneva_drive/removed_step_definition.rb +34 -0
  13. data/lib/geneva_drive/signal.rb +4 -2
  14. data/lib/geneva_drive/step_collection.rb +31 -7
  15. data/lib/geneva_drive/step_definition.rb +9 -0
  16. data/lib/geneva_drive/step_execution.rb +3 -2
  17. data/lib/geneva_drive/version.rb +1 -1
  18. data/lib/geneva_drive/workflow.rb +170 -2
  19. data/lib/geneva_drive.rb +2 -0
  20. data/test/dummy/config/database.yml +40 -5
  21. data/test/dummy/config/initializers/geneva_drive.rb +46 -0
  22. data/test/dummy_install/db/migrate/20260217112949_create_geneva_drive_workflows.rb +76 -0
  23. data/test/dummy_install/db/migrate/20260217112950_create_geneva_drive_step_executions.rb +99 -0
  24. data/test/dummy_install/db/migrate/20260217112951_add_finished_at_to_geneva_drive_step_executions.rb +27 -0
  25. data/test/dummy_install/db/migrate/20260217112952_add_error_class_name_to_geneva_drive_step_executions.rb +9 -0
  26. data/test/dummy_install/db/migrate/20260217112953_add_resumable_step_support_to_geneva_drive_step_executions.rb +128 -0
  27. data/test/dummy_install/db/migrate/20260217112954_add_chained_step_executions_to_geneva_drive_step_executions.rb +136 -0
  28. data/test/dummy_install/db/schema.rb +75 -0
  29. data/test/dummy_install/log/test.log +1852 -0
  30. data/test/test_helper.rb +51 -13
  31. data/test/workflow/removed_step_test.rb +294 -0
  32. data/test/workflow/resume_at_test.rb +170 -0
  33. metadata +16 -2
data/test/test_helper.rb CHANGED
@@ -7,29 +7,52 @@ require_relative "../test/dummy/config/environment"
7
7
  ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
8
8
  ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__)
9
9
 
10
- # Ensure GenevaDrive migrations exist and database is prepared.
11
- # The generator is the single source of truth for migrations.
10
+ # Bring the dummy app's database in line with the migrations the install
11
+ # generator produces, recreating it from scratch whenever it has drifted.
12
+ #
13
+ # Rebuilding rather than migrating forward is deliberate. The generated
14
+ # migrations are gitignored and get fresh timestamps every time they are
15
+ # regenerated, so a database left behind by an earlier generation records
16
+ # versions that no longer exist on disk: every migration reads as pending, and
17
+ # replaying them onto the existing tables just fails on "table already exists".
12
18
  unless defined?(GENEVA_DRIVE_TEST_DB_PREPARED)
13
19
  GENEVA_DRIVE_TEST_DB_PREPARED = true
14
20
 
15
21
  dummy_root = File.expand_path("../test/dummy", __dir__)
16
- migrations = Dir.glob("#{dummy_root}/db/migrate/*geneva_drive*.rb")
17
- tables_exist = begin
18
- ActiveRecord::Base.connection.table_exists?("geneva_drive_workflows")
19
- rescue
20
- false
21
- end
22
+ db_config = ActiveRecord::Base.connection_db_config
23
+ # Adapter-specific, and deliberately so - see the comment in the dummy app's
24
+ # config/database.yml. Ask Rails for the path rather than rebuilding it here.
25
+ schema_file = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(db_config)
22
26
 
23
- if migrations.empty? && !tables_exist
27
+ # The generator is the single source of truth for migrations.
28
+ if Dir.glob("#{dummy_root}/db/migrate/*geneva_drive*.rb").empty?
24
29
  puts "Generating GenevaDrive migrations..."
25
30
  Dir.chdir(dummy_root) do
26
31
  system("bin/rails", "generate", "geneva_drive:install", "--skip") || abort("Failed to generate migrations")
27
- system("bin/rails", "db:migrate") || abort("Failed to run migrations")
28
32
  end
29
- elsif migrations.any? && !tables_exist
30
- puts "Running pending migrations..."
33
+ end
34
+
35
+ on_disk = Dir.glob("#{dummy_root}/db/migrate/*.rb").map { |path| File.basename(path)[/\A\d+/] }
36
+ applied = begin
37
+ ActiveRecord::Base.connection.select_values("SELECT version FROM schema_migrations").map(&:to_s)
38
+ rescue
39
+ [] # No database, or no schema_migrations in it yet
40
+ end
41
+
42
+ if (on_disk - applied).any? || !File.exist?(schema_file)
43
+ puts "Recreating the #{db_config.adapter} test database..."
44
+ # db:migrate seeds an empty database from the schema dump before applying
45
+ # anything, so the outdated dump has to go first - otherwise it recreates
46
+ # exactly the tables the migrations are about to create.
47
+ File.delete(schema_file) if File.exist?(schema_file)
48
+ # bin/rails cannot drop a database this process still holds a connection to.
49
+ ActiveRecord::Base.connection_handler.clear_all_connections!
31
50
  Dir.chdir(dummy_root) do
32
- system("bin/rails", "db:prepare") || abort("Failed to prepare database")
51
+ # One bin/rails per task, deliberately. Asking a single process to drop,
52
+ # recreate and migrate carries its column cache across the drop, and the
53
+ # schema it then dumps silently loses column defaults.
54
+ system("bin/rails", "db:drop") || abort("Failed to drop the test database")
55
+ system("bin/rails", "db:prepare") || abort("Failed to recreate the test database")
33
56
  end
34
57
  end
35
58
  end
@@ -44,6 +67,21 @@ if ActiveSupport::TestCase.respond_to?(:fixture_paths=)
44
67
  ActiveSupport::TestCase.fixtures :all
45
68
  end
46
69
 
70
+ # Rails 8.1 clears Active Record's connections before forking the parallel test
71
+ # workers. 7.2 and 8.0 fork with them still open, and a libpq connection
72
+ # inherited across the fork takes the child down with a segfault in
73
+ # PG::Connection#connect_start as it opens its own - a race, so it only bites
74
+ # some of the time, which makes it maddening rather than merely broken. Where
75
+ # upstream has no pre-fork hook to register on, wrap the fork point itself.
76
+ unless ActiveSupport::Testing::Parallelization.respond_to?(:before_fork_hook)
77
+ ActiveSupport::Testing::Parallelization.prepend(Module.new do
78
+ def start
79
+ ActiveRecord::Base.connection_handler.clear_all_connections!
80
+ super
81
+ end
82
+ end)
83
+ end
84
+
47
85
  # Test helper methods
48
86
  class ActiveSupport::TestCase
49
87
  # Run tests in parallel with specified workers
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class RemovedStepTest < ActiveSupport::TestCase
6
+ include GenevaDrive::TestHelpers
7
+
8
+ # The shape a workflow has after `capture_payment` was taken out of the
9
+ # middle of it and a gravestone left behind.
10
+ class PaymentWorkflow < GenevaDrive::Workflow
11
+ step :authorize do
12
+ Thread.current[:ran] << :authorize
13
+ end
14
+
15
+ removed_step :capture_payment
16
+
17
+ step :send_receipt do
18
+ Thread.current[:ran] << :send_receipt
19
+ end
20
+ end
21
+
22
+ # A gravestone in first position, so nothing runnable precedes it.
23
+ class LeadingRemovalWorkflow < GenevaDrive::Workflow
24
+ removed_step :old_preflight
25
+
26
+ step :only_step do
27
+ Thread.current[:ran] << :only_step
28
+ end
29
+ end
30
+
31
+ # A gravestone in last position, so nothing runnable follows it.
32
+ class TrailingRemovalWorkflow < GenevaDrive::Workflow
33
+ step :only_step do
34
+ Thread.current[:ran] << :only_step
35
+ end
36
+
37
+ removed_step :old_teardown
38
+ end
39
+
40
+ # Every step removed - there is nothing left to run at all.
41
+ class FullyRemovedWorkflow < GenevaDrive::Workflow
42
+ removed_step :gone_one
43
+ removed_step :gone_two
44
+ end
45
+
46
+ # A step deleted outright, with no gravestone. This is the situation
47
+ # removed_step exists to prevent, kept here to prove the default is
48
+ # unchanged: the workflow still pauses rather than guessing.
49
+ class UndeclaredRemovalWorkflow < GenevaDrive::Workflow
50
+ step :step_one do
51
+ Thread.current[:ran] << :step_one
52
+ end
53
+
54
+ step :step_two do
55
+ Thread.current[:ran] << :step_two
56
+ end
57
+ end
58
+
59
+ setup do
60
+ @user = create_user
61
+ Thread.current[:ran] = []
62
+ end
63
+
64
+ teardown do
65
+ Thread.current[:ran] = nil
66
+ end
67
+
68
+ # ===========================================
69
+ # The declaration itself
70
+ # ===========================================
71
+
72
+ test "removed_step registers a gravestone that reports itself as removed" do
73
+ step_def = PaymentWorkflow.steps.named("capture_payment")
74
+
75
+ assert_not_nil step_def
76
+ assert_predicate step_def, :removed?
77
+ assert_equal "capture_payment", step_def.name
78
+ end
79
+
80
+ test "an ordinary step is not removed" do
81
+ assert_not_predicate PaymentWorkflow.steps.named("authorize"), :removed?
82
+ end
83
+
84
+ test "a removed step keeps its name resolvable" do
85
+ assert PaymentWorkflow.steps.key?("capture_payment")
86
+ end
87
+
88
+ test "a removed step holds its position in the declared order" do
89
+ assert_equal %w[authorize capture_payment send_receipt], PaymentWorkflow.steps.map(&:name)
90
+ end
91
+
92
+ test "runnable omits removed steps and removed lists them" do
93
+ assert_equal %w[authorize send_receipt], PaymentWorkflow.steps.runnable.map(&:name)
94
+ assert_equal %w[capture_payment], PaymentWorkflow.steps.removed.map(&:name)
95
+ end
96
+
97
+ test "removed_step refuses a name already taken by a step" do
98
+ error = assert_raises(GenevaDrive::StepConfigurationError) do
99
+ Class.new(GenevaDrive::Workflow) do
100
+ def self.name = "DoubleDeclarationWorkflow"
101
+
102
+ step :thing do
103
+ # no-op
104
+ end
105
+
106
+ removed_step :thing
107
+ end
108
+ end
109
+
110
+ assert_match(/already defined/, error.message)
111
+ end
112
+
113
+ test "removed_step requires a name" do
114
+ assert_raises(ArgumentError) do
115
+ Class.new(GenevaDrive::Workflow) do
116
+ removed_step nil
117
+ end
118
+ end
119
+ end
120
+
121
+ test "before_step can still reference a removed step" do
122
+ klass = Class.new(GenevaDrive::Workflow) do
123
+ def self.name = "PositionedAroundRemovalWorkflow"
124
+
125
+ removed_step :gone
126
+
127
+ step :inserted, before_step: :gone do
128
+ # no-op
129
+ end
130
+ end
131
+
132
+ assert_equal %w[inserted gone], klass.steps.map(&:name)
133
+ end
134
+
135
+ # ===========================================
136
+ # Navigation never lands on a gravestone
137
+ # ===========================================
138
+
139
+ test "next_after steps over a removed step" do
140
+ assert_equal "send_receipt", PaymentWorkflow.steps.next_after("authorize").name
141
+ end
142
+
143
+ test "next_after from a removed step returns the next runnable one" do
144
+ assert_equal "send_receipt", PaymentWorkflow.steps.next_after("capture_payment").name
145
+ end
146
+
147
+ test "next_after returns nil when only removed steps remain" do
148
+ assert_nil TrailingRemovalWorkflow.steps.next_after("only_step")
149
+ end
150
+
151
+ test "next_after from the beginning skips a leading removed step" do
152
+ assert_equal "only_step", LeadingRemovalWorkflow.steps.next_after(nil).name
153
+ end
154
+
155
+ test "next_after still returns nil for a name the collection never had" do
156
+ assert_nil PaymentWorkflow.steps.next_after("never_existed")
157
+ end
158
+
159
+ test "previous_before steps over a removed step" do
160
+ assert_equal "authorize", PaymentWorkflow.steps.previous_before("send_receipt").name
161
+ end
162
+
163
+ test "previous_before returns nil when only removed steps precede" do
164
+ assert_nil LeadingRemovalWorkflow.steps.previous_before("only_step")
165
+ end
166
+
167
+ # ===========================================
168
+ # Scheduling never spools a removed step
169
+ # ===========================================
170
+
171
+ test "a new workflow runs straight past a removed middle step" do
172
+ workflow = PaymentWorkflow.create!(hero: @user)
173
+ speedrun_workflow(workflow)
174
+
175
+ assert_equal %i[authorize send_receipt], Thread.current[:ran]
176
+ assert_workflow_state(workflow, :finished)
177
+ assert_empty workflow.step_executions.where(step_name: "capture_payment")
178
+ end
179
+
180
+ test "a new workflow starts at the first runnable step, not a leading gravestone" do
181
+ workflow = LeadingRemovalWorkflow.create!(hero: @user)
182
+
183
+ assert_equal "only_step", workflow.next_step_name
184
+
185
+ speedrun_workflow(workflow)
186
+
187
+ assert_equal %i[only_step], Thread.current[:ran]
188
+ assert_workflow_state(workflow, :finished)
189
+ end
190
+
191
+ test "a trailing gravestone finishes the workflow instead of being scheduled" do
192
+ workflow = TrailingRemovalWorkflow.create!(hero: @user)
193
+ speedrun_workflow(workflow)
194
+
195
+ assert_workflow_state(workflow, :finished)
196
+ assert_empty workflow.step_executions.where(step_name: "old_teardown")
197
+ end
198
+
199
+ test "a workflow whose every step was removed finishes immediately" do
200
+ workflow = FullyRemovedWorkflow.create!(hero: @user)
201
+
202
+ assert_workflow_state(workflow, :finished)
203
+ assert_empty workflow.step_executions
204
+ end
205
+
206
+ # ===========================================
207
+ # Executions scheduled before the removal shipped
208
+ # ===========================================
209
+
210
+ test "an execution scheduled before the removal is skipped and the workflow continues" do
211
+ workflow = PaymentWorkflow.create!(hero: @user)
212
+
213
+ # Stand in for a rolling deploy: this row was written by the old code,
214
+ # which still had capture_payment as a real step.
215
+ execution = reschedule_at(workflow, "capture_payment")
216
+
217
+ execution.execute!
218
+
219
+ assert_equal "skipped", execution.reload.state
220
+ assert_equal "skipped", execution.outcome
221
+
222
+ speedrun_workflow(workflow)
223
+
224
+ assert_equal %i[send_receipt], Thread.current[:ran]
225
+ assert_workflow_state(workflow, :finished)
226
+ end
227
+
228
+ test "an execution for a trailing removed step finishes the workflow" do
229
+ workflow = TrailingRemovalWorkflow.create!(hero: @user)
230
+ execution = reschedule_at(workflow, "old_teardown")
231
+
232
+ execution.execute!
233
+
234
+ assert_equal "skipped", execution.reload.state
235
+ assert_workflow_state(workflow, :finished)
236
+ end
237
+
238
+ test "a step deleted without a gravestone still pauses the workflow" do
239
+ workflow = UndeclaredRemovalWorkflow.create!(hero: @user)
240
+ execution = reschedule_at(workflow, "deleted_without_a_trace")
241
+
242
+ assert_raises(GenevaDrive::StepNotDefinedError) { execution.execute! }
243
+
244
+ assert_workflow_state(workflow, :paused)
245
+ end
246
+
247
+ # ===========================================
248
+ # Knowing when the gravestone can go
249
+ # ===========================================
250
+
251
+ test "removed_steps_in_flight counts rows still pointing at a removed step" do
252
+ workflow = PaymentWorkflow.create!(hero: @user)
253
+ reschedule_at(workflow, "capture_payment")
254
+
255
+ assert_equal({"capture_payment" => 1}, PaymentWorkflow.removed_steps_in_flight)
256
+ end
257
+
258
+ test "removed_steps_in_flight ignores workflows that will never run again" do
259
+ workflow = PaymentWorkflow.create!(hero: @user)
260
+ reschedule_at(workflow, "capture_payment")
261
+ workflow.update!(state: "canceled")
262
+
263
+ assert_empty PaymentWorkflow.removed_steps_in_flight
264
+ end
265
+
266
+ test "removed_steps_in_flight is empty once nothing references the name" do
267
+ PaymentWorkflow.create!(hero: @user)
268
+
269
+ assert_empty PaymentWorkflow.removed_steps_in_flight
270
+ end
271
+
272
+ test "removed_steps_in_flight is empty for a workflow with no removals" do
273
+ assert_empty UndeclaredRemovalWorkflow.removed_steps_in_flight
274
+ end
275
+
276
+ private
277
+
278
+ # Replaces whatever is scheduled with an execution for the given step name,
279
+ # the way a row written by a previous deploy would look.
280
+ def reschedule_at(workflow, step_name)
281
+ workflow.step_executions.where(state: %w[scheduled waiting]).update_all(
282
+ state: "canceled",
283
+ outcome: "canceled",
284
+ canceled_at: Time.current
285
+ )
286
+ execution = workflow.step_executions.create!(
287
+ step_name: step_name,
288
+ state: "scheduled",
289
+ scheduled_for: Time.current
290
+ )
291
+ workflow.update!(next_step_name: step_name, current_step_name: nil)
292
+ execution
293
+ end
294
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ResumeAtTest < ActiveSupport::TestCase
6
+ include GenevaDrive::TestHelpers
7
+
8
+ class ThreeStepWorkflow < GenevaDrive::Workflow
9
+ step :step_one do
10
+ Thread.current[:ran] << :step_one
11
+ end
12
+
13
+ step :step_two do
14
+ Thread.current[:ran] << :step_two
15
+ end
16
+
17
+ step :step_three do
18
+ Thread.current[:ran] << :step_three
19
+ end
20
+ end
21
+
22
+ class WorkflowWithRemoval < GenevaDrive::Workflow
23
+ step :step_one do
24
+ Thread.current[:ran] << :step_one
25
+ end
26
+
27
+ removed_step :parked_forever
28
+
29
+ step :step_three do
30
+ Thread.current[:ran] << :step_three
31
+ end
32
+ end
33
+
34
+ setup do
35
+ @user = create_user
36
+ Thread.current[:ran] = []
37
+ end
38
+
39
+ teardown do
40
+ Thread.current[:ran] = nil
41
+ end
42
+
43
+ # ===========================================
44
+ # resume_at!
45
+ # ===========================================
46
+
47
+ test "resume_at! continues from the named step" do
48
+ workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "step_two")
49
+
50
+ execution = workflow.resume_at!(:step_three)
51
+
52
+ assert_equal "step_three", execution.step_name
53
+ assert_equal "step_three", workflow.reload.next_step_name
54
+ assert_equal "ready", workflow.state
55
+
56
+ speedrun_workflow(workflow)
57
+
58
+ assert_equal %i[step_three], Thread.current[:ran]
59
+ assert_workflow_state(workflow, :finished)
60
+ end
61
+
62
+ test "resume_at! cancels the execution the workflow was pointed at" do
63
+ workflow = ThreeStepWorkflow.create!(hero: @user)
64
+ stale = workflow.current_execution
65
+ workflow.pause!
66
+
67
+ workflow.resume_at!(:step_three)
68
+
69
+ assert_equal "canceled", stale.reload.state
70
+ end
71
+
72
+ test "resume_at! accepts a string as readily as a symbol" do
73
+ workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "step_two")
74
+
75
+ assert_equal "step_three", workflow.resume_at!("step_three").step_name
76
+ end
77
+
78
+ test "resume_at! rejects a step that is not defined" do
79
+ workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "step_two")
80
+
81
+ error = assert_raises(GenevaDrive::StepNotDefinedError) { workflow.resume_at!(:nowhere) }
82
+
83
+ assert_match(/not defined/, error.message)
84
+ assert_match(/step_one, step_two, step_three/, error.message)
85
+ assert_equal "paused", workflow.reload.state
86
+ end
87
+
88
+ test "resume_at! refuses to land on a removed step" do
89
+ workflow = paused_at(WorkflowWithRemoval.create!(hero: @user), "step_one")
90
+
91
+ error = assert_raises(GenevaDrive::StepNotDefinedError) { workflow.resume_at!(:parked_forever) }
92
+
93
+ assert_match(/removed_step/, error.message)
94
+ assert_equal "paused", workflow.reload.state
95
+ end
96
+
97
+ test "resume_at! only applies to paused workflows" do
98
+ workflow = ThreeStepWorkflow.create!(hero: @user)
99
+
100
+ assert_raises(GenevaDrive::InvalidStateError) { workflow.resume_at!(:step_three) }
101
+ end
102
+
103
+ # ===========================================
104
+ # resume! on a workflow pointed at a vanished step
105
+ # ===========================================
106
+
107
+ test "resume! explains itself when the step it points at is gone" do
108
+ workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "deleted_without_a_trace")
109
+
110
+ error = assert_raises(GenevaDrive::StepNotDefinedError) { workflow.resume! }
111
+
112
+ assert_match(/no longer defined/, error.message)
113
+ assert_match(/resume_at!/, error.message)
114
+ assert_match(/removed_step :deleted_without_a_trace/, error.message)
115
+ assert_match(/step_one, step_two, step_three/, error.message)
116
+ end
117
+
118
+ test "resume_at! rescues a workflow stranded by a vanished step" do
119
+ workflow = paused_at(ThreeStepWorkflow.create!(hero: @user), "deleted_without_a_trace")
120
+
121
+ workflow.resume_at!(:step_three)
122
+ speedrun_workflow(workflow)
123
+
124
+ assert_equal %i[step_three], Thread.current[:ran]
125
+ assert_workflow_state(workflow, :finished)
126
+ end
127
+
128
+ # ===========================================
129
+ # A step removed while an execution was parked
130
+ # ===========================================
131
+
132
+ test "resume! releases an execution parked on a since-removed step" do
133
+ workflow = WorkflowWithRemoval.create!(hero: @user)
134
+
135
+ # The old code parked here waiting for a signal; the new code has only a
136
+ # gravestone, so nothing can ever match.
137
+ workflow.step_executions.where(state: %w[scheduled waiting]).update_all(
138
+ state: "canceled", outcome: "canceled", canceled_at: Time.current
139
+ )
140
+ parked = workflow.step_executions.create!(
141
+ step_name: "parked_forever",
142
+ state: "waiting",
143
+ scheduled_for: Time.current,
144
+ waiting_since: Time.current
145
+ )
146
+ workflow.update!(next_step_name: "parked_forever", current_step_name: nil)
147
+ workflow.pause!
148
+
149
+ workflow.resume!
150
+
151
+ assert_equal "skipped", parked.reload.state
152
+
153
+ speedrun_workflow(workflow)
154
+
155
+ assert_equal %i[step_three], Thread.current[:ran]
156
+ assert_workflow_state(workflow, :finished)
157
+ end
158
+
159
+ private
160
+
161
+ # Puts the workflow in the state it would be in after pausing while pointed
162
+ # at the given step name, with no live execution left behind.
163
+ def paused_at(workflow, step_name)
164
+ workflow.step_executions.where(state: %w[scheduled waiting]).update_all(
165
+ state: "canceled", outcome: "canceled", canceled_at: Time.current
166
+ )
167
+ workflow.update!(state: "paused", next_step_name: step_name, current_step_name: nil)
168
+ workflow
169
+ end
170
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geneva_drive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-10-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -94,6 +94,7 @@ files:
94
94
  - MANUAL.md
95
95
  - README.md
96
96
  - Rakefile
97
+ - bin/matrix
97
98
  - bin/md2html
98
99
  - bin/rails
99
100
  - bin/rubocop
@@ -121,7 +122,9 @@ files:
121
122
  - lib/geneva_drive/job_options.rb
122
123
  - lib/geneva_drive/jobs/housekeeping_job.rb
123
124
  - lib/geneva_drive/jobs/perform_step_job.rb
125
+ - lib/geneva_drive/json_column.rb
124
126
  - lib/geneva_drive/migration_helpers.rb
127
+ - lib/geneva_drive/removed_step_definition.rb
125
128
  - lib/geneva_drive/resumable_step_definition.rb
126
129
  - lib/geneva_drive/signal.rb
127
130
  - lib/geneva_drive/signal_matcher.rb
@@ -168,6 +171,7 @@ files:
168
171
  - test/dummy/config/initializers/assets.rb
169
172
  - test/dummy/config/initializers/content_security_policy.rb
170
173
  - test/dummy/config/initializers/filter_parameter_logging.rb
174
+ - test/dummy/config/initializers/geneva_drive.rb
171
175
  - test/dummy/config/initializers/inflections.rb
172
176
  - test/dummy/config/locales/en.yml
173
177
  - test/dummy/config/puma.rb
@@ -182,6 +186,14 @@ files:
182
186
  - test/dummy/public/icon.png
183
187
  - test/dummy/public/icon.svg
184
188
  - test/dummy_install/config/storage.yml
189
+ - test/dummy_install/db/migrate/20260217112949_create_geneva_drive_workflows.rb
190
+ - test/dummy_install/db/migrate/20260217112950_create_geneva_drive_step_executions.rb
191
+ - test/dummy_install/db/migrate/20260217112951_add_finished_at_to_geneva_drive_step_executions.rb
192
+ - test/dummy_install/db/migrate/20260217112952_add_error_class_name_to_geneva_drive_step_executions.rb
193
+ - test/dummy_install/db/migrate/20260217112953_add_resumable_step_support_to_geneva_drive_step_executions.rb
194
+ - test/dummy_install/db/migrate/20260217112954_add_chained_step_executions_to_geneva_drive_step_executions.rb
195
+ - test/dummy_install/db/schema.rb
196
+ - test/dummy_install/log/test.log
185
197
  - test/geneva_drive_test.rb
186
198
  - test/jobs/housekeeping_job_test.rb
187
199
  - test/jobs/housekeeping_signals_test.rb
@@ -205,10 +217,12 @@ files:
205
217
  - test/workflow/pause_resume_test.rb
206
218
  - test/workflow/precondition_exception_test.rb
207
219
  - test/workflow/reattempt_on_exception_integration_test.rb
220
+ - test/workflow/removed_step_test.rb
208
221
  - test/workflow/resumable_step_integration_test.rb
209
222
  - test/workflow/resumable_step_test.rb
210
223
  - test/workflow/resumable_without_migration_test.rb
211
224
  - test/workflow/resume_and_skip_test.rb
225
+ - test/workflow/resume_at_test.rb
212
226
  - test/workflow/signal_consumption_test.rb
213
227
  - test/workflow/signal_delivery_test.rb
214
228
  - test/workflow/signal_durability_test.rb