orchestr8 0.1.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.
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestr8
4
+ class WorkflowsController < ApplicationController
5
+ helper_method :compute_layers
6
+
7
+ def index
8
+ @workflows = Workflow.order(created_at: :desc)
9
+ @workflows = @workflows.by_status(params[:status]) if params[:status].present?
10
+ @workflows = @workflows.by_type(params[:type]) if params[:type].present?
11
+ end
12
+
13
+ def show
14
+ @workflow = Workflow.find(params[:id])
15
+ @steps = @workflow.steps.includes(:dependencies, :dependents)
16
+ end
17
+
18
+ def retry
19
+ workflow = Workflow.find(params[:id])
20
+ workflow.retry!
21
+ redirect_to workflow_path(workflow), notice: "Retrying failed steps"
22
+ end
23
+
24
+ private
25
+
26
+ def compute_layers(steps)
27
+ return [] if steps.empty?
28
+
29
+ remaining = steps.to_a.dup
30
+ layers = []
31
+
32
+ until remaining.empty?
33
+ layer = remaining.select do |step|
34
+ step.dependencies.all? { |dep| layers.flatten.include?(dep) }
35
+ end
36
+
37
+ break if layer.empty?
38
+
39
+ layers << layer
40
+ remaining -= layer
41
+ end
42
+
43
+ layers
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestr8
4
+ class Step < ActiveRecord::Base
5
+ self.table_name = "orchestr8_steps"
6
+
7
+ STATUSES = %w[pending enqueued running completed failed].freeze
8
+ FAILURE_MODES = %w[halt continue].freeze
9
+
10
+ belongs_to :workflow, class_name: "Orchestr8::Workflow", inverse_of: :steps
11
+
12
+ has_many :step_dependencies,
13
+ class_name: "Orchestr8::StepDependency",
14
+ foreign_key: :step_id,
15
+ dependent: :destroy
16
+
17
+ has_many :dependencies,
18
+ through: :step_dependencies,
19
+ source: :dependency
20
+
21
+ has_many :dependent_links,
22
+ class_name: "Orchestr8::StepDependency",
23
+ foreign_key: :dependency_id,
24
+ dependent: :destroy
25
+
26
+ has_many :dependents,
27
+ through: :dependent_links,
28
+ source: :step
29
+
30
+ validates :name, presence: true, uniqueness: { scope: :workflow_id }
31
+ validates :job_class, presence: true
32
+ validates :status, inclusion: { in: STATUSES }
33
+ validates :on_failure, inclusion: { in: FAILURE_MODES }
34
+
35
+ STATUSES.each do |status_name|
36
+ define_method(:"#{status_name}?") { status == status_name }
37
+ end
38
+
39
+ def halt_on_failure?
40
+ on_failure == "halt"
41
+ end
42
+
43
+ def ready?
44
+ return false unless pending?
45
+
46
+ dependencies.all? { |dep| dep.completed? || (dep.failed? && !dep.halt_on_failure?) }
47
+ end
48
+
49
+ def retry!
50
+ update!(status: "pending", error: nil, started_at: nil, finished_at: nil, active_job_id: nil)
51
+ workflow.update!(status: "running", finished_at: nil) if workflow.failed?
52
+ Scheduler.call(workflow)
53
+ end
54
+
55
+ after_update_commit :broadcast_status_change, if: :saved_change_to_status?
56
+
57
+ private
58
+
59
+ def broadcast_status_change
60
+ Turbo::StreamsChannel.broadcast_replace_to(
61
+ "orchestr8_workflow_#{workflow_id}",
62
+ target: "step-#{id}",
63
+ partial: "orchestr8/workflows/step_node",
64
+ locals: { step: self }
65
+ )
66
+ rescue StandardError
67
+ # Turbo broadcast is best-effort; do not fail if ActionCable is not configured
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestr8
4
+ class StepDependency < ActiveRecord::Base
5
+ self.table_name = "orchestr8_step_dependencies"
6
+
7
+ belongs_to :step, class_name: "Orchestr8::Step"
8
+ belongs_to :dependency, class_name: "Orchestr8::Step"
9
+
10
+ validates :dependency_id, uniqueness: { scope: :step_id }
11
+ end
12
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestr8
4
+ class Workflow < ActiveRecord::Base
5
+ self.table_name = "orchestr8_workflows"
6
+ include Orchestr8::Dsl
7
+
8
+ STATUSES = %w[pending running completed failed].freeze
9
+
10
+ has_many :steps,
11
+ class_name: "Orchestr8::Step",
12
+ foreign_key: :workflow_id,
13
+ dependent: :destroy,
14
+ inverse_of: :workflow
15
+
16
+ validates :status, inclusion: { in: STATUSES }
17
+
18
+ STATUSES.each do |status_name|
19
+ define_method(:"#{status_name}?") { status == status_name }
20
+ end
21
+
22
+ scope :by_status, ->(status) { where(status: status) }
23
+ scope :by_type, ->(type) { where(type: type) }
24
+
25
+ def self.create!(arguments: {}, globals: {}, **attrs)
26
+ workflow = new(type: name, arguments: arguments, globals: globals, **attrs)
27
+ workflow.save!
28
+ create_steps_from_definitions!(workflow)
29
+ create_dependencies_from_definitions!(workflow)
30
+ Scheduler.call(workflow)
31
+ workflow.reload
32
+ end
33
+
34
+ def steps_by_name
35
+ steps.index_by { |s| s.name.to_sym }
36
+ end
37
+
38
+ def retry!
39
+ transaction do
40
+ steps.where(status: "failed").find_each do |step|
41
+ step.update!(status: "pending", error: nil, started_at: nil, finished_at: nil, active_job_id: nil)
42
+ end
43
+ update!(status: "running", finished_at: nil) if failed?
44
+ end
45
+ Scheduler.call(self)
46
+ end
47
+
48
+ class << self
49
+ private
50
+
51
+ def create_steps_from_definitions!(workflow)
52
+ step_definitions.each do |step_name, definition|
53
+ workflow.steps.create!(
54
+ name: step_name.to_s,
55
+ job_class: definition[:job].to_s,
56
+ on_failure: definition[:on_failure].to_s,
57
+ queue: definition[:queue]
58
+ )
59
+ end
60
+ end
61
+
62
+ def create_dependencies_from_definitions!(workflow)
63
+ steps_map = workflow.steps_by_name
64
+
65
+ step_definitions.each do |step_name, definition|
66
+ definition[:after].each do |dep_name|
67
+ StepDependency.create!(
68
+ step: steps_map[step_name],
69
+ dependency: steps_map[dep_name]
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,206 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Orchestr8</title>
7
+ <%= csrf_meta_tags %>
8
+ <style>
9
+ *, *::before, *::after { box-sizing: border-box; }
10
+
11
+ .orchestr8 {
12
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
13
+ font-size: 14px;
14
+ color: #1a1a2e;
15
+ background: #f5f5f7;
16
+ min-height: 100vh;
17
+ }
18
+
19
+ .orchestr8-header {
20
+ background: #1a1a2e;
21
+ color: #fff;
22
+ padding: 16px 24px;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ }
27
+
28
+ .orchestr8-header h1 { margin: 0; font-size: 20px; font-weight: 700; }
29
+ .orchestr8-header h1 a { color: #fff; text-decoration: none; }
30
+ .orchestr8-header h1 a:hover { text-decoration: underline; }
31
+
32
+ .orchestr8-main {
33
+ padding: 24px;
34
+ max-width: 1280px;
35
+ margin: 0 auto;
36
+ }
37
+
38
+ .orchestr8-notice {
39
+ background: #d1fae5; border: 1px solid #6ee7b7; border-radius: 6px;
40
+ color: #065f46; padding: 12px 16px; margin-bottom: 20px;
41
+ }
42
+
43
+ .orchestr8-alert {
44
+ background: #fee2e2; border: 1px solid #fca5a5; border-radius: 6px;
45
+ color: #991b1b; padding: 12px 16px; margin-bottom: 20px;
46
+ }
47
+
48
+ .orchestr8-filters {
49
+ display: flex; align-items: center; gap: 12px;
50
+ margin-bottom: 20px; flex-wrap: wrap;
51
+ }
52
+ .orchestr8-filters label { font-weight: 600; color: #374151; }
53
+ .orchestr8-filters select {
54
+ padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px;
55
+ background: #fff; font-size: 14px; cursor: pointer; color: #374151;
56
+ }
57
+ .orchestr8-filters select:focus {
58
+ outline: none; border-color: #6366f1;
59
+ box-shadow: 0 0 0 2px rgba(99,102,241,0.2);
60
+ }
61
+
62
+ .orchestr8-table {
63
+ width: 100%; border-collapse: collapse; background: #fff;
64
+ border-radius: 8px; overflow: hidden;
65
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
66
+ }
67
+ .orchestr8-table thead { background: #f9fafb; }
68
+ .orchestr8-table th {
69
+ padding: 12px 16px; text-align: left; font-weight: 600;
70
+ color: #6b7280; font-size: 12px; text-transform: uppercase;
71
+ letter-spacing: 0.05em; border-bottom: 1px solid #e5e7eb;
72
+ }
73
+ .orchestr8-table td {
74
+ padding: 12px 16px; border-bottom: 1px solid #f3f4f6; color: #374151;
75
+ }
76
+ .orchestr8-table tbody tr:last-child td { border-bottom: none; }
77
+ .orchestr8-table tbody tr:hover { background: #f9fafb; cursor: pointer; }
78
+ .orchestr8-table a { color: #6366f1; text-decoration: none; font-weight: 500; }
79
+ .orchestr8-table a:hover { text-decoration: underline; }
80
+
81
+ .orchestr8-badge {
82
+ display: inline-flex; align-items: center; padding: 3px 10px;
83
+ border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: lowercase;
84
+ }
85
+ .orchestr8-badge--pending { background: #f3f4f6; color: #6b7280; }
86
+ .orchestr8-badge--enqueued { background: #ede9fe; color: #7c3aed; }
87
+ .orchestr8-badge--running { background: #dbeafe; color: #1d4ed8; }
88
+ .orchestr8-badge--completed { background: #d1fae5; color: #065f46; }
89
+ .orchestr8-badge--failed { background: #fee2e2; color: #991b1b; }
90
+
91
+ .orchestr8-progress { display: flex; align-items: center; gap: 8px; }
92
+ .orchestr8-progress-bar {
93
+ flex: 1; height: 8px; background: #e5e7eb;
94
+ border-radius: 9999px; overflow: hidden; min-width: 80px;
95
+ }
96
+ .orchestr8-progress-fill {
97
+ height: 100%; background: #6366f1;
98
+ border-radius: 9999px; transition: width 0.3s ease;
99
+ }
100
+ .orchestr8-progress-text { font-size: 12px; color: #6b7280; white-space: nowrap; }
101
+
102
+ .orchestr8-btn {
103
+ display: inline-flex; align-items: center; gap: 6px;
104
+ padding: 8px 16px; border-radius: 6px; font-size: 14px;
105
+ font-weight: 500; cursor: pointer; border: none;
106
+ text-decoration: none; transition: background 0.15s, box-shadow 0.15s;
107
+ }
108
+ .orchestr8-btn--primary { background: #6366f1; color: #fff; }
109
+ .orchestr8-btn--primary:hover { background: #4f46e5; color: #fff; }
110
+ .orchestr8-btn--secondary { background: #f3f4f6; color: #374151; }
111
+ .orchestr8-btn--secondary:hover { background: #e5e7eb; color: #374151; }
112
+ .orchestr8-btn--danger { background: #fee2e2; color: #991b1b; }
113
+ .orchestr8-btn--danger:hover { background: #fecaca; color: #7f1d1d; }
114
+ .orchestr8-btn--sm { padding: 4px 10px; font-size: 12px; }
115
+
116
+ .orchestr8-page-header {
117
+ display: flex; align-items: center; justify-content: space-between;
118
+ margin-bottom: 24px; flex-wrap: wrap; gap: 12px;
119
+ }
120
+ .orchestr8-page-header h2 {
121
+ margin: 0; font-size: 22px; font-weight: 700; color: #1a1a2e;
122
+ }
123
+
124
+ .orchestr8-detail-header {
125
+ background: #fff; border-radius: 8px; padding: 20px 24px;
126
+ margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
127
+ display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
128
+ }
129
+ .orchestr8-detail-header h2 {
130
+ margin: 0; font-size: 20px; font-weight: 700; color: #1a1a2e; flex: 1;
131
+ }
132
+ .orchestr8-detail-meta { font-size: 12px; color: #6b7280; margin-top: 4px; }
133
+
134
+ .orchestr8-dag {
135
+ background: #fff; border-radius: 8px; padding: 24px;
136
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow-x: auto;
137
+ }
138
+ .orchestr8-dag-layers {
139
+ display: flex; align-items: center; gap: 0; min-width: fit-content;
140
+ }
141
+ .orchestr8-dag-layer {
142
+ display: flex; flex-direction: column;
143
+ align-items: center; gap: 12px; padding: 0 8px;
144
+ }
145
+ .orchestr8-dag-arrow {
146
+ display: flex; align-items: center; color: #d1d5db;
147
+ font-size: 24px; padding: 0 4px; align-self: center;
148
+ }
149
+
150
+ .orchestr8-node {
151
+ border: 2px solid #e5e7eb; border-radius: 8px; padding: 12px 16px;
152
+ min-width: 180px; max-width: 220px; background: #fff;
153
+ transition: border-color 0.2s, box-shadow 0.2s;
154
+ }
155
+ .orchestr8-node:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
156
+ .orchestr8-node--pending { border-color: #d1d5db; background: #f9fafb; }
157
+ .orchestr8-node--enqueued { border-color: #a78bfa; background: #faf5ff; }
158
+ .orchestr8-node--running {
159
+ border-color: #60a5fa; background: #eff6ff;
160
+ animation: orchestr8-pulse 2s infinite;
161
+ }
162
+ .orchestr8-node--completed { border-color: #34d399; background: #f0fdf4; }
163
+ .orchestr8-node--failed { border-color: #f87171; background: #fff5f5; }
164
+
165
+ @keyframes orchestr8-pulse {
166
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(96,165,250,0.4); }
167
+ 50% { box-shadow: 0 0 0 6px rgba(96,165,250,0); }
168
+ }
169
+
170
+ .orchestr8-node-header {
171
+ display: flex; align-items: center;
172
+ justify-content: space-between; margin-bottom: 8px;
173
+ }
174
+ .orchestr8-node-name { font-weight: 700; font-size: 13px; color: #1a1a2e; }
175
+ .orchestr8-node-job { font-size: 11px; color: #6b7280; margin-bottom: 8px; word-break: break-all; }
176
+ .orchestr8-node-meta {
177
+ display: flex; align-items: center;
178
+ justify-content: space-between; font-size: 11px; color: #9ca3af;
179
+ }
180
+ .orchestr8-node-error {
181
+ margin-top: 8px; font-size: 11px; color: #ef4444;
182
+ background: #fee2e2; border-radius: 4px; padding: 4px 8px; word-break: break-word;
183
+ }
184
+ .orchestr8-node-actions { margin-top: 10px; display: flex; gap: 6px; }
185
+
186
+ .orchestr8-empty { text-align: center; padding: 48px 24px; color: #9ca3af; }
187
+ .orchestr8-empty p { font-size: 16px; margin: 0; }
188
+ </style>
189
+ </head>
190
+ <body>
191
+ <div class="orchestr8">
192
+ <header class="orchestr8-header">
193
+ <h1><%= link_to "Orchestr8", orchestr8.root_path %></h1>
194
+ </header>
195
+ <main class="orchestr8-main">
196
+ <% if notice.present? %>
197
+ <div class="orchestr8-notice"><%= notice %></div>
198
+ <% end %>
199
+ <% if alert.present? %>
200
+ <div class="orchestr8-alert"><%= alert %></div>
201
+ <% end %>
202
+ <%= yield %>
203
+ </main>
204
+ </div>
205
+ </body>
206
+ </html>
@@ -0,0 +1,35 @@
1
+ <div id="step-<%= step.id %>" class="orchestr8-node orchestr8-node--<%= step.status %>">
2
+ <div class="orchestr8-node-header">
3
+ <span class="orchestr8-node-name"><%= step.name %></span>
4
+ <span class="orchestr8-badge orchestr8-badge--<%= step.status %>"><%= step.status %></span>
5
+ </div>
6
+
7
+ <div class="orchestr8-node-job"><%= step.job_class %></div>
8
+
9
+ <div class="orchestr8-node-meta">
10
+ <% if step.on_failure == "continue" %>
11
+ <span title="This step will not halt the workflow on failure">&#9888; continues</span>
12
+ <% else %>
13
+ <span></span>
14
+ <% end %>
15
+ <% if step.started_at %>
16
+ <% duration = step.finished_at ? (step.finished_at - step.started_at).round : (Time.current - step.started_at).round %>
17
+ <span><%= duration %>s</span>
18
+ <% end %>
19
+ </div>
20
+
21
+ <% if step.error.present? %>
22
+ <div class="orchestr8-node-error" title="<%= step.error %>">
23
+ <%= truncate(step.error, length: 80) %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <% if step.failed? %>
28
+ <div class="orchestr8-node-actions">
29
+ <%= button_to "Retry",
30
+ orchestr8.retry_workflow_step_path(step.workflow_id, step),
31
+ method: :post,
32
+ class: "orchestr8-btn orchestr8-btn--danger orchestr8-btn--sm" %>
33
+ </div>
34
+ <% end %>
35
+ </div>
@@ -0,0 +1,71 @@
1
+ <div class="orchestr8-page-header">
2
+ <h2>Workflows</h2>
3
+ </div>
4
+
5
+ <div class="orchestr8-filters">
6
+ <label for="status-filter">Status:</label>
7
+ <%= form_with url: orchestr8.workflows_path, method: :get, local: true do |f| %>
8
+ <%= f.select :status,
9
+ [["All", ""], ["Pending", "pending"], ["Running", "running"],
10
+ ["Completed", "completed"], ["Failed", "failed"]],
11
+ { selected: params[:status] },
12
+ { id: "status-filter", onchange: "this.form.submit()" } %>
13
+ <% if params[:type].present? %>
14
+ <%= f.hidden_field :type, value: params[:type] %>
15
+ <% end %>
16
+ <% end %>
17
+ </div>
18
+
19
+ <% if @workflows.empty? %>
20
+ <div class="orchestr8-empty">
21
+ <p>No workflows found.</p>
22
+ </div>
23
+ <% else %>
24
+ <table class="orchestr8-table">
25
+ <thead>
26
+ <tr>
27
+ <th>ID</th>
28
+ <th>Workflow Type</th>
29
+ <th>Status</th>
30
+ <th>Progress</th>
31
+ <th>Duration</th>
32
+ <th>Started</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% @workflows.each do |workflow| %>
37
+ <% total = workflow.steps.size %>
38
+ <% completed = workflow.steps.count { |s| %w[enqueued running completed].include?(s.status) } %>
39
+ <% pct = total > 0 ? (completed.to_f / total * 100).round : 0 %>
40
+ <% duration = if workflow.finished_at && workflow.started_at
41
+ seconds = (workflow.finished_at - workflow.started_at).round
42
+ "#{seconds}s"
43
+ elsif workflow.started_at
44
+ seconds = (Time.current - workflow.started_at).round
45
+ "#{seconds}s"
46
+ else
47
+ "-"
48
+ end %>
49
+ <tr onclick="window.location='<%= orchestr8.workflow_path(workflow) %>'" style="cursor:pointer;">
50
+ <td><%= link_to "##{workflow.id}", orchestr8.workflow_path(workflow) %></td>
51
+ <td><%= workflow.type %></td>
52
+ <td>
53
+ <span class="orchestr8-badge orchestr8-badge--<%= workflow.status %>">
54
+ <%= workflow.status %>
55
+ </span>
56
+ </td>
57
+ <td>
58
+ <div class="orchestr8-progress">
59
+ <div class="orchestr8-progress-bar">
60
+ <div class="orchestr8-progress-fill" style="width: <%= pct %>%;"></div>
61
+ </div>
62
+ <span class="orchestr8-progress-text"><%= completed %>/<%= total %></span>
63
+ </div>
64
+ </td>
65
+ <td><%= duration %></td>
66
+ <td><%= workflow.started_at ? workflow.started_at.strftime("%Y-%m-%d %H:%M:%S") : "-" %></td>
67
+ </tr>
68
+ <% end %>
69
+ </tbody>
70
+ </table>
71
+ <% end %>
@@ -0,0 +1,56 @@
1
+ <% layers = compute_layers(@steps) %>
2
+
3
+ <div class="orchestr8-detail-header">
4
+ <div style="flex: 1;">
5
+ <h2><%= @workflow.type %></h2>
6
+ <div class="orchestr8-detail-meta">
7
+ ID: #<%= @workflow.id %>
8
+ &nbsp;&middot;&nbsp;
9
+ <% if @workflow.started_at %>
10
+ Started: <%= @workflow.started_at.strftime("%Y-%m-%d %H:%M:%S") %>
11
+ <% end %>
12
+ <% if @workflow.finished_at %>
13
+ &nbsp;&middot;&nbsp;
14
+ Finished: <%= @workflow.finished_at.strftime("%Y-%m-%d %H:%M:%S") %>
15
+ &nbsp;&middot;&nbsp;
16
+ Duration: <%= (@workflow.finished_at - @workflow.started_at).round %>s
17
+ <% end %>
18
+ </div>
19
+ </div>
20
+
21
+ <span class="orchestr8-badge orchestr8-badge--<%= @workflow.status %>">
22
+ <%= @workflow.status %>
23
+ </span>
24
+
25
+ <% if @workflow.failed? %>
26
+ <%= button_to "Retry Workflow",
27
+ orchestr8.retry_workflow_path(@workflow),
28
+ method: :post,
29
+ class: "orchestr8-btn orchestr8-btn--danger" %>
30
+ <% end %>
31
+
32
+ <%= link_to "&#8592; Back", orchestr8.workflows_path, class: "orchestr8-btn orchestr8-btn--secondary" %>
33
+ </div>
34
+
35
+ <% if defined?(turbo_stream_from) %>
36
+ <%= turbo_stream_from "orchestr8_workflow_#{@workflow.id}" %>
37
+ <% end %>
38
+
39
+ <div class="orchestr8-dag">
40
+ <% if @steps.empty? %>
41
+ <div class="orchestr8-empty"><p>No steps found for this workflow.</p></div>
42
+ <% else %>
43
+ <div class="orchestr8-dag-layers">
44
+ <% layers.each_with_index do |layer, idx| %>
45
+ <% if idx > 0 %>
46
+ <div class="orchestr8-dag-arrow">&#8594;</div>
47
+ <% end %>
48
+ <div class="orchestr8-dag-layer">
49
+ <% layer.each do |step| %>
50
+ <%= render "orchestr8/workflows/step_node", step: step %>
51
+ <% end %>
52
+ </div>
53
+ <% end %>
54
+ </div>
55
+ <% end %>
56
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Orchestr8::Engine.routes.draw do
4
+ resources :workflows, only: [:index, :show] do
5
+ member do
6
+ post :retry
7
+ end
8
+
9
+ resources :steps, only: [] do
10
+ member do
11
+ post :retry
12
+ end
13
+ end
14
+ end
15
+
16
+ root to: "workflows#index"
17
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateOrchestr8Tables < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :orchestr8_workflows do |t|
6
+ t.string :type, null: false
7
+ t.string :status, null: false, default: "pending"
8
+ t.json :arguments, default: {}
9
+ t.json :globals, default: {}
10
+ t.datetime :started_at
11
+ t.datetime :finished_at
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :orchestr8_workflows, :type
16
+ add_index :orchestr8_workflows, :status
17
+
18
+ create_table :orchestr8_steps do |t|
19
+ t.references :workflow, null: false, foreign_key: { to_table: :orchestr8_workflows }
20
+ t.string :name, null: false
21
+ t.string :job_class, null: false
22
+ t.string :status, null: false, default: "pending"
23
+ t.json :output
24
+ t.text :error
25
+ t.string :queue
26
+ t.string :on_failure, null: false, default: "halt"
27
+ t.string :active_job_id
28
+ t.datetime :started_at
29
+ t.datetime :finished_at
30
+ t.timestamps
31
+ end
32
+
33
+ add_index :orchestr8_steps, [:workflow_id, :status]
34
+ add_index :orchestr8_steps, [:workflow_id, :name], unique: true
35
+ add_index :orchestr8_steps, :active_job_id
36
+
37
+ create_table :orchestr8_step_dependencies do |t|
38
+ t.references :step, null: false, foreign_key: { to_table: :orchestr8_steps }
39
+ t.references :dependency, null: false, foreign_key: { to_table: :orchestr8_steps }
40
+ end
41
+
42
+ add_index :orchestr8_step_dependencies, [:step_id, :dependency_id], unique: true,
43
+ name: "idx_orchestr8_step_deps_unique"
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orchestr8
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("../../../../db/migrate", __dir__)
7
+
8
+ def copy_migrations
9
+ rake "orchestr8:install:migrations"
10
+ end
11
+ end
12
+ end
13
+ end