houston-core 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +69 -68
  3. data/app/adapters/houston/adapters/deployment/engineyard.rb +1 -1
  4. data/app/adapters/houston/adapters/version_control/git_adapter/github_repo.rb +19 -0
  5. data/app/adapters/houston/adapters/version_control/git_adapter/remote_repo.rb +27 -0
  6. data/app/assets/images/drag-grip.png +0 -0
  7. data/app/assets/javascripts/app/infinite_scroll.coffee +1 -1
  8. data/app/assets/javascripts/app/views/_show_sprint_view.coffee +9 -9
  9. data/app/assets/javascripts/application.js +1 -0
  10. data/app/assets/javascripts/core/app.coffee +5 -0
  11. data/app/assets/javascripts/{app → core}/stacked_area_graph.coffee +0 -0
  12. data/app/assets/javascripts/{app → core}/stacked_bar_graph.coffee +0 -0
  13. data/app/assets/javascripts/dashboard.js +1 -0
  14. data/app/assets/javascripts/vendor.js +0 -1
  15. data/app/assets/stylesheets/application/exceptions.scss +3 -1
  16. data/app/assets/stylesheets/application/navigation.scss +84 -21
  17. data/app/assets/stylesheets/application/releases.scss +32 -2
  18. data/app/assets/stylesheets/application/test.scss +34 -0
  19. data/app/assets/stylesheets/core/colors.scss.erb +33 -3
  20. data/app/assets/stylesheets/dashboard/dashboard.scss +11 -7
  21. data/app/assets/stylesheets/variables.scss +3 -0
  22. data/app/concerns/belongs_to_commit.rb +14 -0
  23. data/app/concerns/project_adapter.rb +24 -6
  24. data/app/controllers/api/v1/projects_controller.rb +18 -0
  25. data/app/controllers/api/v1/sprint_tasks_controller.rb +1 -1
  26. data/app/controllers/deploys_controller.rb +1 -0
  27. data/app/controllers/project_tests_controller.rb +49 -19
  28. data/app/controllers/releases_controller.rb +5 -0
  29. data/app/controllers/test_runs_controller.rb +16 -1
  30. data/app/helpers/test_run_helper.rb +24 -0
  31. data/app/models/deploy.rb +13 -0
  32. data/app/models/github/pull_request.rb +39 -4
  33. data/app/models/release.rb +42 -0
  34. data/app/models/task.rb +3 -2
  35. data/app/models/test_run.rb +4 -0
  36. data/app/presenters/project_presenter.rb +28 -0
  37. data/app/views/deploys/show.html.erb +4 -0
  38. data/app/views/github/pulls/index.html.erb +4 -1
  39. data/app/views/layouts/_mobile_navigation.html.erb +14 -17
  40. data/app/views/layouts/_navigation.html.erb +87 -87
  41. data/app/views/layouts/dashboard.html.erb +2 -2
  42. data/app/views/project_tests/index.html.erb +22 -7
  43. data/app/views/releases/_index.html.erb +65 -0
  44. data/app/views/releases/_results.html.erb +47 -0
  45. data/app/views/releases/index.html.erb +29 -65
  46. data/app/views/sprints/dashboard.html.erb +4 -2
  47. data/config/environments/production.rb +1 -1
  48. data/config/environments/test.rb +4 -1
  49. data/config/initializers/add_navigation_renderers.rb +6 -0
  50. data/config/initializers/requirements.rb +1 -0
  51. data/config/routes.rb +3 -0
  52. data/db/migrate/20151226154901_add_search_vector_to_releases.rb +6 -0
  53. data/db/migrate/20151226155305_generate_index_on_releases.rb +5 -0
  54. data/db/migrate/20151228183704_drop_unused_tables.rb +35 -0
  55. data/db/migrate/20160120145757_add_successful_to_deploys.rb +10 -0
  56. data/db/structure.sql +19 -67
  57. data/houston.gemspec +3 -3
  58. data/lib/configuration.rb +4 -2
  59. data/lib/core_ext/array.rb +37 -0
  60. data/lib/houston/version.rb +1 -1
  61. data/test/integration/ci_integration_test.rb +14 -13
  62. data/test/unit/models/project_test.rb +33 -0
  63. data/test/unit/models/pull_request_test.rb +71 -1
  64. metadata +24 -14
  65. data/app/models/historical_head.rb +0 -5
@@ -7,6 +7,20 @@ module BelongsToCommit
7
7
  validates :sha, presence: {message: "must refer to a commit"}
8
8
  end
9
9
 
10
+ module ClassMethods
11
+ def find_by_sha!(sha)
12
+ find_by_sha(sha) || raise(ActiveRecord::RecordNotFound)
13
+ end
14
+
15
+ def find_by_sha(sha)
16
+ with_sha_like(sha).first if sha
17
+ end
18
+
19
+ def with_sha_like(sha)
20
+ where(["sha LIKE ?", "#{sha.strip}%"])
21
+ end
22
+ end
23
+
10
24
  private
11
25
 
12
26
  def identify_commit
@@ -18,6 +18,7 @@ module ProjectAdapter
18
18
  adapter.define_methods!
19
19
 
20
20
  validate adapter.validation_method
21
+ before_update adapter.before_update_method
21
22
  end
22
23
  end
23
24
 
@@ -41,6 +42,18 @@ module ProjectAdapter
41
42
  :"#{attribute_name}_configuration_is_valid"
42
43
  end
43
44
 
45
+ def before_update_method
46
+ :"#{attribute_name}_before_update"
47
+ end
48
+
49
+ def adapter_method
50
+ :"#{attribute_name}_adapter"
51
+ end
52
+
53
+ def params_method
54
+ :"parameters_for_#{attribute_name}_adapter"
55
+ end
56
+
44
57
  def define_methods!
45
58
  model.module_eval <<-RUBY
46
59
  def self.with_#{attribute_name}
@@ -52,24 +65,29 @@ module ProjectAdapter
52
65
  end
53
66
 
54
67
  def #{validation_method}
55
- #{attribute_name}_adapter.errors_with_parameters(self, *parameters_for_#{attribute_name}_adapter.values).each do |attribute, messages|
68
+ #{adapter_method}.errors_with_parameters(self, *#{params_method}.values).each do |attribute, messages|
56
69
  errors.add(attribute, messages) if messages.any?
57
70
  end
58
71
  end
59
72
 
73
+ def #{before_update_method}
74
+ return true unless #{attribute_name}.respond_to?(:before_update)
75
+ #{attribute_name}.before_update(self)
76
+ end
77
+
60
78
  def #{attribute_name}
61
- @#{attribute_name} ||= #{attribute_name}_adapter
62
- .build(self, *parameters_for_#{attribute_name}_adapter.values)
79
+ @#{attribute_name} ||= #{adapter_method}
80
+ .build(self, *#{params_method}.values)
63
81
  .extend(FeatureSupport)
64
82
  end
65
83
 
66
- def parameters_for_#{attribute_name}_adapter
67
- #{attribute_name}_adapter.parameters.each_with_object({}) do |parameter, hash|
84
+ def #{params_method}
85
+ #{adapter_method}.parameters.each_with_object({}) do |parameter, hash|
68
86
  hash[parameter] = extended_attributes[parameter.to_s]
69
87
  end
70
88
  end
71
89
 
72
- def #{attribute_name}_adapter
90
+ def #{adapter_method}
73
91
  #{namespace}.adapter(#{attribute_name}_name)
74
92
  end
75
93
  RUBY
@@ -0,0 +1,18 @@
1
+ module Api
2
+ module V1
3
+ class ProjectsController < ApplicationController
4
+ before_filter :api_authenticate!
5
+ skip_before_filter :verify_authenticity_token
6
+
7
+ rescue_from ActiveRecord::RecordNotFound do
8
+ head 404
9
+ end
10
+
11
+ def index
12
+ @projects = Project.unretired
13
+ render json: ProjectPresenter.new(@projects)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -21,7 +21,7 @@ module Api
21
21
  def mine
22
22
  render json: sprint.tasks
23
23
  .includes(:ticket => :project)
24
- .checked_out_by(current_user)
24
+ .checked_out_by(current_user, during: sprint)
25
25
  .map { |task| present_task(task) }
26
26
  end
27
27
 
@@ -24,6 +24,7 @@ class DeploysController < ApplicationController
24
24
  branch: branch,
25
25
  deployer: deployer,
26
26
  duration: milliseconds,
27
+ successful: params.fetch(:successful, true),
27
28
  completed_at: Time.now)
28
29
 
29
30
  head 200
@@ -2,31 +2,61 @@ class ProjectTestsController < ApplicationController
2
2
 
3
3
  def index
4
4
  @project = Project.find_by_slug! params[:slug]
5
+ @title = "#{@project.name} Tests"
5
6
 
6
7
  head = params.fetch :at, @project.head_sha
7
- commits = params.fetch(:limit, 500).to_i
8
+ commits = params.fetch(:limit, 128).to_i
8
9
 
9
10
  @commits = Houston.benchmark("[project_tests#index] fetch commits") {
10
11
  @project.repo.ancestors(head, including_self: true, limit: commits) }
11
- @runs = @project.test_runs.where(sha: @commits.map(&:sha))
12
+ shas = @commits.map(&:sha)
13
+ test_run_id_by_sha = Hash[@project.test_runs.where(sha: shas).pluck(:sha, :id)]
14
+ test_run_ids = test_run_id_by_sha.values
12
15
 
13
- @tests = @project.tests.order(:suite, :name)
14
- .joins(<<-SQL)
15
- LEFT JOIN LATERAL (
16
- SELECT COUNT(*) FROM test_results
17
- WHERE test_results.test_run_id IN (#{@runs.pluck(:id).join(",")})
18
- AND test_results.status='pass'
19
- AND test_results.test_id=tests.id
20
- ) "passes" ON TRUE
21
- LEFT JOIN LATERAL (
22
- SELECT COUNT(*) FROM test_results
23
- WHERE test_results.test_run_id IN (#{@runs.pluck(:id).join(",")})
24
- AND test_results.status='fail'
25
- AND test_results.test_id=tests.id
26
- ) "fails" ON TRUE
27
- SQL
28
- .where("passes.count + fails.count > 0")
29
- .pluck("tests.id", "tests.suite", "tests.name", "passes.count", "fails.count")
16
+ # We're looking at the history of the tests that exist in the
17
+ # most recent commit; ignore tests that didn't exist before this
18
+ test_ids = Houston.benchmark("[project_tests#index] pick tests") do
19
+ latest_test_run_id = @project.test_runs.where(sha: shas)
20
+ .joins("LEFT OUTER JOIN test_results ON test_runs.id=test_results.test_run_id")
21
+ .where.not("test_results.test_run_id" => nil)
22
+ .limit(1)
23
+ .pluck(:id)
24
+ .first
25
+ TestResult.where(test_run_id: latest_test_run_id).pluck("DISTINCT test_id")
26
+ end
27
+
28
+ # Get all the results that we're going to graph
29
+ test_results = Houston.benchmark("[project_tests#index] load results") do
30
+ TestResult.where(test_run_id: test_run_ids, test_id: test_ids).pluck(:test_run_id, :test_id, :status, :duration)
31
+ end
32
+
33
+ # Now we need to map results to tests
34
+ # and make sure that they're in the same order
35
+ # that the last 200 commits occurred in.
36
+ @tests = Houston.benchmark("[project_tests#index] map results") do
37
+ map = Hash.new { |hash, test_id| hash[test_id] = {} }
38
+ durations = Hash.new { |hash, test_id| hash[test_id] = [] }
39
+ test_results.each do |(test_run_id, test_id, status, duration)|
40
+ map[test_id][test_run_id] = status
41
+ durations[test_id] << duration if duration
42
+ end
43
+
44
+ @project.tests
45
+ .where(id: test_ids)
46
+ .order(:suite, :name)
47
+ .pluck("tests.id", :suite, :name)
48
+ .map do |(id, suite, name)|
49
+ status_by_test_run_id = map[id]
50
+ d = durations[id]
51
+ { id: id,
52
+ suite: suite,
53
+ name: name,
54
+ duration_avg: d.mean,
55
+ duration5: d.percentile(5),
56
+ duration95: d.percentile(95),
57
+ results: shas.map { |sha| status_by_test_run_id[test_run_id_by_sha[sha]] } }
58
+ end
59
+ end
30
60
  end
31
61
 
32
62
  def show
@@ -10,6 +10,11 @@ class ReleasesController < ApplicationController
10
10
  def index
11
11
  @title = "Releases • #{@project.name}"
12
12
  @title << " (#{@environment})" if @environment
13
+ @q = params[:q]
14
+ @q = nil if @q.blank?
15
+ @releases = @project.releases.preload(:deploy).search(@q) if @q
16
+
17
+ render partial: (@q ? "releases/results" : "releases/index") if request.xhr?
13
18
  end
14
19
 
15
20
  def new
@@ -1,5 +1,6 @@
1
1
  class TestRunsController < ApplicationController
2
2
  before_filter :find_test_run
3
+ skip_before_filter :verify_authenticity_token, only: [:save_results]
3
4
 
4
5
  def show
5
6
  @title = "Test Results for #{@test_run.sha[0...8]}"
@@ -25,11 +26,25 @@ class TestRunsController < ApplicationController
25
26
  end
26
27
  end
27
28
 
29
+ def save_results
30
+ results_url = params[:results_url]
31
+
32
+ if results_url.blank?
33
+ message = "#{@project.ci_server_name} is not appropriately configured to build #{@project.name}."
34
+ additional_info = "#{@project.ci_server_name} did not supply 'results_url' when it triggered the post_build hook"
35
+ ProjectNotification.ci_configuration_error(@test_run, message, additional_info: additional_info).deliver!
36
+ return
37
+ end
38
+
39
+ @test_run.completed!(results_url)
40
+ head :ok
41
+ end
42
+
28
43
  private
29
44
 
30
45
  def find_test_run
31
46
  @project = Project.find_by_slug!(params[:slug])
32
- @test_run = @project.test_runs.find_by_sha(params[:commit]) || (raise ActiveRecord::RecordNotFound)
47
+ @test_run = @project.test_runs.find_by_sha!(params[:commit])
33
48
  end
34
49
 
35
50
  end
@@ -49,4 +49,28 @@ module TestRunHelper
49
49
  end
50
50
  end
51
51
 
52
+ def test_results_pass_count(test)
53
+ test[:results].count { |result| result == "pass" }
54
+ end
55
+
56
+ def test_results_fail_count(test)
57
+ test[:results].count { |result| result == "fail" }
58
+ end
59
+
60
+ def test_results_count(test)
61
+ test[:results].count { |result| !result.nil? }
62
+ end
63
+
64
+ def test_results_graph(test)
65
+ html = "<svg class=\"dot-graph\">"
66
+ percent = 100 / test[:results].length.to_f
67
+ left = 0
68
+ test[:results].reverse_each do |result|
69
+ html << "<rect x=\"#{left}%\" y=\"0\" rx=\"2\" ry=\"2\" width=\"#{percent}%\" height=\"100%\" class=\"dot-graph-rect #{result}\" data-index=\"0\"></rect>" if result
70
+ left += percent
71
+ end
72
+ html << "</svg>"
73
+ html.html_safe
74
+ end
75
+
52
76
  end
@@ -59,6 +59,14 @@ class Deploy < ActiveRecord::Base
59
59
  completed_at.present?
60
60
  end
61
61
 
62
+ def succeeded?
63
+ successful?
64
+ end
65
+
66
+ def failed?
67
+ !successful?
68
+ end
69
+
62
70
  def environment
63
71
  environment_name
64
72
  end
@@ -96,6 +104,11 @@ private
96
104
  def notify_if_completed
97
105
  if just_completed?
98
106
  update_column :duration, completed_at - created_at if duration.nil?
107
+ if successful?
108
+ Houston.observer.fire "deploy:succeeded", self
109
+ else
110
+ Houston.observer.fire "deploy:failed", self
111
+ end
99
112
  Houston.observer.fire "deploy:completed", self
100
113
  end
101
114
  end
@@ -8,10 +8,12 @@ module Github
8
8
  belongs_to :project
9
9
  belongs_to :user
10
10
  has_and_belongs_to_many :commits
11
+ belongs_to :base, class_name: "Commit", foreign_key: "base_sha", primary_key: "sha"
12
+ belongs_to :head, class_name: "Commit", foreign_key: "head_sha", primary_key: "sha"
11
13
 
12
14
  before_validation :associate_project_with_self, if: :repo_changed?
13
15
  before_save :associate_user_with_self, if: :username_changed?
14
- after_commit :associate_commits_with_self, autosave: false
16
+ after_commit :associate_commits_with_self
15
17
 
16
18
  after_destroy { Houston.observer.fire "github:pull:closed", self }
17
19
  after_create { Houston.observer.fire "github:pull:opened", self }
@@ -139,6 +141,11 @@ module Github
139
141
  def labeled(*labels)
140
142
  where(["exists (select 1 from jsonb_array_elements(pull_requests.json_labels) as \"label\" where \"label\"->>'name' IN (?))", labels])
141
143
  end
144
+ alias :with_labels :labeled
145
+
146
+ def without_labels(*labels)
147
+ where(["not exists (select 1 from jsonb_array_elements(pull_requests.json_labels) as \"label\" where \"label\"->>'name' IN (?))", labels])
148
+ end
142
149
  end
143
150
 
144
151
 
@@ -147,6 +154,14 @@ module Github
147
154
  self.json_labels = value.map { |label| label.to_h.stringify_keys.pick("name", "color") }
148
155
  end
149
156
 
157
+ def labeled?(*values)
158
+ values.all? { |value| labels.any? { |label| label["name"] == value } }
159
+ end
160
+
161
+ def labeled_any?(*values)
162
+ values.any? { |value| labels.any? { |label| label["name"] == value } }
163
+ end
164
+
150
165
  def labels
151
166
  json_labels
152
167
  end
@@ -173,20 +188,32 @@ module Github
173
188
 
174
189
 
175
190
 
191
+ def to_s
192
+ "#{repo}##{number}"
193
+ end
194
+
195
+
196
+
197
+ def publish_commit_status!(status={})
198
+ project.repo.create_commit_status(head_sha, status)
199
+ end
200
+
201
+
202
+
176
203
  def merge_attributes(pr)
177
204
  self.repo = pr["base"]["repo"]["name"] unless repo
178
205
  self.number = pr["number"] unless number
179
206
  self.username = pr["user"]["login"] unless username
180
207
  self.avatar_url = pr["user"]["avatar_url"] unless avatar_url
181
208
  self.url = pr["html_url"] unless url
182
- self.base_sha = pr["base"]["sha"] unless base_sha
183
209
  self.base_ref = pr["base"]["ref"] unless base_ref
210
+ self.head_ref = pr["head"]["ref"] unless head_ref
211
+ self.created_at = pr["created_at"] unless created_at
184
212
 
185
- self.created_at = pr["created_at"]
186
213
  self.title = pr["title"]
187
214
  self.body = pr["body"]
215
+ self.base_sha = pr["base"]["sha"]
188
216
  self.head_sha = pr["head"]["sha"]
189
- self.head_ref = pr["head"]["ref"]
190
217
  self.labels = pr["labels"] if pr.key?("labels")
191
218
 
192
219
  self
@@ -203,9 +230,17 @@ module Github
203
230
  end
204
231
 
205
232
  def associate_commits_with_self
233
+ return unless commits_changes_before_commit?
234
+
206
235
  Houston.try({max_tries: 2, base: 0}, ActiveRecord::RecordNotUnique) do
207
236
  self.commits = project.commits.between(base_sha, head_sha)
208
237
  end
238
+
239
+ Houston.observer.fire "github:pull:synchronize", self
240
+ end
241
+
242
+ def commits_changes_before_commit?
243
+ previous_changes.key?(:base_sha) || previous_changes.key?(:head_sha)
209
244
  end
210
245
 
211
246
  end
@@ -5,6 +5,7 @@ class Release < ActiveRecord::Base
5
5
  after_create :release_each_task!
6
6
  after_create :release_each_antecedent!
7
7
  after_create { Houston.observer.fire "release:create", self }
8
+ after_save :update_search_vector, :if => :search_vector_should_change?
8
9
 
9
10
  belongs_to :project
10
11
  belongs_to :user
@@ -79,6 +80,30 @@ class Release < ActiveRecord::Base
79
80
  AND releases.created_at=most_recent_releases.created_at
80
81
  SQL
81
82
  end
83
+
84
+ def reindex!
85
+ update_all "search_vector = to_tsvector('english', release_changes)"
86
+ end
87
+
88
+ def search(query_string)
89
+ config = PgSearch::Configuration.new({against: "plain_text"}, self)
90
+ normalizer = PgSearch::Normalizer.new(config)
91
+ options = { dictionary: "english", tsvector_column: "search_vector" }
92
+ query = PgSearch::Features::TSearch.new(query_string, options, config.columns, self, normalizer)
93
+
94
+ excerpt = ts_headline(:release_changes, query,
95
+ start_sel: "<em>",
96
+ stop_sel: "</em>",
97
+
98
+ # Hack: show the entire value of `release_changes`
99
+ min_words: 65534,
100
+ max_words: 65535,
101
+ max_fragments: 0)
102
+
103
+ columns = (column_names - %w{release_changes search_vector}).map { |column| "releases.\"#{column}\"" }
104
+ columns.push excerpt.as("release_changes")
105
+ where(query.conditions).select(*columns)
106
+ end
82
107
  end
83
108
 
84
109
 
@@ -183,6 +208,15 @@ class Release < ActiveRecord::Base
183
208
 
184
209
 
185
210
 
211
+ def update_search_vector
212
+ self.class.where(id: id).reindex!
213
+ end
214
+
215
+ def search_vector_should_change?
216
+ (changed & %w{release_changes}).any?
217
+ end
218
+
219
+
186
220
  private
187
221
 
188
222
  def identify_commit(sha)
@@ -214,4 +248,12 @@ private
214
248
  end
215
249
  end
216
250
 
251
+ # http://www.postgresql.org/docs/9.1/static/textsearch-controls.html#TEXTSEARCH-HEADLINE
252
+ def self.ts_headline(column, query, options={})
253
+ column = arel_table[column] if column.is_a?(Symbol)
254
+ options = options.map { |(key, value)| "#{key.to_s.camelize}=#{value}" }.join(", ")
255
+ tsquery = Arel.sql(query.send(:tsquery))
256
+ Arel::Nodes::NamedFunction.new("ts_headline", [column, Arel::Nodes.build_quoted(tsquery), Arel::Nodes.build_quoted(options)])
257
+ end
258
+
217
259
  end