diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7ac922a9b2..ff096ce5b9 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -322,20 +322,35 @@ def link_to_chart(name, values)
link_to icon_tag('signal'), url, target: :blank
end
- # show which stages this reference is deploy(ed+ing) to
+ # Show which stages this reference has been or is currently being deployed to.
def deployed_or_running_list(stages, reference)
- html = "".html_safe
- stages.each do |stage|
- next unless deploy = stage.deployed_or_running_deploy
- next unless deploy.references?(reference)
- label = (deploy.active? ? "label-warning" : "label-success")
-
- text = "".html_safe
- text << stage.name
- html << content_tag(:span, text, class: "label #{label} release-stage")
- html << " "
+ deploys = Deploy.of_reference_in_stages(reference, stages)
+
+ pieces = stages.map do |stage|
+ deploy = deploys[stage]
+
+ label = if deploy.nil?
+ nil
+ elsif deploy.running?
+ "label-warning"
+ elsif deploy.succeeded?
+ if deploy == stage.last_deploy
+ "label-success"
+ else
+ # Deploy is neither active nor is it the last successful one, but it
+ # succeeded in the past.
+ "label-default"
+ end
+ end
+
+ if label
+ text = "".html_safe
+ text << stage.name
+ content_tag(:span, text, class: "label #{label} release-stage")
+ end
end
- html
+
+ safe_join(pieces, " ")
end
def check_box_section(section_title, help_text, object, method, collection)
diff --git a/app/models/deploy.rb b/app/models/deploy.rb
index 1faecc22d3..b54abe0e7a 100644
--- a/app/models/deploy.rb
+++ b/app/models/deploy.rb
@@ -171,6 +171,26 @@ def self.after(deploy)
where("#{table_name}.id > ?", deploy.id)
end
+ # Returns a Hash of Stage => Deploy for the given reference and stages.
+ def self.of_reference_in_stages(reference, stages)
+ # Group by stage, then select the latest deploy id.
+ deploys_and_stages = where(reference: reference).
+ where(stage_id: stages.map(&:id)).
+ group(:stage_id).
+ select("MAX(id) AS id, stage_id")
+
+ # Fetch the actual deploys.
+ deploys = Deploy.where(id: deploys_and_stages.map(&:id))
+
+ # Map to a hash of stage => deploy entries.
+ deploys.map do |deploy|
+ # Don't trigger another query in order to fetch the stage.
+ stage = stages.find { |stage| stage.id == deploy.stage_id }
+
+ [stage, deploy]
+ end.to_h
+ end
+
def self.expired
threshold = BuddyCheck.time_limit.ago
stale = where(buddy_id: nil).joins(:job).where(jobs: {status: 'pending'}).where("jobs.created_at < ?", threshold)
diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb
index 655ac68a1d..9ba216346b 100644
--- a/test/helpers/application_helper_test.rb
+++ b/test/helpers/application_helper_test.rb
@@ -698,6 +698,15 @@ def render
describe "#deployed_or_running_list" do
let(:stage_list) { [stages(:test_staging)] }
+ let!(:deploy) { create_deploy "v1" }
+
+ def create_deploy(reference)
+ stages(:test_staging).deploys.create!(
+ project: projects(:test),
+ reference: reference,
+ job: jobs(:succeeded_test)
+ )
+ end
it "produces safe output" do
html = deployed_or_running_list([], "foo")
@@ -705,27 +714,54 @@ def render
assert html.html_safe?
end
- it "renders succeeded deploys" do
- html = deployed_or_running_list(stage_list, "staging")
- html.must_equal "Staging "
+ it "renders current, succeeded deploys" do
+ html = deployed_or_running_list(stage_list, "v1")
+ html.must_equal "Staging"
+ end
+
+ it "renders past, succeeded deploys" do
+ # This deploy will be newer.
+ create_deploy "v2"
+
+ html = deployed_or_running_list(stage_list, "v1")
+ html.must_equal "Staging"
end
it "ignores failed deploys" do
- deploys(:succeeded_test).job.update_column(:status, 'failed')
- html = deployed_or_running_list(stage_list, "staging")
+ deploy.job.update_column(:status, "failed")
+ html = deployed_or_running_list(stage_list, "v1")
html.must_equal ""
end
it "ignores non-matching deploys" do
- deploys(:succeeded_test).update_column(:reference, 'nope')
- html = deployed_or_running_list(stage_list, "staging")
+ html = deployed_or_running_list(stage_list, "yolo")
html.must_equal ""
end
it "shows active deploys" do
- deploys(:succeeded_test).job.update_column(:status, 'running')
- html = deployed_or_running_list(stage_list, "staging")
- html.must_equal "Staging "
+ deploy.job.update_column(:status, 'running')
+ html = deployed_or_running_list(stage_list, "v1")
+ html.must_equal "Staging"
+ end
+
+ it "uses 4 queries when the deploy is the most recent one on the stage" do
+ assert_sql_queries 4 do
+ deployed_or_running_list(stage_list, "v1")
+ end
+ end
+
+ it "uses 3 queries if the deploy is currently running" do
+ deploy.job.update_column(:status, "running")
+
+ assert_sql_queries 3 do
+ deployed_or_running_list(stage_list, "v1")
+ end
+ end
+
+ it "uses 2 queries if there have been no deploys of that reference" do
+ assert_sql_queries 2 do
+ deployed_or_running_list(stage_list, "yolo")
+ end
end
end
diff --git a/test/models/deploy_test.rb b/test/models/deploy_test.rb
index 37bb6ef859..199194cbf8 100644
--- a/test/models/deploy_test.rb
+++ b/test/models/deploy_test.rb
@@ -656,6 +656,39 @@ def deploy
end
end
+ describe "#of_reference_in_stages" do
+ it "returns the latest deploy with the given reference in the specified stages" do
+ stage1 = create_stage!(name: "stage1")
+ stage2 = create_stage!(name: "stage2")
+ stage3 = create_stage!(name: "stage3")
+
+ create_deploy!(stage: stage1, reference: "v42")
+ deploy1 = create_deploy!(stage: stage1, reference: "v42")
+ deploy2 = create_deploy!(stage: stage2, reference: "v42")
+ create_deploy!(stage: stage3, reference: "v42")
+
+ expected = {
+ stage1 => deploy1,
+ stage2 => deploy2
+ }
+
+ Deploy.of_reference_in_stages("v42", [stage1, stage2]).must_equal expected
+ end
+
+ it "uses only two queries" do
+ stage1 = create_stage!(name: "stage1")
+ stage2 = create_stage!(name: "stage2")
+
+ create_deploy!(stage: stage1, reference: "v42")
+ create_deploy!(stage: stage1, reference: "v42")
+ create_deploy!(stage: stage2, reference: "v42")
+
+ assert_sql_queries 2 do
+ Deploy.of_reference_in_stages("v42", [stage1, stage2])
+ end
+ end
+ end
+
describe "#as_json" do
it "includes simple methods status" do
deploy.as_json.fetch("status").must_equal "succeeded"
@@ -690,4 +723,13 @@ def create_job!(attrs = {})
Job.create!(default_attrs.merge(attrs))
end
+
+ def create_stage!(attrs = {})
+ default_attrs = {
+ project: project,
+ name: "some-stage"
+ }
+
+ Stage.create!(default_attrs.merge(attrs))
+ end
end