shipit-engine 0.11.0 → 0.12.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -37
  3. data/app/assets/javascripts/task/tty.js.coffee +40 -22
  4. data/app/assets/stylesheets/_pages/_deploy.scss +1 -0
  5. data/app/controllers/shipit/api/locks_controller.rb +3 -3
  6. data/app/controllers/shipit/api/stacks_controller.rb +14 -1
  7. data/app/controllers/shipit/commit_checks_controller.rb +9 -2
  8. data/app/controllers/shipit/shipit_controller.rb +1 -1
  9. data/app/controllers/shipit/stacks_controller.rb +11 -5
  10. data/app/controllers/shipit/status_controller.rb +1 -1
  11. data/app/controllers/shipit/webhooks_controller.rb +15 -5
  12. data/app/helpers/shipit/deploys_helper.rb +1 -1
  13. data/app/helpers/shipit/shipit_helper.rb +2 -0
  14. data/app/jobs/shipit/perform_commit_checks_job.rb +2 -0
  15. data/app/jobs/shipit/perform_task_job.rb +18 -8
  16. data/app/models/shipit/commit.rb +22 -12
  17. data/app/models/shipit/commit_checks.rb +20 -53
  18. data/app/models/shipit/deploy.rb +1 -1
  19. data/app/models/shipit/ephemeral_commit_checks.rb +76 -0
  20. data/app/models/shipit/stack.rb +40 -10
  21. data/app/models/shipit/status.rb +14 -8
  22. data/app/models/shipit/status_group.rb +1 -1
  23. data/app/models/shipit/task.rb +2 -0
  24. data/app/models/shipit/unknown_status.rb +4 -0
  25. data/app/models/shipit/user.rb +9 -1
  26. data/app/serializers/shipit/stack_serializer.rb +6 -1
  27. data/app/views/shipit/commit_checks/_checks.html.erb +13 -0
  28. data/app/views/shipit/commit_checks/show.html.erb +5 -0
  29. data/app/views/shipit/stacks/_header.html.erb +1 -1
  30. data/app/views/shipit/stacks/show.html.erb +15 -0
  31. data/config/routes.rb +2 -1
  32. data/db/migrate/20140226233935_create_baseline.rb +10 -10
  33. data/db/migrate/20160802092812_add_continuous_delivery_delayed_since_to_stacks.rb +5 -0
  34. data/db/migrate/20160822131405_add_locked_since_to_stacks.rb +5 -0
  35. data/lib/shipit.rb +4 -0
  36. data/lib/shipit/command.rb +1 -1
  37. data/lib/shipit/commands.rb +10 -7
  38. data/lib/shipit/csv_serializer.rb +1 -1
  39. data/lib/shipit/stack_commands.rb +17 -5
  40. data/lib/shipit/task_commands.rb +2 -2
  41. data/lib/shipit/version.rb +1 -1
  42. data/lib/snippets/push-to-heroku +20 -16
  43. data/test/controllers/api/deploys_controller_test.rb +8 -8
  44. data/test/controllers/api/hooks_controller_test.rb +11 -9
  45. data/test/controllers/api/locks_controller_test.rb +16 -6
  46. data/test/controllers/api/outputs_controller_test.rb +1 -1
  47. data/test/controllers/api/stacks_controller_test.rb +48 -3
  48. data/test/controllers/api/tasks_controller_test.rb +6 -6
  49. data/test/controllers/commit_checks_controller_test.rb +3 -3
  50. data/test/controllers/deploys_controller_test.rb +13 -13
  51. data/test/controllers/rollbacks_controller_test.rb +7 -7
  52. data/test/controllers/stacks_controller_test.rb +36 -29
  53. data/test/controllers/tasks_controller_test.rb +14 -14
  54. data/test/controllers/webhooks_controller_test.rb +16 -25
  55. data/test/dummy/config/application.rb +0 -3
  56. data/test/dummy/config/environments/test.rb +2 -2
  57. data/test/dummy/data/stacks/byroot/junk/production/git/bar.txt +2 -0
  58. data/test/dummy/data/stacks/byroot/junk/production/git/bin/slow +7 -0
  59. data/test/dummy/data/stacks/byroot/junk/production/git/bin/timeout +10 -0
  60. data/test/dummy/data/stacks/byroot/junk/production/git/dkfdsf +0 -0
  61. data/test/dummy/data/stacks/byroot/junk/production/git/dskjfsd +0 -0
  62. data/test/dummy/data/stacks/byroot/junk/production/git/dslkjfjsdf +0 -0
  63. data/test/dummy/data/stacks/byroot/junk/production/git/plopfizz +0 -0
  64. data/test/dummy/data/stacks/byroot/junk/production/git/sd +0 -0
  65. data/test/dummy/data/stacks/byroot/junk/production/git/sdkfjsdf +1 -0
  66. data/test/dummy/data/stacks/byroot/junk/production/git/sdlfjsdfdsfj +0 -0
  67. data/test/dummy/data/stacks/byroot/junk/production/git/sdlkfjsdlkfjsdlkfjdsfsdfksdfjsldkfjsdlkfjsdf +0 -0
  68. data/test/dummy/data/stacks/byroot/junk/production/git/shipit.yml +21 -0
  69. data/test/dummy/data/stacks/byroot/junk/production/git/toto.txt +2 -0
  70. data/test/dummy/db/development.sqlite3 +0 -0
  71. data/test/dummy/db/schema.rb +15 -13
  72. data/test/dummy/db/test.sqlite3 +0 -0
  73. data/test/fixtures/shipit/commit_deployments.yml +8 -8
  74. data/test/fixtures/shipit/output_chunks.yml +12 -12
  75. data/test/fixtures/shipit/tasks.yml +9 -1
  76. data/test/fixtures/shipit/users.yml +9 -2
  77. data/test/jobs/perform_task_job_test.rb +14 -11
  78. data/test/models/commits_test.rb +33 -14
  79. data/test/models/stacks_test.rb +78 -4
  80. data/test/models/users_test.rb +16 -0
  81. data/test/unit/commands_test.rb +4 -0
  82. data/test/unit/deploy_commands_test.rb +1 -1
  83. metadata +133 -34
  84. data/app/jobs/shipit/git_mirror_update_job.rb +0 -14
  85. data/app/views/shipit/deploys/_checks.html.erb +0 -11
@@ -42,7 +42,7 @@ module Shipit
42
42
  teams = Shipit.github_teams
43
43
  unless teams.empty? || current_user.teams.where(id: teams).exists?
44
44
  team_list = teams.map(&:handle).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')
45
- render text: "You must be a member of #{team_list} to access this application.", status: :forbidden
45
+ render plain: "You must be a member of #{team_list} to access this application.", status: :forbidden
46
46
  end
47
47
  else
48
48
  redirect_to Shipit::Engine.routes.url_helpers.github_authentication_path(origin: request.original_url)
@@ -45,7 +45,7 @@ module Shipit
45
45
  RefreshStatusesJob.perform_later(stack_id: @stack.id)
46
46
  GithubSyncJob.perform_later(stack_id: @stack.id)
47
47
  flash[:success] = 'Refresh scheduled'
48
- redirect_to request.referer ? :back : stack_path(@stack)
48
+ redirect_to request.referer.presence || stack_path(@stack)
49
49
  end
50
50
 
51
51
  def update
@@ -53,6 +53,14 @@ module Shipit
53
53
  unless @stack.update(update_params)
54
54
  options = {flash: {warning: @stack.errors.full_messages.to_sentence}}
55
55
  end
56
+
57
+ reason = params[:stack][:lock_reason]
58
+ if reason.present?
59
+ @stack.lock(reason, current_user)
60
+ elsif @stack.locked?
61
+ @stack.unlock
62
+ end
63
+
56
64
  redirect_to(params[:return_to].presence || stack_settings_path(@stack), options)
57
65
  end
58
66
 
@@ -77,10 +85,8 @@ module Shipit
77
85
  end
78
86
 
79
87
  def update_params
80
- params.require(:stack).permit(:deploy_url, :lock_reason, :environment,
81
- :continuous_deployment, :ignore_ci).tap do |params|
82
- params[:lock_author_id] = params[:lock_reason].present? ? current_user.id : nil
83
- end
88
+ params.require(:stack).permit(:deploy_url, :environment,
89
+ :continuous_deployment, :ignore_ci)
84
90
  end
85
91
  end
86
92
  end
@@ -1,7 +1,7 @@
1
1
  module Shipit
2
2
  class StatusController < ActionController::Base
3
3
  def version
4
- render text: Shipit.revision
4
+ render plain: Shipit.revision
5
5
  end
6
6
  end
7
7
  end
@@ -9,17 +9,27 @@ module Shipit
9
9
 
10
10
  if branch == stack.branch
11
11
  GithubSyncJob.perform_later(stack_id: stack.id)
12
- GitMirrorUpdateJob.perform_later(stack)
13
12
  end
14
13
 
15
14
  head :ok
16
15
  end
17
16
 
17
+ params do
18
+ requires :sha, String
19
+ requires :state, String
20
+ accepts :description, String
21
+ accepts :target_url, String
22
+ accepts :context, String
23
+ accepts :created_at, String
24
+
25
+ accepts :branches, Array do
26
+ requires :name, String
27
+ end
28
+ end
18
29
  def state
19
- branches = params[:branches] || []
20
- if branches.find { |branch| branch[:name] == stack.branch }
21
- commit = stack.commits.find_by_sha!(params[:sha])
22
- commit.add_status(params.permit(:state, :description, :target_url, :context, :created_at))
30
+ if params.branches.map(&:name).include?(stack.branch)
31
+ commit = stack.commits.find_by_sha!(params.sha)
32
+ commit.create_status_from_github!(params)
23
33
  end
24
34
  head :ok
25
35
  end
@@ -12,7 +12,7 @@ module Shipit
12
12
 
13
13
  def render_checks(commit)
14
14
  return unless commit.stack.checks?
15
- render 'shipit/deploys/checks', commit: commit
15
+ render 'shipit/commit_checks/checks', commit: commit
16
16
  end
17
17
 
18
18
  def render_monitoring_panel(panel_spec)
@@ -36,6 +36,8 @@ module Shipit
36
36
  If you haven't created an application on GitHub yet, you can do so in the
37
37
  #{link_to 'Settings', Shipit.github_url('/settings/applications/new'), target: '_blank'}
38
38
  section of your profile. You can also create applications for organizations.
39
+ When setting up your application in Github, set the Homepage URL to your domain
40
+ and the Authorization callback URL to '<yourdomain>/github/auth/github/callback'.
39
41
  MESSAGE
40
42
  end
41
43
 
@@ -1,5 +1,7 @@
1
1
  module Shipit
2
2
  class PerformCommitChecksJob < BackgroundJob
3
+ include BackgroundJob::Unique
4
+
3
5
  def perform(commit:)
4
6
  commit.checks.run
5
7
  end
@@ -53,24 +53,26 @@ module Shipit
53
53
 
54
54
  def perform_task
55
55
  Bundler.with_clean_env do
56
- capture_all @commands.install_dependencies
57
- capture_all @commands.perform
56
+ capture_all! @commands.install_dependencies
57
+ capture_all! @commands.perform
58
58
  end
59
59
  end
60
60
 
61
61
  def checkout_repository
62
62
  @task.acquire_git_cache_lock do
63
- capture @commands.fetch
64
- capture @commands.clone
63
+ unless capture @commands.fetched?(@task.until_commit)
64
+ capture! @commands.fetch
65
+ end
65
66
  end
66
- capture @commands.checkout(@task.until_commit)
67
+ capture! @commands.clone
68
+ capture! @commands.checkout(@task.until_commit)
67
69
  end
68
70
 
69
- def capture_all(commands)
70
- commands.map { |c| capture(c) }
71
+ def capture_all!(commands)
72
+ commands.map { |c| capture!(c) }
71
73
  end
72
74
 
73
- def capture(command)
75
+ def capture!(command)
74
76
  command.start do
75
77
  @task.ping
76
78
  check_for_abort
@@ -81,6 +83,14 @@ module Shipit
81
83
  @task.write(line)
82
84
  end
83
85
  @task.write("\n")
86
+ command.success?
87
+ end
88
+
89
+ def capture(command)
90
+ capture!(command)
91
+ command.success?
92
+ rescue Command::Error
93
+ false
84
94
  end
85
95
  end
86
96
  end
@@ -82,22 +82,14 @@ module Shipit
82
82
  def refresh_statuses!
83
83
  github_statuses = stack.handle_github_redirections { Shipit.github_api.statuses(github_repo_name, sha) }
84
84
  github_statuses.each do |status|
85
- statuses.replicate_from_github!(status)
85
+ create_status_from_github!(status)
86
86
  end
87
87
  end
88
88
 
89
- def add_status(status_attributes)
90
- previous_status = significant_status
91
- statuses.create!(status_attributes)
92
- reload # to get the statuses into the right order (since sorted :desc)
93
- new_status = significant_status
94
-
95
- payload = {commit: self, stack: stack, status: new_status.state}
96
- Hook.emit(:commit_status, stack, payload.merge(commit_status: new_status)) if previous_status != new_status
97
- if previous_status.simple_state != new_status.simple_state && !new_status.pending?
98
- Hook.emit(:deployable_status, stack, payload.merge(deployable_status: new_status))
89
+ def create_status_from_github!(github_status)
90
+ add_status do
91
+ statuses.replicate_from_github!(github_status)
99
92
  end
100
- new_status
101
93
  end
102
94
 
103
95
  def checks
@@ -192,8 +184,26 @@ module Shipit
192
184
  stack.last_deployed_commit.id >= id
193
185
  end
194
186
 
187
+ def deploy_failed?
188
+ stack.deploys.unsuccessful.where(until_commit_id: id).any?
189
+ end
190
+
195
191
  private
196
192
 
193
+ def add_status
194
+ previous_status = significant_status
195
+ yield
196
+ reload # to get the statuses into the right order (since sorted :desc)
197
+ new_status = significant_status
198
+
199
+ payload = {commit: self, stack: stack, status: new_status.state}
200
+ Hook.emit(:commit_status, stack, payload.merge(commit_status: new_status)) if previous_status != new_status
201
+ if previous_status.simple_state != new_status.simple_state && (!new_status.pending? || previous_status.unknown?)
202
+ Hook.emit(:deployable_status, stack, payload.merge(deployable_status: new_status))
203
+ end
204
+ new_status
205
+ end
206
+
197
207
  def missing_statuses
198
208
  stack.required_statuses - last_statuses.map(&:context)
199
209
  end
@@ -1,5 +1,5 @@
1
1
  module Shipit
2
- class CommitChecks
2
+ class CommitChecks < EphemeralCommitChecks
3
3
  OUTPUT_TTL = 10.minutes.to_i
4
4
  FINAL_STATUSES = %w(failed error success).freeze
5
5
 
@@ -7,30 +7,28 @@ module Shipit
7
7
  @commit = commit
8
8
  end
9
9
 
10
- def run
11
- self.status = 'running'
12
- commands = StackCommands.new(stack)
13
- commands.with_temporary_working_directory(commit: commit) do |directory|
14
- deploy_spec = DeploySpec::FileSystem.new(directory, stack.environment)
15
- Bundler.with_clean_env do
16
- capture_all(build_commands(deploy_spec.dependencies_steps, chdir: directory))
17
- capture_all(build_commands(deploy_spec.review_checks, chdir: directory))
18
- end
19
- end
20
- rescue Command::Error
21
- self.status = 'failed'
22
- rescue
23
- self.status = 'error'
24
- raise
25
- else
26
- self.status = 'success'
10
+ def synchronize(&block)
11
+ @lock ||= Redis::Lock.new('lock', redis, expiration: 1, timeout: 2)
12
+ @lock.lock(&block)
27
13
  end
28
14
 
29
15
  def schedule
30
- if redis.set('output', '', ex: OUTPUT_TTL, nx: true)
31
- self.status = 'scheduled'
32
- PerformCommitChecksJob.perform_later(commit: commit)
16
+ return false if redis.get('status').present?
17
+ synchronize do
18
+ return false if redis.get('status').present?
19
+
20
+ initialize_redis_state
21
+ end
22
+ PerformCommitChecksJob.perform_later(commit: commit)
23
+ true
24
+ end
25
+
26
+ def initialize_redis_state
27
+ redis.pipelined do
28
+ redis.set('output', '', ex: OUTPUT_TTL)
29
+ redis.set('status', 'scheduled', ex: OUTPUT_TTL)
33
30
  end
31
+ @status = 'scheduled'
34
32
  end
35
33
 
36
34
  def status
@@ -38,14 +36,10 @@ module Shipit
38
36
  end
39
37
 
40
38
  def status=(status)
41
- redis.set('status', status, ex: OUTPUT_TTL)
39
+ redis.set('status', status)
42
40
  @status = status
43
41
  end
44
42
 
45
- def finished?
46
- FINAL_STATUSES.include?(status)
47
- end
48
-
49
43
  def output(since: 0)
50
44
  redis.getrange('output', since, -1)
51
45
  end
@@ -56,35 +50,8 @@ module Shipit
56
50
 
57
51
  private
58
52
 
59
- def build_commands(commands, chdir:)
60
- commands.map { |c| Command.new(c, env: Shipit.env, chdir: chdir) }
61
- end
62
-
63
- def capture_all(commands)
64
- commands.map { |c| capture(c) }
65
- end
66
-
67
- def capture(command)
68
- command.start
69
- write("$ #{command}\n")
70
- command.stream! do |line|
71
- write(line)
72
- end
73
- rescue Command::Error => error
74
- write(error.message)
75
- raise
76
- ensure
77
- write("\n")
78
- end
79
-
80
- attr_reader :commit
81
-
82
53
  def redis
83
54
  @redis ||= Shipit.redis("commit:#{commit.id}:checks")
84
55
  end
85
-
86
- def stack
87
- @stack ||= commit.stack
88
- end
89
56
  end
90
57
  end
@@ -42,7 +42,7 @@ module Shipit
42
42
  parent_id: id,
43
43
  since_commit: stack.last_deployed_commit,
44
44
  until_commit: until_commit,
45
- env: env || {},
45
+ env: env.try!(:to_h) || {},
46
46
  )
47
47
  end
48
48
 
@@ -0,0 +1,76 @@
1
+ module Shipit
2
+ class EphemeralCommitChecks
3
+ FINAL_STATUSES = %w(failed error success).freeze
4
+
5
+ def initialize(commit)
6
+ @commit = commit
7
+ end
8
+
9
+ attr_accessor :status
10
+ attr_reader :output
11
+
12
+ def run
13
+ self.status = 'running'
14
+ commands = StackCommands.new(stack)
15
+ commands.with_temporary_working_directory(commit: commit) do |directory|
16
+ deploy_spec = DeploySpec::FileSystem.new(directory, stack.environment)
17
+ Bundler.with_clean_env do
18
+ capture_all(build_commands(deploy_spec.dependencies_steps, chdir: directory))
19
+ capture_all(build_commands(deploy_spec.review_checks, chdir: directory))
20
+ end
21
+ end
22
+ self
23
+ rescue Command::Error
24
+ self.status = 'failed'
25
+ self
26
+ rescue
27
+ self.status = 'error'
28
+ raise
29
+ else
30
+ self.status = 'success'
31
+ self
32
+ end
33
+
34
+ def success?
35
+ status == 'success'
36
+ end
37
+
38
+ def finished?
39
+ FINAL_STATUSES.include?(status)
40
+ end
41
+
42
+ def write(output)
43
+ @output ||= ''
44
+ @output += output
45
+ end
46
+
47
+ private
48
+
49
+ def build_commands(commands, chdir:)
50
+ commands.map { |c| Command.new(c, env: Shipit.env, chdir: chdir) }
51
+ end
52
+
53
+ def capture_all(commands)
54
+ commands.map { |c| capture(c) }
55
+ end
56
+
57
+ def capture(command)
58
+ command.start
59
+ write("$ #{command}\n")
60
+ command.stream! do |line|
61
+ write(line)
62
+ end
63
+ rescue Command::Error => error
64
+ write(error.message)
65
+ raise
66
+ ensure
67
+ write("\n")
68
+ end
69
+
70
+ attr_reader :commit
71
+
72
+ def stack
73
+ @stack ||= commit.stack
74
+ end
75
+ end
76
+ end
@@ -58,6 +58,7 @@ module Shipit
58
58
  validates :repo_owner, format: {with: /\A[a-z0-9_\-\.]+\z/}, length: {maximum: REPO_OWNER_MAX_SIZE}
59
59
  validates :repo_name, format: {with: /\A[a-z0-9_\-\.]+\z/}, length: {maximum: REPO_NAME_MAX_SIZE}
60
60
  validates :environment, format: {with: /\A[a-z0-9\-_\:]+\z/}, length: {maximum: ENVIRONMENT_MAX_SIZE}
61
+ validates :deploy_url, format: {with: URI.regexp(%w(http https ssh))}, allow_blank: true
61
62
 
62
63
  validates :lock_reason, length: {maximum: 4096}
63
64
 
@@ -86,7 +87,7 @@ module Shipit
86
87
  definition: find_task_definition(definition_id),
87
88
  until_commit_id: commit.id,
88
89
  since_commit_id: commit.id,
89
- env: filter_task_envs(definition_id, (env || {})),
90
+ env: filter_task_envs(definition_id, (env.try!(:to_h) || {})),
90
91
  )
91
92
  task.enqueue
92
93
  task
@@ -98,7 +99,7 @@ module Shipit
98
99
  user_id: user.id,
99
100
  until_commit: until_commit,
100
101
  since_commit: since_commit,
101
- env: filter_deploy_envs(env || {}),
102
+ env: filter_deploy_envs(env.try!(:to_h) || {}),
102
103
  )
103
104
  end
104
105
 
@@ -106,17 +107,36 @@ module Shipit
106
107
  deploy = build_deploy(*args)
107
108
  deploy.save!
108
109
  deploy.enqueue
110
+ continuous_delivery_resumed!
109
111
  deploy
110
112
  end
111
113
 
114
+ def continuous_delivery_resumed!
115
+ update!(continuous_delivery_delayed_since: nil)
116
+ end
117
+
118
+ def continuous_delivery_delayed?
119
+ continuous_delivery_delayed_since? && continuous_deployment? && checks?
120
+ end
121
+
122
+ def continuous_delivery_delayed!
123
+ touch(:continuous_delivery_delayed_since, :updated_at) unless continuous_delivery_delayed?
124
+ end
125
+
112
126
  def trigger_continuous_delivery
113
- return unless deployable?
114
- return if deployed_too_recently?
127
+ commit = next_commit_to_deploy
115
128
 
116
- if commit = next_commit_to_deploy
117
- return if commit.deployed?
118
- trigger_deploy(commit, Shipit.user)
129
+ if !deployable? || deployed_too_recently? || commit.nil? || commit.deployed?
130
+ continuous_delivery_resumed!
131
+ return
119
132
  end
133
+
134
+ if commit.deploy_failed? || (checks? && !EphemeralCommitChecks.new(commit).run.success?)
135
+ continuous_delivery_delayed!
136
+ return
137
+ end
138
+
139
+ trigger_deploy(commit, Shipit.user)
120
140
  end
121
141
 
122
142
  def next_commit_to_deploy
@@ -183,7 +203,7 @@ module Shipit
183
203
  end
184
204
 
185
205
  def last_successful_deploy
186
- deploys_and_rollbacks.success.order(created_at: :desc).first
206
+ deploys_and_rollbacks.success.last
187
207
  end
188
208
 
189
209
  def last_active_task
@@ -238,12 +258,12 @@ module Shipit
238
258
  File.join(base_path, "git")
239
259
  end
240
260
 
241
- def acquire_git_cache_lock(timeout: 15, &block)
261
+ def acquire_git_cache_lock(timeout: 15, expiration: 60, &block)
242
262
  Redis::Lock.new(
243
263
  "stack:#{id}:git-cache-lock",
244
264
  Shipit.redis,
245
265
  timeout: timeout,
246
- expiration: 60,
266
+ expiration: expiration,
247
267
  ).lock(&block)
248
268
  end
249
269
 
@@ -298,6 +318,16 @@ module Shipit
298
318
  lock_reason.present?
299
319
  end
300
320
 
321
+ def lock(reason, user)
322
+ params = {lock_reason: reason, lock_author: user}
323
+ params[:locked_since] = Time.current if locked_since.nil?
324
+ update!(params)
325
+ end
326
+
327
+ def unlock
328
+ update!(lock_reason: nil, lock_author: nil, locked_since: nil)
329
+ end
330
+
301
331
  def to_param
302
332
  [repo_owner, repo_name, environment].join('/')
303
333
  end