shipit-engine 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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