shipit-engine 0.27.1 → 0.28.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -1
  3. data/app/assets/stylesheets/_pages/_commits.scss +2 -0
  4. data/app/assets/stylesheets/_pages/_deploy.scss +6 -0
  5. data/app/controllers/shipit/api/release_statuses_controller.rb +22 -0
  6. data/app/controllers/shipit/api/stacks_controller.rb +6 -1
  7. data/app/controllers/shipit/commits_controller.rb +12 -1
  8. data/app/controllers/shipit/deploys_controller.rb +11 -0
  9. data/app/controllers/shipit/stacks_controller.rb +29 -1
  10. data/app/controllers/shipit/tasks_controller.rb +13 -1
  11. data/app/helpers/shipit/merge_status_helper.rb +2 -2
  12. data/app/helpers/shipit/stacks_helper.rb +10 -4
  13. data/app/jobs/shipit/perform_task_job.rb +1 -0
  14. data/app/jobs/shipit/update_github_last_deployed_ref_job.rb +41 -0
  15. data/app/models/shipit/command_line_user.rb +58 -0
  16. data/app/models/shipit/commit.rb +42 -2
  17. data/app/models/shipit/deploy.rb +31 -2
  18. data/app/models/shipit/deploy_spec/lerna_discovery.rb +43 -19
  19. data/app/models/shipit/deploy_spec/pypi_discovery.rb +5 -1
  20. data/app/models/shipit/rollback.rb +4 -2
  21. data/app/models/shipit/stack.rb +72 -15
  22. data/app/models/shipit/task.rb +30 -0
  23. data/app/models/shipit/undeployed_commit.rb +10 -1
  24. data/app/serializers/shipit/command_line_user_serializer.rb +4 -0
  25. data/app/views/layouts/shipit.html.erb +5 -1
  26. data/app/views/shipit/commits/_commit.html.erb +7 -2
  27. data/app/views/shipit/merge_status/_commit_count_warning.html.erb +1 -5
  28. data/app/views/shipit/merge_status/backlogged.html.erb +1 -1
  29. data/app/views/shipit/merge_status/failure.html.erb +1 -1
  30. data/app/views/shipit/merge_status/locked.html.erb +1 -1
  31. data/app/views/shipit/merge_status/success.html.erb +1 -1
  32. data/app/views/shipit/stacks/show.html.erb +10 -1
  33. data/app/views/shipit/tasks/_task_output.html.erb +2 -2
  34. data/config/locales/en.yml +2 -1
  35. data/config/routes.rb +8 -1
  36. data/db/migrate/20190502020249_add_lock_author_id_to_commits.rb +5 -0
  37. data/lib/shipit.rb +14 -2
  38. data/lib/shipit/cast_value.rb +9 -0
  39. data/lib/shipit/command.rb +62 -16
  40. data/lib/shipit/line_buffer.rb +42 -0
  41. data/lib/shipit/version.rb +1 -1
  42. data/lib/tasks/shipit.rake +27 -0
  43. data/test/controllers/api/release_statuses_controller_test.rb +66 -0
  44. data/test/controllers/api/stacks_controller_test.rb +19 -0
  45. data/test/controllers/commits_controller_test.rb +30 -6
  46. data/test/controllers/deploys_controller_test.rb +51 -2
  47. data/test/controllers/tasks_controller_test.rb +24 -0
  48. data/test/dummy/db/schema.rb +2 -1
  49. data/test/dummy/db/seeds.rb +2 -0
  50. data/test/fixtures/shipit/check_runs.yml +11 -0
  51. data/test/fixtures/shipit/commits.yml +104 -0
  52. data/test/fixtures/shipit/stacks.yml +98 -3
  53. data/test/fixtures/shipit/tasks.yml +42 -0
  54. data/test/jobs/update_github_last_deployed_ref_job_test.rb +88 -0
  55. data/test/models/commits_test.rb +88 -1
  56. data/test/models/deploy_spec_test.rb +34 -6
  57. data/test/models/deploys_test.rb +308 -6
  58. data/test/models/rollbacks_test.rb +17 -11
  59. data/test/models/stacks_test.rb +217 -4
  60. data/test/models/tasks_test.rb +13 -0
  61. data/test/models/undeployed_commits_test.rb +62 -3
  62. data/test/test_helper.rb +0 -1
  63. data/test/unit/command_test.rb +55 -0
  64. data/test/unit/line_buffer_test.rb +20 -0
  65. metadata +142 -128
@@ -107,6 +107,25 @@ module Shipit
107
107
  assert_response :ok
108
108
  assert_json 'last_deployed_at', @stack.last_deployed_at
109
109
  end
110
+
111
+ test "#destroy schedules stack deletion job" do
112
+ assert_enqueued_with(job: DestroyStackJob) do
113
+ delete :destroy, params: {id: @stack.to_param}
114
+ end
115
+ assert_response :accepted
116
+ end
117
+
118
+ test "#destroy fails with insufficient permissions" do
119
+ @client.permissions.delete('write:stack')
120
+ @client.save!
121
+
122
+ assert_no_difference 'Stack.count' do
123
+ delete :destroy, params: {id: @stack.to_param}
124
+ end
125
+
126
+ assert_response :forbidden
127
+ assert_json 'message', 'This operation requires the `write:stack` permission'
128
+ end
110
129
  end
111
130
  end
112
131
  end
@@ -5,14 +5,38 @@ module Shipit
5
5
  setup do
6
6
  @stack = shipit_stacks(:shipit)
7
7
  @commit = shipit_commits(:first)
8
- session[:user_id] = shipit_users(:walrus).id
8
+ @user = shipit_users(:walrus)
9
+ session[:user_id] = @user.id
9
10
  end
10
11
 
11
- test "#update allows to lock a commit" do
12
- refute_predicate @commit, :locked?
13
- patch :update, params: {stack_id: @stack.to_param, id: @commit.id, commit: {locked: true}}
14
- assert_response :ok
15
- assert_predicate @commit.reload, :locked?
12
+ test "#update allows commits to be locked and sets the lock author" do
13
+ refute_predicate(@commit, :locked?)
14
+
15
+ patch(:update, params: {
16
+ stack_id: @stack.to_param,
17
+ id: @commit.id,
18
+ commit: {locked: true},
19
+ })
20
+
21
+ assert_response(:ok)
22
+ @commit.reload
23
+ assert_predicate(@commit, :locked?)
24
+ assert_equal(@user, @commit.lock_author)
25
+ end
26
+
27
+ test "#update allows commits to be unlocked and clears the lock author" do
28
+ @commit.lock(@user)
29
+
30
+ patch(:update, params: {
31
+ stack_id: @stack.to_param,
32
+ id: @commit.id,
33
+ commit: {locked: false},
34
+ })
35
+
36
+ assert_response(:ok)
37
+ @commit.reload
38
+ refute_predicate(@commit, :locked?)
39
+ assert_nil(@commit.lock_author_id)
16
40
  end
17
41
  end
18
42
  end
@@ -7,11 +7,12 @@ module Shipit
7
7
  @stack = shipit_stacks(:shipit)
8
8
  @deploy = shipit_deploys(:shipit)
9
9
  @commit = shipit_commits(:second)
10
- session[:user_id] = shipit_users(:walrus).id
10
+ @user = shipit_users(:walrus)
11
+ session[:user_id] = @user.id
11
12
  end
12
13
 
13
14
  test ":show is success" do
14
- get :show, params: {stack_id: @stack.to_param, id: @deploy.id}
15
+ get :show, params: {stack_id: @stack.to_param, id: @stack.deploys.last.id}
15
16
  assert_response :success
16
17
  end
17
18
 
@@ -100,6 +101,54 @@ module Shipit
100
101
  assert_select '#new_rollback #force', 1
101
102
  end
102
103
 
104
+ test ":rollback button shows deploy and commit ids" do
105
+ previous_deploy = @stack.deploys.second_to_last
106
+ previous_deploy.status = "success"
107
+ previous_deploy.type = "Shipit::Deploy"
108
+ previous_deploy.since_commit_id = 1
109
+ previous_deploy.until_commit_id = 2
110
+ previous_deploy.save
111
+
112
+ latest_deploy = @stack.deploys.last
113
+ latest_deploy.status = "running"
114
+ latest_deploy.type = "Shipit::Deploy"
115
+ latest_deploy.since_commit_id = 3
116
+ latest_deploy.until_commit_id = 4
117
+ latest_deploy.save
118
+
119
+ rollback_commit = @stack.commits.where(id: 2).first
120
+
121
+ get :show, params: {stack_id: @stack, id: latest_deploy.id, format: 'html'}
122
+
123
+ expected_result = "Abort and Rollback to <span class=\"short-sha-no-bg\">#{rollback_commit.short_sha}</span>"
124
+ expected_rolling_back_element = "Aborting with Rollback... to <span class=\"short-sha-no-bg\">#{rollback_commit.short_sha}</span>"
125
+
126
+ assert_select 'span.caption--ready', {html: expected_result}, "rollback button element was not found, or did not match the expected result of '#{expected_result}'"
127
+ assert_select 'span.caption--pending', {html: expected_rolling_back_element}, "ready rollback button element was not found, or did not match the expected result of '#{expected_rolling_back_element}'"
128
+ end
129
+
130
+ test ":rollback (regression) works correctly when a previous deploy is not found" do
131
+ rollback_commit_id = 3
132
+ latest_deploy = @stack.deploys.last
133
+ latest_deploy.status = "running"
134
+ latest_deploy.type = "Shipit::Deploy"
135
+ latest_deploy.since_commit_id = rollback_commit_id
136
+ latest_deploy.until_commit_id = 4
137
+ latest_deploy.save
138
+
139
+ @stack.deploys.where.not(id: latest_deploy.id).delete_all
140
+
141
+ rollback_commit = @stack.commits.where(id: rollback_commit_id).take
142
+
143
+ get :show, params: {stack_id: @stack, id: latest_deploy.id, format: 'html'}
144
+
145
+ expected_result = "Abort and Rollback to <span class=\"short-sha-no-bg\">#{rollback_commit.short_sha}</span>"
146
+ expected_rolling_back_element = "Aborting with Rollback... to <span class=\"short-sha-no-bg\">#{rollback_commit.short_sha}</span>"
147
+
148
+ assert_select 'span.caption--ready', {html: expected_result}, "rollback button element was not found, or did not match the expected result of '#{expected_result}'"
149
+ assert_select 'span.caption--pending', {html: expected_rolling_back_element}, "ready rollback button element was not found, or did not match the expected result of '#{expected_rolling_back_element}'"
150
+ end
151
+
103
152
  test ":revert redirect to the proper rollback page" do
104
153
  get :revert, params: {stack_id: @stack.to_param, id: shipit_deploys(:shipit2).id}
105
154
  assert_redirected_to rollback_stack_deploy_path(@stack, shipit_deploys(:shipit))
@@ -123,5 +123,29 @@ module Shipit
123
123
  assert_response :success
124
124
  assert_json_keys %w(status output rollback_url)
125
125
  end
126
+
127
+ test ":lookup returns stack task url if the task is an instance of Task" do
128
+ @task = shipit_tasks(:shipit_restart)
129
+
130
+ get :lookup, params: {id: @task.id}
131
+
132
+ assert_redirected_to stack_task_path(@task.stack, @task)
133
+ end
134
+
135
+ test ":lookup returns stack deploy url if the task is an instance of Deploy" do
136
+ @task = shipit_tasks(:shipit)
137
+
138
+ get :lookup, params: {id: @task.id}
139
+
140
+ assert_redirected_to stack_deploy_path(@task.stack, @task)
141
+ end
142
+
143
+ test ":lookup returns stack deploy url if the task is an instance of Rollback" do
144
+ @task = shipit_tasks(:shipit_rollback)
145
+
146
+ get :lookup, params: {id: @task.id}
147
+
148
+ assert_redirected_to stack_deploy_path(@task.stack, @task)
149
+ end
126
150
  end
127
151
  end
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2018_10_10_150947) do
13
+ ActiveRecord::Schema.define(version: 2019_05_02_020249) do
14
14
 
15
15
  create_table "api_clients", force: :cascade do |t|
16
16
  t.text "permissions", limit: 65535
@@ -76,6 +76,7 @@ ActiveRecord::Schema.define(version: 2018_10_10_150947) do
76
76
  t.string "pull_request_title", limit: 1024
77
77
  t.integer "pull_request_id"
78
78
  t.boolean "locked", default: false, null: false
79
+ t.integer "lock_author_id", limit: 4
79
80
  t.index ["author_id"], name: "index_commits_on_author_id"
80
81
  t.index ["committer_id"], name: "index_commits_on_committer_id"
81
82
  t.index ["created_at"], name: "index_commits_on_created_at"
@@ -9,6 +9,8 @@ module Shipit
9
9
  Stack.send(:define_method, :sync_github) {}
10
10
  Commit.send(:define_method, :fetch_stats!) {}
11
11
  Commit.send(:define_method, :refresh_statuses!) {}
12
+ Commit.send(:define_method, :refresh_check_runs!) {}
13
+ ReleaseStatus.send(:define_method, :create_status_on_github!) {}
12
14
 
13
15
  users = 3.times.map do
14
16
  User.create!(
@@ -9,6 +9,17 @@ second_pending_travis:
9
9
  details_url: "http://www.example.com/build/424242"
10
10
  created_at: <%= 10.days.ago.to_s(:db) %>
11
11
 
12
+ check_runs_first_pending_coveralls:
13
+ stack: check_runs
14
+ commit_id: 201 # check_runs_first
15
+ github_id: 43
16
+ title: lets go
17
+ name: Coverage metrics
18
+ created_at: <%= 10.days.ago.to_s(:db) %>
19
+ conclusion: pending
20
+ html_url: "http://www.example.com/run/434343"
21
+ details_url: "http://www.example.com/build/434343"
22
+
12
23
  check_runs_first_success_coveralls:
13
24
  stack: check_runs
14
25
  commit_id: 201 # check_runs_first
@@ -246,3 +246,107 @@ canaries_fifth:
246
246
  additions: 1
247
247
  deletions: 24
248
248
  updated_at: <%= 8.days.ago.to_s(:db) %>
249
+
250
+ undeployed_1:
251
+ id: 401
252
+ sha: 4d9278037b872fd9a6690523e411ecb3aa181355
253
+ message: "lets go"
254
+ stack: shipit_undeployed
255
+ author: walrus
256
+ committer: walrus
257
+ authored_at: <%= 10.days.ago.to_s(:db) %>
258
+ committed_at: <%= 9.days.ago.to_s(:db) %>
259
+ additions: 42
260
+ deletions: 24
261
+ updated_at: <%= 8.days.ago.to_s(:db) %>
262
+
263
+ undeployed_2:
264
+ id: 402
265
+ sha: 4890fd8b5f2be05d1fedb763a3605ee461c39074
266
+ message: "sheep it!"
267
+ stack: shipit_undeployed
268
+ author: walrus
269
+ committer: walrus
270
+ authored_at: <%= 8.days.ago.to_s(:db) %>
271
+ committed_at: <%= 7.days.ago.to_s(:db) %>
272
+ additions: 1
273
+ deletions: 1
274
+ updated_at: <%= 8.days.ago.to_s(:db) %>
275
+
276
+ undeployed_3:
277
+ id: 403
278
+ sha: 467578b362bf2b4df5903e1c7960929361c39074
279
+ message: "fix it!"
280
+ stack: shipit_undeployed
281
+ author: walrus
282
+ committer: walrus
283
+ authored_at: <%= 6.days.ago.to_s(:db) %>
284
+ committed_at: <%= 5.days.ago.to_s(:db) %>
285
+ additions: 12
286
+ deletions: 64
287
+ updated_at: <%= 8.days.ago.to_s(:db) %>
288
+
289
+ undeployed_4:
290
+ id: 404
291
+ sha: 447578b362bf2b4df5903e1c7960929361c3435a
292
+ message: "Merge pull request #7 from shipit-engine/yoloshipit\n\nyoloshipit!"
293
+ stack: shipit_undeployed
294
+ author: walrus
295
+ committer: walrus
296
+ authored_at: <%= 4.days.ago.to_s(:db) %>
297
+ committed_at: <%= 3.days.ago.to_s(:db) %>
298
+ additions: 420
299
+ deletions: 342
300
+ updated_at: <%= 8.days.ago.to_s(:db) %>
301
+
302
+ undeployed_5:
303
+ id: 405
304
+ sha: 457578b362bf2b4df5903e1c7960929361c3435a
305
+ message: "Merge pull request #7 from shipit-engine/yoloshipit\n\nyoloshipit!"
306
+ stack: shipit_undeployed
307
+ author: walrus
308
+ committer: walrus
309
+ authored_at: <%= 4.days.ago.to_s(:db) %>
310
+ committed_at: <%= 3.days.ago.to_s(:db) %>
311
+ additions: 420
312
+ deletions: 342
313
+ updated_at: <%= 8.days.ago.to_s(:db) %>
314
+
315
+ undeployed_6:
316
+ id: 406
317
+ sha: 467578b362bf2b4df5903e1c7960929361c3435a
318
+ message: "Merge pull request #7 from shipit-engine/yoloshipit\n\nyoloshipit!"
319
+ stack: shipit_undeployed
320
+ author: walrus
321
+ committer: walrus
322
+ authored_at: <%= 4.days.ago.to_s(:db) %>
323
+ committed_at: <%= 3.days.ago.to_s(:db) %>
324
+ additions: 420
325
+ deletions: 342
326
+ updated_at: <%= 8.days.ago.to_s(:db) %>
327
+
328
+ undeployed_7:
329
+ id: 407
330
+ sha: 477578b362bf2b4df5903e1c7960929361c3435a
331
+ message: "Merge pull request #7 from shipit-engine/yoloshipit\n\nyoloshipit!"
332
+ stack: shipit_undeployed
333
+ author: walrus
334
+ committer: walrus
335
+ authored_at: <%= 4.days.ago.to_s(:db) %>
336
+ committed_at: <%= 3.days.ago.to_s(:db) %>
337
+ additions: 420
338
+ deletions: 342
339
+ updated_at: <%= 8.days.ago.to_s(:db) %>
340
+
341
+ single:
342
+ id: 501
343
+ sha: 547578b362bf2b4df5903e1c7960929361c3435a
344
+ message: "first commit"
345
+ stack: shipit_single
346
+ author: walrus
347
+ committer: walrus
348
+ authored_at: <%= 4.days.ago.to_s(:db) %>
349
+ committed_at: <%= 3.days.ago.to_s(:db) %>
350
+ additions: 420
351
+ deletions: 342
352
+ updated_at: <%= 8.days.ago.to_s(:db) %>
@@ -5,8 +5,8 @@ shipit:
5
5
  branch: master
6
6
  ignore_ci: false
7
7
  merge_queue_enabled: true
8
- tasks_count: 3
9
- undeployed_commits_count: 1
8
+ tasks_count: 8
9
+ undeployed_commits_count: 2
10
10
  cached_deploy_spec: >
11
11
  {
12
12
  "machine": {"environment": {}},
@@ -62,6 +62,7 @@ shipit_canaries:
62
62
  merge_queue_enabled: true
63
63
  tasks_count: 3
64
64
  undeployed_commits_count: 1
65
+ continuous_deployment: true
65
66
  cached_deploy_spec: >
66
67
  {
67
68
  "machine": {"environment": {}},
@@ -224,4 +225,98 @@ check_runs:
224
225
  environment: production
225
226
  branch: master
226
227
  tasks_count: 0
227
- undeployed_commits_count: 1
228
+ undeployed_commits_count: 1
229
+
230
+ shipit_undeployed:
231
+ repo_owner: "shopify"
232
+ repo_name: "shipit-engine"
233
+ environment: "undeployed"
234
+ branch: master
235
+ ignore_ci: true
236
+ merge_queue_enabled: true
237
+ tasks_count: 2
238
+ undeployed_commits_count: 6
239
+ continuous_deployment: true
240
+ cached_deploy_spec: >
241
+ {
242
+ "machine": {"environment": {}},
243
+ "review": {
244
+ "checklist": ["foo", "bar", "baz"],
245
+ "monitoring": [
246
+ {"image": "https://example.com/monitor.png", "width": 200, "height": 300}
247
+ ]
248
+ },
249
+ "dependencies": {"override": []},
250
+ "deploy": {"override": null, "interval": 60, "max_commits": 3, "variables": [{"name": "SAFETY_DISABLED", "title": "Set to 1 to do dangerous things", "default": 0}]},
251
+ "rollback": {"override": ["echo 'Rollback!'"]},
252
+ "fetch": ["echo '42'"],
253
+ "tasks": {
254
+ "restart": {
255
+ "action": "Restart application",
256
+ "description": "Restart app and job servers",
257
+ "variables": [
258
+ {"name": "FOO", "title": "Set to 0 to foo", "default": 1},
259
+ {"name": "BAR", "title": "Set to 1 to bar", "default": 0},
260
+ {"name": "WALRUS", "title": "Walrus without a default value"}
261
+ ],
262
+ "steps": [
263
+ "cap $ENVIRONMENT deploy:restart"
264
+ ]
265
+ },
266
+ "flush_cache": {
267
+ "action": "Flush cache",
268
+ "description": "Flush the application cache",
269
+ "steps": [
270
+ "cap $ENVIRONMENT cache:flush"
271
+ ],
272
+ "allow_concurrency": true
273
+ }
274
+ },
275
+ "merge": {
276
+ "revalidate_after": 900
277
+ },
278
+ "ci": {
279
+ "hide": ["ci/hidden"],
280
+ "allow_failures": ["ci/ok_to_fail"]
281
+ }
282
+ }
283
+ last_deployed_at: <%= 8.days.ago.to_s(:db) %>
284
+ updated_at: <%= 8.days.ago.to_s(:db) %>
285
+
286
+ shipit_single:
287
+ repo_owner: "shopify"
288
+ repo_name: "shipit-engine"
289
+ environment: "single"
290
+ branch: master
291
+ ignore_ci: false
292
+ merge_queue_enabled: true
293
+ tasks_count: 2
294
+ undeployed_commits_count: 1
295
+ cached_deploy_spec: >
296
+ {
297
+ "machine": {"environment": {}},
298
+ "review": {
299
+ "checklist": ["foo", "bar", "baz"],
300
+ "monitoring": [
301
+ {"image": "https://example.com/monitor.png", "width": 200, "height": 300}
302
+ ]
303
+ },
304
+ "dependencies": {"override": []},
305
+ "deploy": {"override": null},
306
+ "rollback": {"override": ["echo 'Rollback!'"]},
307
+ "fetch": ["echo '42'"],
308
+ "tasks": {
309
+ "restart": {
310
+ "action": "Restart application",
311
+ "description": "Restart app and job servers",
312
+ "steps": [
313
+ "cap $ENVIRONMENT deploy:restart"
314
+ ]
315
+ }
316
+ },
317
+ "ci": {
318
+ "blocking": ["soc/compliance"]
319
+ }
320
+ }
321
+ updated_at: <%= 8.days.ago.to_s(:db) %>
322
+ last_deployed_at: <%= 8.days.ago.to_s(:db) %>
@@ -236,3 +236,45 @@ shipit_with_title_parsing_issue:
236
236
  created_at: <%= (60 - 3).minutes.ago.to_s(:db) %>
237
237
  started_at: <%= (60 - 3).minutes.ago.to_s(:db) %>
238
238
  ended_at: <%= (60 - 4).minutes.ago.to_s(:db) %>
239
+
240
+ shipit_undeployed_1:
241
+ id: 201
242
+ user: walrus
243
+ since_commit_id: 401
244
+ until_commit_id: 401
245
+ type: Shipit::Deploy
246
+ stack: shipit_undeployed
247
+ status: success
248
+ additions: 1
249
+ deletions: 1
250
+ created_at: <%= (60 - 1).minutes.ago.to_s(:db) %>
251
+ started_at: <%= (60 - 1).minutes.ago.to_s(:db) %>
252
+ ended_at: <%= (60 - 3).minutes.ago.to_s(:db) %>
253
+
254
+ shipit_undeployed_2:
255
+ id: 202
256
+ user: walrus
257
+ since_commit_id: 402
258
+ until_commit_id: 403
259
+ type: Shipit::Deploy
260
+ stack: shipit_undeployed
261
+ status: running
262
+ additions: 12
263
+ deletions: 64
264
+ created_at: <%= (60 - 2).minutes.ago.to_s(:db) %>
265
+ started_at: <%= (60 - 2).minutes.ago.to_s(:db) %>
266
+ ended_at: <%= (60 - 4).minutes.ago.to_s(:db) %>
267
+
268
+ shipit_single:
269
+ id: 301
270
+ user: walrus
271
+ since_commit_id: 501
272
+ until_commit_id: 501
273
+ type: Shipit::Deploy
274
+ stack: shipit_single
275
+ status: running
276
+ additions: 12
277
+ deletions: 64
278
+ created_at: <%= (60 - 2).minutes.ago.to_s(:db) %>
279
+ started_at: <%= (60 - 2).minutes.ago.to_s(:db) %>
280
+ ended_at: <%= (60 - 4).minutes.ago.to_s(:db) %>