shipit-engine 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -11
  3. data/app/assets/javascripts/shipit.js.coffee +7 -1
  4. data/app/assets/stylesheets/_base/_buttons.scss +3 -0
  5. data/app/assets/stylesheets/_base/_colors.scss +1 -1
  6. data/app/assets/stylesheets/_base/_status-items.scss +1 -1
  7. data/app/assets/stylesheets/_pages/_deploy.scss +1 -1
  8. data/app/assets/stylesheets/_structure/_main.scss +1 -1
  9. data/app/controllers/shipit/stacks_controller.rb +1 -5
  10. data/app/helpers/shipit/shipit_helper.rb +7 -9
  11. data/app/helpers/shipit/stacks_helper.rb +14 -28
  12. data/app/jobs/shipit/continuous_delivery_job.rb +1 -1
  13. data/app/jobs/shipit/update_estimated_deploy_duration.rb +9 -0
  14. data/app/models/shipit/commit_checks.rb +1 -1
  15. data/app/models/shipit/deploy.rb +1 -1
  16. data/app/models/shipit/deploy_spec/bundler_discovery.rb +1 -1
  17. data/app/models/shipit/deploy_spec/file_system.rb +6 -1
  18. data/app/models/shipit/deploy_spec.rb +12 -0
  19. data/app/models/shipit/duration.rb +29 -9
  20. data/app/models/shipit/github_hook.rb +0 -2
  21. data/app/models/shipit/stack.rb +48 -11
  22. data/app/models/shipit/task.rb +13 -2
  23. data/app/models/shipit/team.rb +1 -1
  24. data/app/models/shipit/undeployed_commit.rb +36 -0
  25. data/app/views/layouts/shipit.html.erb +6 -4
  26. data/app/views/shipit/deploys/show.html.erb +1 -1
  27. data/app/views/shipit/stacks/_header.html.erb +7 -0
  28. data/app/views/shipit/stacks/show.html.erb +1 -1
  29. data/config/locales/en.yml +9 -3
  30. data/config/secrets.development.example.yml +19 -0
  31. data/config/secrets.development.shopify.yml +19 -0
  32. data/config/secrets.development.yml +16 -0
  33. data/db/migrate/20160502150713_add_estimated_deploy_duration_to_stacks.rb +5 -0
  34. data/db/migrate/20160526192650_reorder_active_tasks_index.rb +7 -0
  35. data/lib/shipit/command.rb +15 -2
  36. data/lib/shipit/engine.rb +2 -1
  37. data/lib/shipit/stat.rb +13 -0
  38. data/lib/shipit/version.rb +1 -1
  39. data/lib/shipit.rb +13 -0
  40. data/lib/tasks/cron.rake +1 -0
  41. data/test/controllers/github_authentication_controller_test.rb +5 -5
  42. data/test/dummy/config/initializers/0_load_development_secrets.rb +9 -0
  43. data/test/dummy/config/secrets.yml +5 -16
  44. data/test/dummy/db/development.sqlite3 +0 -0
  45. data/test/dummy/db/schema.rb +14 -14
  46. data/test/dummy/db/seeds.rb +1 -0
  47. data/test/dummy/db/test.sqlite3 +0 -0
  48. data/test/fixtures/shipit/stacks.yml +2 -2
  49. data/test/fixtures/shipit/tasks.yml +3 -1
  50. data/test/fixtures/shipit/users.yml +5 -0
  51. data/test/helpers/queries_helper.rb +1 -1
  52. data/test/models/deploys_test.rb +7 -0
  53. data/test/models/duration_test.rb +23 -0
  54. data/test/models/stacks_test.rb +93 -4
  55. data/test/models/undeployed_commits_test.rb +100 -0
  56. data/test/test_helper.rb +1 -0
  57. data/test/unit/deploy_spec_test.rb +1 -1
  58. data/test/unit/shipit_test.rb +2 -1
  59. metadata +19 -10
  60. data/db/schema.rb +0 -188
  61. data/test/dummy/config/secrets.example.yml +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: af8d2464cc17caa2786a2a0a9500583b5ed0c7f3
4
- data.tar.gz: e00c5b97da7d17dfc23e449630f158e724a5910f
3
+ metadata.gz: c4329e9e481f6b7065a401570ebd011d1ea4fb19
4
+ data.tar.gz: ef736fcc3a561d2db82c35795092388abbbfc90a
5
5
  SHA512:
6
- metadata.gz: 4bff19eadd58b4c9e0bc84c7facd5aaf4dfdc85000c19e1ba50754b7d011a4133ad620e92fef5c6863709643a299c31ecdbcb94634fcd23a48ee137b1fc2fd48
7
- data.tar.gz: 9a9ef4184bac10c36545cb0c14105d6c7600a1c528bfa087295542f7d3cf01c1f435ed748ee543bf0cd451bc2707662b85d757496eae2d138bcd0a72ba228c0b
6
+ metadata.gz: e79a3a01a5d2c71d48dc0bc0ec8ae032721046cccecb5680c876b82c0c246da5ce75a4812ee24afe9aeeaca06d5749637bffb6cb2f9b9aa374fe96f2c790cbff
7
+ data.tar.gz: 60bde57fcfbd57ff17bc6d0d64785b81bf2e2cfd9ee69d5c9849911a5a6936ceccd9ac80fb2a50bb10254a9789084b26f1095acc43b7c7593ebcd19255d93554
data/README.md CHANGED
@@ -33,7 +33,7 @@ This guide aims to help you [set up](#installation-and-setup), [use](#using-ship
33
33
  * [Format and content of shipit.yml](#configuring-shipit)
34
34
  * [Format and content of secrets.yml](#configuring-secrets)
35
35
  * [Script parameters](#script-parameters)
36
- * [Free samples](#sample-file)
36
+ * [Free samples](/examples/shipit.yml)
37
37
 
38
38
  * * *
39
39
 
@@ -45,8 +45,8 @@ This guide aims to help you [set up](#installation-and-setup), [use](#using-ship
45
45
 
46
46
  Shipit provides you with a Rails template. To bootstrap your Shipit installation:
47
47
 
48
- 1. If you don't have Rails installed, run this command: `gem install rails`
49
- 2. Run this command: `rails new shipit -m https://raw.githubusercontent.com/Shopify/shipit-engine/master/template.rb`
48
+ 1. If you don't have Rails installed, run this command: `gem install rails -v 4.2.6`
49
+ 2. Run this command: `rails _4.2.6_ new shipit -m https://raw.githubusercontent.com/Shopify/shipit-engine/master/template.rb`
50
50
  3. Enter your **Client ID**, **Client Secret**, and **GitHub API access token** when prompted. These can be found on your application's GitHub page.
51
51
  4. To setup the database, run this command: `rake db:setup`
52
52
 
@@ -250,6 +250,18 @@ deploy:
250
250
  ```
251
251
  <br>
252
252
 
253
+ **<code>deploy.max_commits</code>** allow to set a limit to the number of commits being shipped per deploys.
254
+
255
+ Human users will be warned that they are not respecting the recommandation, but allowed to continue.
256
+
257
+ For example:
258
+
259
+ ```yaml
260
+ deploy:
261
+ max_commits: 5
262
+ ```
263
+ <br>
264
+
253
265
  **<code>rollback.override</code>** contains an array of the shell commands required to rollback the application to a previous state. Shipit will try to infer it from the repository structure, but you can change the default inference. This key defaults to disabled unless Capistrano is detected.
254
266
 
255
267
  For example:
@@ -360,7 +372,7 @@ You can create custom tasks that users execute directly from a stack's overview
360
372
  tasks:
361
373
  restart:
362
374
  action: "Restart Application"
363
- description: "Sometimes needed if you the application to restart but don't want to ship any new code."
375
+ description: "Sometimes needed if you want the application to restart but don't want to ship any new code."
364
376
  steps:
365
377
  - ssh deploy@myserver.example.com 'touch myapp/restart.txt'
366
378
  ```
@@ -382,7 +394,7 @@ Tasks like deploys can prompt for user defined environment variables:
382
394
  tasks:
383
395
  restart:
384
396
  action: "Restart Application"
385
- description: "Sometimes needed if you the application to restart but don't want to ship any new code."
397
+ description: "Sometimes needed if you want the application to restart but don't want to ship any new code."
386
398
  steps:
387
399
  - ssh deploy@myserver.example.com 'touch myapp/restart.txt'
388
400
  variables:
@@ -444,7 +456,7 @@ The settings in the `secrets.yml` file relate to the ways that GitHub connects w
444
456
  For example:
445
457
 
446
458
  ```yml
447
- development:
459
+ production:
448
460
  secret_key_base: s3cr3t # This needs to be a very long, fully random
449
461
  ```
450
462
  <br>
@@ -462,7 +474,7 @@ After you change the list of time you have to invoke `bin/rake teams:fetch` in p
462
474
  For example:
463
475
 
464
476
  ```yml
465
- development:
477
+ production:
466
478
  github_oauth:
467
479
  id: (your application's Client ID)
468
480
  secret: (your application's Client Secret)
@@ -479,7 +491,7 @@ If you specify an `access_token`, you don't need a `login` and `password`. The o
479
491
  For example:
480
492
 
481
493
  ```yml
482
- development:
494
+ production:
483
495
  github_api:
484
496
  access_token: 10da65c687f6degaf5475ce12a980d5vd8c44d2a
485
497
  ```
@@ -489,7 +501,7 @@ development:
489
501
 
490
502
  For example:
491
503
  ```yml
492
- development:
504
+ production:
493
505
  host: 'http://localhost:3000'
494
506
  ```
495
507
  <br>
@@ -499,7 +511,7 @@ development:
499
511
  For example:
500
512
 
501
513
  ```yml
502
- development:
514
+ production:
503
515
  redis_url: "redis://127.0.0.1:6379/7"
504
516
  ```
505
517
 
@@ -509,11 +521,21 @@ If you use GitHub Enterprise, you must also specify the `github_domain`.
509
521
 
510
522
  For example:
511
523
  ```yml
512
- development:
524
+ production:
513
525
  github_domain: "github.example.com"
514
526
 
515
527
  ```
516
528
 
529
+ <br>
530
+
531
+ **`commands_inactivity_timeout`** is the duration after which shipit will terminate a command if no ouput was received. Default is `300` (5 minutes).
532
+
533
+ For example:
534
+ ```yml
535
+ production:
536
+ commands_inactivity_timeout: 900 # 15 minutes
537
+ ```
538
+
517
539
  <h2 id="script-parameters">Script parameters</h2>
518
540
 
519
541
  Your deploy scripts have access to the following environment variables:
@@ -20,8 +20,14 @@
20
20
  $(document).on 'click', '.disabled, .btn--disabled', (event) ->
21
21
  event.preventDefault()
22
22
 
23
+ $(document).on 'click', '.enable-notifications .banner__dismiss', (event) ->
24
+ $(event.target).closest('.banner').addClass('hidden')
25
+ localStorage.setItem("dismissed-enable-notifications", true)
26
+
23
27
  jQuery ->
24
- $notificationNotice = $('.notifications')
28
+ if(localStorage.getItem("dismissed-enable-notifications"))
29
+ return
30
+ $notificationNotice = $('.enable-notifications')
25
31
 
26
32
  if $.notifyCheck() == $.NOTIFY_NOT_ALLOWED
27
33
  $button = $notificationNotice.find('button')
@@ -22,6 +22,9 @@
22
22
  &.btn--disabled {
23
23
  box-shadow: 0 0 0 1px rgba(#000, 0.15);
24
24
  }
25
+ &.btn--warning {
26
+ background-color: $yellow;
27
+ }
25
28
  }
26
29
 
27
30
  .btn--disabled {
@@ -11,7 +11,7 @@ $red: #F39494;
11
11
  $bright-red: #F73B3B;
12
12
  $light-red: #ffd9d6;
13
13
  $yellow: #FFC66C;
14
- $dark_yellow: #CEA61B;
14
+ $dark-yellow: #CEA61B;
15
15
  $orange: #FFAD4C;
16
16
  $light-orange: #ffebcc;
17
17
  $slate: #2E343A;
@@ -95,7 +95,7 @@
95
95
  }
96
96
 
97
97
  .status-item--pending & {
98
- color: $dark_yellow;
98
+ color: $dark-yellow;
99
99
  }
100
100
 
101
101
  .status-item--error & {
@@ -201,7 +201,7 @@
201
201
 
202
202
  &[data-status="scheduled"],
203
203
  &[data-status="running"] {
204
- border-color: $dark_yellow;
204
+ border-color: $dark-yellow;
205
205
  }
206
206
  }
207
207
 
@@ -60,7 +60,7 @@ pre {
60
60
  &.nowrap {
61
61
  white-space: pre;
62
62
  margin-top: -.25rem;
63
- margin-bottom: -3rem;
63
+ margin-bottom: 0rem;
64
64
  }
65
65
  }
66
66
 
@@ -17,11 +17,7 @@ module Shipit
17
17
  return if flash.empty? && !stale?(last_modified: @stack.updated_at)
18
18
 
19
19
  @tasks = @stack.tasks.order(id: :desc).preload(:since_commit, :until_commit, :user).limit(10)
20
- @commits = @stack.commits.reachable.preload(:author, :statuses).order(id: :desc)
21
- if deployed_commit = @stack.last_deployed_commit
22
- @commits = @commits.where('id > ?', deployed_commit.id)
23
- end
24
- @commits = @commits.to_a
20
+ @commits = @stack.undeployed_commits { |scope| scope.preload(:author, :statuses) }
25
21
  end
26
22
 
27
23
  def lookup
@@ -19,9 +19,7 @@ module Shipit
19
19
  end
20
20
 
21
21
  def include_plugins(stack)
22
- stack.plugins.flat_map do |plugin, config|
23
- plugin_tags(plugin, config)
24
- end.join.html_safe
22
+ safe_join(stack.plugins.flat_map { |plugin, config| plugin_tags(plugin, config) })
25
23
  end
26
24
 
27
25
  def plugin_tags(plugin, config)
@@ -33,7 +31,7 @@ module Shipit
33
31
  end
34
32
 
35
33
  def missing_github_oauth_message
36
- (<<-MESSAGE).html_safe
34
+ <<-MESSAGE.html_safe
37
35
  Shipit requires a GitHub application to authenticate users.
38
36
  If you haven't created an application on GitHub yet, you can do so in the
39
37
  #{link_to 'Settings', Shipit.github_url('/settings/applications/new'), target: '_blank'}
@@ -42,21 +40,21 @@ module Shipit
42
40
  end
43
41
 
44
42
  def missing_github_oauth_id_message
45
- (<<-MESSAGE).html_safe
43
+ <<-MESSAGE.html_safe
46
44
  Copy the Client ID from your GitHub application,
47
45
  and paste it into the secrets.yml file under <code>github_oauth.id</code>.
48
46
  MESSAGE
49
47
  end
50
48
 
51
49
  def missing_github_oauth_secret_message
52
- (<<-MESSAGE).html_safe
50
+ <<-MESSAGE.html_safe
53
51
  Copy the Client Secret from your GitHub application,
54
52
  and paste it into the secrets.yml file under <code>github_oauth.secret</code>.
55
53
  MESSAGE
56
54
  end
57
55
 
58
56
  def missing_github_api_credentials_message
59
- (<<-MESSAGE).html_safe
57
+ <<-MESSAGE.html_safe
60
58
  Shipit needs API access to GitHub. You can
61
59
  #{link_to 'create an access token', Shipit.github_url('/settings/tokens'), target: '_blank'}
62
60
  with the following permissions:
@@ -66,14 +64,14 @@ module Shipit
66
64
  end
67
65
 
68
66
  def missing_redis_url_message
69
- (<<-MESSAGE).html_safe
67
+ <<-MESSAGE.html_safe
70
68
  Shipit needs a Redis server. Please configure the Redis URL in the secrets.yml file of your app,
71
69
  under the key <code>redis_url</code>.
72
70
  MESSAGE
73
71
  end
74
72
 
75
73
  def missing_host_message
76
- (<<-MESSAGE).html_safe
74
+ <<-MESSAGE.html_safe
77
75
  Shipit needs the host of the application before generating links in background jobs.
78
76
  Add the host name to the secrets.yml file, under the <code>host</code> key.
79
77
  MESSAGE
@@ -2,33 +2,34 @@ module Shipit
2
2
  module StacksHelper
3
3
  COMMIT_TITLE_LENGTH = 79
4
4
 
5
- def redeploy_button(commit)
6
- url = new_stack_deploy_path(@stack, sha: commit.sha)
5
+ def redeploy_button(deployed_commit)
6
+ commit = UndeployedCommit.new(deployed_commit, 0)
7
+ url = new_stack_deploy_path(commit.stack, sha: commit.sha)
7
8
  classes = %W(btn btn--primary deploy-action #{commit.state})
8
9
 
9
10
  unless commit.stack.deployable?
10
- classes.push(ignore_lock? ? 'btn--warning' : 'btn--disabled')
11
+ classes.push(bypass_safeties? ? 'btn--warning' : 'btn--disabled')
11
12
  end
12
13
 
13
- caption = 'Redeploy'
14
- caption = 'Locked' if commit.stack.locked? && !ignore_lock?
15
- caption = 'Deploy in progress...' if commit.stack.active_task?
16
-
17
- link_to(caption, url, class: classes)
14
+ link_to(t("redeploy_button.caption.#{commit.redeploy_state(bypass_safeties?)}"), url, class: classes)
18
15
  end
19
16
 
20
- def ignore_lock?
17
+ def bypass_safeties?
21
18
  params[:force].present?
22
19
  end
23
20
 
24
21
  def deploy_button(commit)
25
- url = new_stack_deploy_path(@stack, sha: commit.sha)
22
+ url = new_stack_deploy_path(commit.stack, sha: commit.sha)
26
23
  classes = %W(btn btn--primary deploy-action #{commit.state})
27
- if deploy_button_disabled?(commit)
28
- classes.push(params[:force].present? ? 'btn--warning' : 'btn--disabled')
24
+ data = {}
25
+ if commit.deploy_disallowed?
26
+ classes.push(bypass_safeties? ? 'btn--warning' : 'btn--disabled')
27
+ elsif commit.deploy_discouraged?
28
+ classes.push('btn--warning')
29
+ data[:tooltip] = t('deploy_button.hint.max_commits', maximum: commit.stack.maximum_commits_per_deploy)
29
30
  end
30
31
 
31
- link_to(deploy_button_caption(commit), url, class: classes)
32
+ link_to(t("deploy_button.caption.#{commit.deploy_state(bypass_safeties?)}"), url, class: classes, data: data)
32
33
  end
33
34
 
34
35
  def github_change_url(commit)
@@ -59,20 +60,5 @@ module Shipit
59
60
  def render_raw_commit_id_link(commit)
60
61
  link_to(commit.short_sha, github_commit_url(commit), target: '_blank', class: 'number')
61
62
  end
62
-
63
- private
64
-
65
- def deploy_button_disabled?(commit)
66
- !commit.deployable? || !commit.stack.deployable?
67
- end
68
-
69
- def deploy_button_caption(commit)
70
- state = commit.status.state
71
- state = 'locked' if commit.stack.locked? && !ignore_lock?
72
- if commit.deployable?
73
- state = commit.stack.active_task? ? 'deploying' : 'enabled'
74
- end
75
- t("deploy_button.caption.#{state}")
76
- end
77
63
  end
78
64
  end
@@ -6,7 +6,7 @@ module Shipit
6
6
 
7
7
  def perform(stack)
8
8
  return unless stack.continuous_deployment?
9
- stack.trigger_continuous_deploy
9
+ stack.trigger_continuous_delivery
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,9 @@
1
+ module Shipit
2
+ class UpdateEstimatedDeployDurationJob < BackgroundJob
3
+ queue_as :default
4
+
5
+ def perform(stack)
6
+ stack.update_estimated_deploy_duration!
7
+ end
8
+ end
9
+ end
@@ -1,7 +1,7 @@
1
1
  module Shipit
2
2
  class CommitChecks
3
3
  OUTPUT_TTL = 10.minutes.to_i
4
- FINAL_STATUSES = %w(failed error success)
4
+ FINAL_STATUSES = %w(failed error success).freeze
5
5
 
6
6
  def initialize(commit)
7
7
  @commit = commit
@@ -18,7 +18,7 @@ module Shipit
18
18
  'success' => 'success',
19
19
  'error' => 'error',
20
20
  'aborted' => 'error',
21
- }
21
+ }.freeze
22
22
 
23
23
  def append_status(task_status)
24
24
  if github_status = GITHUB_STATUSES[task_status]
@@ -1,7 +1,7 @@
1
1
  module Shipit
2
2
  class DeploySpec
3
3
  module BundlerDiscovery
4
- DEFAULT_BUNDLER_WITHOUT = %w(default production development test staging benchmark debug)
4
+ DEFAULT_BUNDLER_WITHOUT = %w(default production development test staging benchmark debug).freeze
5
5
 
6
6
  def discover_dependencies_steps
7
7
  discover_bundler || super
@@ -44,7 +44,12 @@ module Shipit
44
44
  },
45
45
  'plugins' => plugins,
46
46
  'dependencies' => {'override' => dependencies_steps},
47
- 'deploy' => {'override' => deploy_steps, 'variables' => deploy_variables.map(&:to_h)},
47
+ 'deploy' => {
48
+ 'override' => deploy_steps,
49
+ 'variables' => deploy_variables.map(&:to_h),
50
+ 'max_commits' => maximum_commits_per_deploy,
51
+ 'interval' => pause_between_deploys,
52
+ },
48
53
  'rollback' => {'override' => rollback_steps},
49
54
  'fetch' => fetch_deployed_revision_steps,
50
55
  'tasks' => cacheable_tasks,
@@ -63,6 +63,14 @@ module Shipit
63
63
  end
64
64
  alias_method :dependencies_steps!, :dependencies_steps
65
65
 
66
+ def maximum_commits_per_deploy
67
+ config('deploy', 'max_commits')
68
+ end
69
+
70
+ def pause_between_deploys
71
+ Duration.parse(config('deploy', 'interval') { 0 })
72
+ end
73
+
66
74
  def deploy_steps
67
75
  around_steps('deploy') do
68
76
  config('deploy', 'override') { discover_deploy_steps }
@@ -143,6 +151,10 @@ module Shipit
143
151
  config('machine', 'cleanup') { true }
144
152
  end
145
153
 
154
+ def links
155
+ config('links') { {} }
156
+ end
157
+
146
158
  private
147
159
 
148
160
  def around_steps(section)
@@ -1,5 +1,13 @@
1
1
  module Shipit
2
- class Duration
2
+ class Duration < ActiveSupport::Duration
3
+ FORMAT = /
4
+ \A
5
+ (?<days>\d+d)?
6
+ (?<hours>\d+h)?
7
+ (?<minutes>\d+m)?
8
+ (?<seconds>\d+s?)?
9
+ \z
10
+ /x
3
11
  UNITS = {
4
12
  's' => :seconds,
5
13
  'm' => :minutes,
@@ -7,21 +15,33 @@ module Shipit
7
15
  'd' => :days,
8
16
  }.freeze
9
17
 
10
- def initialize(seconds)
11
- @seconds = seconds
18
+ class << self
19
+ def parse(value)
20
+ unless match = FORMAT.match(value.to_s)
21
+ raise ArgumentError, "not a duration: #{value.inspect}"
22
+ end
23
+ parts = []
24
+ UNITS.values.each do |unit|
25
+ if value = match[unit]
26
+ parts << [unit, value.to_i]
27
+ end
28
+ end
29
+
30
+ time = ::Time.current
31
+ new(time.advance(parts.to_h) - time, parts)
32
+ end
12
33
  end
13
34
 
14
- def to_i
15
- @seconds.to_i
35
+ def initialize(value, parts = [[:seconds, value]])
36
+ super
16
37
  end
17
38
 
18
39
  def to_s
19
- seconds = to_i
20
- days, seconds = seconds.divmod(1.day.to_i)
40
+ days, seconds_left = value.divmod(1.day.to_i)
21
41
  if days > 0
22
- "#{days}d#{Time.at(seconds).utc.strftime('%Hh%Mm%Ss')}"
42
+ "#{days}d#{Time.at(seconds_left).utc.strftime('%Hh%Mm%Ss')}"
23
43
  else
24
- Time.at(seconds).utc.strftime('%Hh%Mm%Ss')[/[^0a-z]\w+/] || '0s'
44
+ Time.at(value).utc.strftime('%Hh%Mm%Ss')[/[^0a-z]\w+/] || '0s'
25
45
  end
26
46
  end
27
47
  end
@@ -45,8 +45,6 @@ module Shipit
45
45
  create_hook!
46
46
  end
47
47
 
48
- private
49
-
50
48
  def endpoint_url
51
49
  raise NotImplementedError.new('Subclasses must implement a `endpoint_url` method')
52
50
  end
@@ -21,7 +21,7 @@ module Shipit
21
21
  REPO_OWNER_MAX_SIZE = 39
22
22
  REPO_NAME_MAX_SIZE = 100
23
23
  ENVIRONMENT_MAX_SIZE = 50
24
- REQUIRED_HOOKS = %i(push status)
24
+ REQUIRED_HOOKS = %i(push status).freeze
25
25
 
26
26
  has_many :commits, dependent: :destroy
27
27
  has_many :tasks, dependent: :destroy
@@ -62,13 +62,19 @@ module Shipit
62
62
  validates :lock_reason, length: {maximum: 4096}
63
63
 
64
64
  serialize :cached_deploy_spec, DeploySpec
65
- delegate :find_task_definition, :supports_rollback?,
65
+ delegate :find_task_definition, :supports_rollback?, :links,
66
66
  :supports_fetch_deployed_revision?, to: :cached_deploy_spec, allow_nil: true
67
67
 
68
68
  def self.refresh_deployed_revisions
69
69
  find_each.select(&:supports_fetch_deployed_revision?).each(&:async_refresh_deployed_revision)
70
70
  end
71
71
 
72
+ def self.schedule_continuous_delivery
73
+ where(continuous_deployment: true).find_each do |stack|
74
+ ContinuousDeliveryJob.perform_later(stack)
75
+ end
76
+ end
77
+
72
78
  def undeployed_commits?
73
79
  undeployed_commits_count > 0
74
80
  end
@@ -103,11 +109,25 @@ module Shipit
103
109
  deploy
104
110
  end
105
111
 
106
- def trigger_continuous_deploy
112
+ def trigger_continuous_delivery
107
113
  return unless deployable?
108
- if commit = last_deployable_commit
114
+ return if deployed_too_recently?
115
+
116
+ if commit = next_commit_to_deploy
109
117
  return if commit.deployed?
110
- trigger_deploy(commit, commit.committer)
118
+ trigger_deploy(commit, Shipit.user)
119
+ end
120
+ end
121
+
122
+ def next_commit_to_deploy
123
+ commits_to_deploy = commits.order(id: :asc).newer_than(last_deployed_commit).reachable.preload(:statuses)
124
+ commits_to_deploy = commits_to_deploy.limit(maximum_commits_per_deploy) if maximum_commits_per_deploy
125
+ commits_to_deploy.to_a.reverse.find(&:deployable?)
126
+ end
127
+
128
+ def deployed_too_recently?
129
+ if task = last_active_task
130
+ task.ended_at? && (task.ended_at + pause_between_deploys).future?
111
131
  end
112
132
  end
113
133
 
@@ -144,6 +164,8 @@ module Shipit
144
164
  'locked'
145
165
  else
146
166
  significant_statuses = undeployed_commits.map(&:significant_status)
167
+ significant_statuses << last_deployed_commit.significant_status unless last_deployed_commit.blank?
168
+
147
169
  last_finalized_status = significant_statuses.reject { |s| %w(pending unknown).include?(s.state) }.first
148
170
  last_finalized_status.try!(:simple_state) || 'pending'
149
171
  end
@@ -155,13 +177,19 @@ module Shipit
155
177
  end
156
178
 
157
179
  def undeployed_commits
158
- commits.reachable.newer_than(last_deployed_commit).order(id: :desc)
180
+ scope = commits.reachable.newer_than(last_deployed_commit).order(id: :asc)
181
+ yield scope if block_given?
182
+ scope.map.with_index { |c, i| UndeployedCommit.new(c, i) }.reverse
159
183
  end
160
184
 
161
185
  def last_successful_deploy
162
186
  deploys_and_rollbacks.success.order(created_at: :desc).first
163
187
  end
164
188
 
189
+ def last_active_task
190
+ tasks.exclusive.last
191
+ end
192
+
165
193
  def last_deployed_commit
166
194
  if deploy = last_successful_deploy
167
195
  deploy.until_commit
@@ -170,10 +198,6 @@ module Shipit
170
198
  end
171
199
  end
172
200
 
173
- def last_deployable_commit
174
- commits.order(id: :desc).newer_than(last_deployed_commit).reachable.preload(:statuses).to_a.find(&:deployable?)
175
- end
176
-
177
201
  def filter_visible_statuses(statuses)
178
202
  statuses.reject { |s| hidden_statuses.include?(s.context) }
179
203
  end
@@ -288,7 +312,8 @@ module Shipit
288
312
  end
289
313
 
290
314
  delegate :plugins, :task_definitions, :hidden_statuses, :required_statuses, :soft_failing_statuses,
291
- :deploy_variables, :filter_task_envs, :filter_deploy_envs, to: :cached_deploy_spec
315
+ :deploy_variables, :filter_task_envs, :filter_deploy_envs, :maximum_commits_per_deploy,
316
+ :pause_between_deploys, to: :cached_deploy_spec
292
317
 
293
318
  def monitoring?
294
319
  monitoring.present?
@@ -356,6 +381,18 @@ module Shipit
356
381
  super
357
382
  end
358
383
 
384
+ def async_update_estimated_deploy_duration
385
+ UpdateEstimatedDeployDurationJob.perform_later(self)
386
+ end
387
+
388
+ def update_estimated_deploy_duration!
389
+ update!(estimated_deploy_duration: Stat.p90(recent_deploys_durations) || 1)
390
+ end
391
+
392
+ def recent_deploys_durations
393
+ tasks.where(type: 'Shipit::Deploy').success.order(id: :desc).limit(100).durations
394
+ end
395
+
359
396
  private
360
397
 
361
398
  def clear_cache
@@ -13,7 +13,7 @@ module Shipit
13
13
  belongs_to :until_commit, class_name: 'Commit'
14
14
  belongs_to :since_commit, class_name: 'Commit'
15
15
 
16
- has_many :chunks, -> { order(:id) }, class_name: 'OutputChunk', dependent: :destroy
16
+ has_many :chunks, -> { order(:id) }, class_name: 'OutputChunk', dependent: :delete_all
17
17
 
18
18
  serialize :definition, TaskDefinition
19
19
  serialize :env, Hash
@@ -28,6 +28,12 @@ module Shipit
28
28
  after_save :record_status_change
29
29
  after_commit :emit_hooks
30
30
 
31
+ class << self
32
+ def durations
33
+ pluck(:started_at, :ended_at).select { |s, e| s && e }.map { |s, e| e - s }
34
+ end
35
+ end
36
+
31
37
  state_machine :status, initial: :pending do
32
38
  before_transition any => :running do |task|
33
39
  task.started_at ||= Time.now.utc
@@ -45,6 +51,10 @@ module Shipit
45
51
  task.update!(confirmations: 0)
46
52
  end
47
53
 
54
+ after_transition any => :success do |task|
55
+ task.async_update_estimated_deploy_duration
56
+ end
57
+
48
58
  event :run do
49
59
  transition pending: :running
50
60
  end
@@ -101,7 +111,8 @@ module Shipit
101
111
  error!
102
112
  end
103
113
 
104
- delegate :acquire_git_cache_lock, :async_refresh_deployed_revision, to: :stack
114
+ delegate :acquire_git_cache_lock, :async_refresh_deployed_revision, :async_update_estimated_deploy_duration,
115
+ to: :stack
105
116
 
106
117
  delegate :checklist, to: :definition
107
118