houston-core 0.6.0 → 0.6.1
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +69 -68
- data/app/adapters/houston/adapters/deployment/engineyard.rb +1 -1
- data/app/adapters/houston/adapters/version_control/git_adapter/github_repo.rb +19 -0
- data/app/adapters/houston/adapters/version_control/git_adapter/remote_repo.rb +27 -0
- data/app/assets/images/drag-grip.png +0 -0
- data/app/assets/javascripts/app/infinite_scroll.coffee +1 -1
- data/app/assets/javascripts/app/views/_show_sprint_view.coffee +9 -9
- data/app/assets/javascripts/application.js +1 -0
- data/app/assets/javascripts/core/app.coffee +5 -0
- data/app/assets/javascripts/{app → core}/stacked_area_graph.coffee +0 -0
- data/app/assets/javascripts/{app → core}/stacked_bar_graph.coffee +0 -0
- data/app/assets/javascripts/dashboard.js +1 -0
- data/app/assets/javascripts/vendor.js +0 -1
- data/app/assets/stylesheets/application/exceptions.scss +3 -1
- data/app/assets/stylesheets/application/navigation.scss +84 -21
- data/app/assets/stylesheets/application/releases.scss +32 -2
- data/app/assets/stylesheets/application/test.scss +34 -0
- data/app/assets/stylesheets/core/colors.scss.erb +33 -3
- data/app/assets/stylesheets/dashboard/dashboard.scss +11 -7
- data/app/assets/stylesheets/variables.scss +3 -0
- data/app/concerns/belongs_to_commit.rb +14 -0
- data/app/concerns/project_adapter.rb +24 -6
- data/app/controllers/api/v1/projects_controller.rb +18 -0
- data/app/controllers/api/v1/sprint_tasks_controller.rb +1 -1
- data/app/controllers/deploys_controller.rb +1 -0
- data/app/controllers/project_tests_controller.rb +49 -19
- data/app/controllers/releases_controller.rb +5 -0
- data/app/controllers/test_runs_controller.rb +16 -1
- data/app/helpers/test_run_helper.rb +24 -0
- data/app/models/deploy.rb +13 -0
- data/app/models/github/pull_request.rb +39 -4
- data/app/models/release.rb +42 -0
- data/app/models/task.rb +3 -2
- data/app/models/test_run.rb +4 -0
- data/app/presenters/project_presenter.rb +28 -0
- data/app/views/deploys/show.html.erb +4 -0
- data/app/views/github/pulls/index.html.erb +4 -1
- data/app/views/layouts/_mobile_navigation.html.erb +14 -17
- data/app/views/layouts/_navigation.html.erb +87 -87
- data/app/views/layouts/dashboard.html.erb +2 -2
- data/app/views/project_tests/index.html.erb +22 -7
- data/app/views/releases/_index.html.erb +65 -0
- data/app/views/releases/_results.html.erb +47 -0
- data/app/views/releases/index.html.erb +29 -65
- data/app/views/sprints/dashboard.html.erb +4 -2
- data/config/environments/production.rb +1 -1
- data/config/environments/test.rb +4 -1
- data/config/initializers/add_navigation_renderers.rb +6 -0
- data/config/initializers/requirements.rb +1 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20151226154901_add_search_vector_to_releases.rb +6 -0
- data/db/migrate/20151226155305_generate_index_on_releases.rb +5 -0
- data/db/migrate/20151228183704_drop_unused_tables.rb +35 -0
- data/db/migrate/20160120145757_add_successful_to_deploys.rb +10 -0
- data/db/structure.sql +19 -67
- data/houston.gemspec +3 -3
- data/lib/configuration.rb +4 -2
- data/lib/core_ext/array.rb +37 -0
- data/lib/houston/version.rb +1 -1
- data/test/integration/ci_integration_test.rb +14 -13
- data/test/unit/models/project_test.rb +33 -0
- data/test/unit/models/pull_request_test.rb +71 -1
- metadata +24 -14
- 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
|
-
#{
|
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} ||= #{
|
62
|
-
.build(self,
|
79
|
+
@#{attribute_name} ||= #{adapter_method}
|
80
|
+
.build(self, *#{params_method}.values)
|
63
81
|
.extend(FeatureSupport)
|
64
82
|
end
|
65
83
|
|
66
|
-
def
|
67
|
-
#{
|
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 #{
|
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
|
@@ -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,
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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])
|
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
|
data/app/models/deploy.rb
CHANGED
@@ -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
|
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
|
data/app/models/release.rb
CHANGED
@@ -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
|