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