# frozen_string_literal: true require 'spec_helper' RSpec.describe Deployment, feature_category: :continuous_delivery do subject { build(:deployment) } let_it_be(:project) { create(:project, :repository) } let_it_be_with_reload(:environment) { create(:environment, project: project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:pipeline_b) { create(:ci_pipeline, project: project) } let_it_be(:deployable) { create(:ci_build, project: project, pipeline: pipeline) } let_it_be(:deployment) { create(:deployment, project: project, environment: environment, deployable: deployable) } # environments let_it_be(:production) { create(:environment, :production, project: project) } let_it_be(:staging) { create(:environment, :staging, project: project) } let_it_be(:testing) { create(:environment, :testing, project: project) } describe 'associations' do it { is_expected.to belong_to(:project).required } it { is_expected.to belong_to(:environment).required } it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:deployable) } it { is_expected.to have_one(:deployment_cluster) } it { is_expected.to have_many(:deployment_merge_requests) } it { is_expected.to have_many(:merge_requests).through(:deployment_merge_requests) } end describe 'delegations' do it { is_expected.to delegate_method(:name).to(:environment).with_prefix } it { is_expected.to delegate_method(:commit).to(:project) } it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) } it { is_expected.to delegate_method(:kubernetes_namespace).to(:deployment_cluster).as(:kubernetes_namespace) } it { is_expected.to delegate_method(:cluster).to(:deployment_cluster) } end describe 'validations' do it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:sha) } end it_behaves_like 'having unique enum values' describe '#manual_actions' do let(:deployment) { build(:deployment) } it 'delegates to environment_manual_actions' do expect(deployment.deployable).to receive(:other_manual_actions).and_call_original deployment.manual_actions end end describe '#scheduled_actions' do let(:deployment) { build(:deployment) } it 'delegates to environment_scheduled_actions' do expect(deployment.deployable).to receive(:other_scheduled_actions).and_call_original deployment.scheduled_actions end end describe 'modules' do it_behaves_like 'AtomicInternalId' do # Use a fresh project specifically for these IID tests let_it_be(:iid_test_project) { create(:project, :repository) } let_it_be(:iid_test_environment) { create(:environment, project: iid_test_project) } let_it_be(:iid_test_deployable) { create(:ci_build, project: iid_test_project) } let(:internal_id_attribute) { :iid } let(:instance) do build(:deployment, project: iid_test_project, deployable: iid_test_deployable, environment: iid_test_environment) end let(:scope) { :project } let(:scope_attrs) { { project: iid_test_project } } let(:usage) { :deployments } end it { is_expected.to include_module(EachBatch) } end describe '.success' do subject { described_class.success } context 'when deployment status is success' do before do deployment.update!(status: :success, finished_at: Time.zone.now) end it { is_expected.to eq([deployment]) } end context 'when deployment status is created' do before do deployment.update!(status: :created) end it { is_expected.to be_empty } end context 'when deployment status is running' do before do deployment.update!(status: :running) end it { is_expected.to be_empty } end end describe 'state machine' do context 'when deployment runs' do let(:deployment) { create(:deployment) } it 'starts running' do freeze_time do deployment.run! expect(deployment).to be_running expect(deployment.finished_at).to be_nil end end it 'executes Deployments::HooksWorker asynchronously' do freeze_time do expect(Deployments::HooksWorker) .to receive(:perform_async) .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running', 'status_changed_at' => Time.current.to_s })) deployment.run! end end end context 'when deployment succeeded' do before do deployment.update!(status: :running) end it 'has correct status' do freeze_time do deployment.succeed! expect(deployment).to be_success expect(deployment.finished_at).to be_like_time(Time.current) end end it 'executes Deployments::UpdateEnvironmentWorker asynchronously' do expect(Deployments::UpdateEnvironmentWorker) .to receive(:perform_async).with(deployment.id) deployment.succeed! end it 'executes Deployments::HooksWorker asynchronously' do freeze_time do expect(Deployments::HooksWorker) .to receive(:perform_async) .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'success', 'status_changed_at' => Time.current.to_s })) deployment.succeed! end end end context 'when deployment failed' do before do deployment.update!(status: :running) end it 'has correct status' do freeze_time do deployment.drop! expect(deployment).to be_failed expect(deployment.finished_at).to be_like_time(Time.current) end end it 'does not execute Deployments::LinkMergeRequestWorker' do expect(Deployments::LinkMergeRequestWorker) .not_to receive(:perform_async).with(deployment.id) deployment.drop! end it 'executes Deployments::HooksWorker asynchronously' do freeze_time do expect(Deployments::HooksWorker) .to receive(:perform_async) .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'failed', 'status_changed_at' => Time.current.to_s })) deployment.drop! end end end context 'when deployment was canceled' do before do deployment.update!(status: :running) end it 'has correct status' do freeze_time do deployment.cancel! expect(deployment).to be_canceled expect(deployment.finished_at).to be_like_time(Time.current) end end it 'does not execute Deployments::LinkMergeRequestWorker' do expect(Deployments::LinkMergeRequestWorker) .not_to receive(:perform_async).with(deployment.id) deployment.cancel! end it 'executes Deployments::HooksWorker asynchronously' do freeze_time do expect(Deployments::HooksWorker) .to receive(:perform_async) .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'canceled', 'status_changed_at' => Time.current.to_s })) deployment.cancel! end end end context 'when deployment was skipped' do before do deployment.update!(status: :running, finished_at: nil) end it 'has correct status' do deployment.skip! expect(deployment).to be_skipped expect(deployment.finished_at).to be_nil end it 'does not execute Deployments::LinkMergeRequestWorker asynchronously' do expect(Deployments::LinkMergeRequestWorker) .not_to receive(:perform_async).with(deployment.id) deployment.skip! end it 'does not execute Deployments::HooksWorker' do freeze_time do expect(Deployments::HooksWorker) .not_to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current) deployment.skip! end end end context 'when deployment is blocked' do before do deployment.update!(status: :created, finished_at: nil) end it 'has correct status' do deployment.block! expect(deployment).to be_blocked expect(deployment.finished_at).to be_nil end it 'does not execute Deployments::LinkMergeRequestWorker asynchronously' do expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async) deployment.block! end it 'does not execute Deployments::HooksWorker' do expect(Deployments::HooksWorker).not_to receive(:perform_async) deployment.block! end end describe 'synching status to Jira' do let(:deployment) { create(:deployment, project: project) } let(:worker) { ::JiraConnect::SyncDeploymentsWorker } context 'when Jira Connect subscription does not exist' do it 'does not call the worker' do expect(worker).not_to receive(:perform_async) deployment end end context 'when Jira Connect subscription exists' do before_all do create(:jira_connect_subscription, namespace: project.namespace) end it 'calls the worker on creation' do expect(worker).to receive(:perform_async).with(Integer) deployment end it 'does not call the worker for skipped deployments' do expect(deployment).to be_present # warm-up, ignore the creation trigger expect(worker).not_to receive(:perform_async) deployment.skip! end %i[run! succeed! drop! cancel!].each do |event| context "when we call pipeline.#{event}" do it 'triggers a Jira synch worker' do expect(worker).to receive(:perform_async).with(deployment.id) deployment.send(event) end end end end end end describe '#older_than_last_successful_deployment?' do subject { deployment.older_than_last_successful_deployment? } context 'when deployment is current deployment' do before do deployment.update!(status: :success, finished_at: Time.zone.now) end it { is_expected.to be_falsey } end context 'when deployment is behind current deployment' do let_it_be(:commits) { project.repository.commits('master', limit: 2) } let!(:deployment) do create( :deployment, :success, project: project, environment: environment, finished_at: 1.year.ago, sha: commits[0].sha ) end let!(:last_deployment) do create(:deployment, :success, project: project, environment: environment, sha: commits[1].sha) end it { is_expected.to be_truthy } end context 'when deployment is the same sha as the current deployment' do let!(:deployment) do create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago) end let!(:last_deployment) do create(:deployment, :success, project: project, environment: environment, sha: deployment.sha) end it { is_expected.to be_falsey } end context 'when environment is undefined' do let(:deployment) { build(:deployment, :success, project: project, environment: environment) } before do deployment.environment = nil end it { is_expected.to be_falsey } end end describe '#success?' do subject { deployment.success? } context 'when deployment status is success' do before do deployment.update!(status: :success, finished_at: Time.zone.now) end it { is_expected.to be_truthy } end context 'when deployment status is failed' do before do deployment.update!(status: :failed, finished_at: Time.zone.now) end it { is_expected.to be_falsy } end end describe '#status_name' do subject { deployment.status_name } context 'when deployment status is success' do before do deployment.update!(status: :success, finished_at: Time.zone.now) end it { is_expected.to eq(:success) } end context 'when deployment status is failed' do before do deployment.update!(status: :failed, finished_at: Time.zone.now) end it { is_expected.to eq(:failed) } end end describe '#deployed_at' do subject { deployment.deployed_at } context 'when deployment status is created' do it { is_expected.to be_nil } end context 'when deployment status is success' do before do deployment.update!(status: :success, finished_at: Time.zone.now) end it { is_expected.to eq(deployment.read_attribute(:finished_at)) } end context 'when deployment status is running' do before do deployment.update!(status: :running) end it { is_expected.to be_nil } end end describe 'scopes' do let_it_be_with_reload(:deployment_2) { create(:deployment, project: project) } let_it_be_with_reload(:deployment_3) { create(:deployment, project: project) } describe '.stoppable' do subject { described_class.stoppable } context 'when deployment is stoppable' do before do deployment.update!(status: :success, finished_at: Time.zone.now, on_stop: 'stop-review') end it { is_expected.to eq([deployment]) } end context 'when deployment is not stoppable' do before do deployment.update!(status: :failed, finished_at: Time.zone.now) end it { is_expected.to be_empty } end end describe '.find_successful_deployment!' do before do deployment.update!(status: :success, finished_at: Time.zone.now) end it 'returns a successful deployment' do expect(described_class.find_successful_deployment!(deployment.iid)).to eq(deployment) end it 'raises when no deployment is found' do expect { described_class.find_successful_deployment!(-1) } .to raise_error(ActiveRecord::RecordNotFound) end end describe '.jobs' do subject { described_class.jobs } it 'retrieves jobs for the deployments' do is_expected.to match_array([deployment.deployable, deployment_2.deployable, deployment_3.deployable]) end it 'does not fetch the null deployable_ids' do deployment_3.update!(deployable_id: nil, deployable_type: nil) is_expected.to match_array([deployment.deployable, deployment_2.deployable]) end end describe '.archivables_in' do subject(:archivables_in) { described_class.archivables_in(project, limit: limit) } let(:limit) { 100 } context 'when there are no archivable deployments in the project' do it { is_expected.to be_empty } end context 'when there are archivable deployments in the project' do before do stub_const("::Deployment::ARCHIVABLE_OFFSET", 1) end it 'returns all archivable deployments' do expect(archivables_in.count).to eq(2) expect(archivables_in).to contain_exactly(deployment, deployment_2) end context 'with limit' do let(:limit) { 1 } it 'takes the limit into account' do expect(archivables_in.count).to eq(1) expect(archivables_in.take).to be_in([deployment, deployment_2]) end end end end describe '.for_iid' do subject { described_class.for_iid(project, iid) } let(:iid) { deployment.iid } it 'finds the deployment' do is_expected.to contain_exactly(deployment) end context 'when iid does not match' do let(:iid) { non_existing_record_id } it 'does not find the deployment' do is_expected.to be_empty end end end describe '.for_environment_name' do subject { described_class.for_environment_name(project, environment_name) } let_it_be(:other_project) { create(:project, :repository) } let_it_be(:other_production) { create(:environment, :production, project: other_project) } let(:environment_name) { production.name } context 'when deployment belongs to the environment' do before do deployment.update!(environment: production) end it { is_expected.to eq([deployment]) } end context 'when deployment belongs to the same project but different environment name' do before do deployment.update!(environment: staging) end it { is_expected.to be_empty } end context 'when deployment belongs to the same environment name but different project' do before do deployment.update!(project: other_project, environment: other_production) end it { is_expected.to be_empty } end end describe '.last_for_environment' do before do deployment.update!(environment: production) deployment_2.update!(environment: staging) deployment_3.update!(environment: production) end let(:deployments) { [deployment, deployment_2, deployment_3] } it 'retrieves last deployments for environments' do last_deployments = described_class.last_for_environment([staging, production, testing]) expect(last_deployments.size).to eq(2) expect(last_deployments).to match_array(deployments.last(2)) end end describe '.active' do subject(:active) { described_class.active } before do deployment.update!(status: :created) deployment_2.update!(status: :running) deployment_3.update!(status: :failed) end it 'retrieves the active deployments' do create(:deployment, status: :canceled) create(:deployment, status: :skipped) create(:deployment, status: :blocked) is_expected.to contain_exactly(deployment, deployment_2) end end describe '.older_than' do subject(:older_than) { described_class.older_than(deployment_3) } it 'retrives the correct older deployments' do is_expected.to contain_exactly(deployment, deployment_2) end end describe '.finished_before' do before do deployment.update!(finished_at: 1.day.ago) deployment_2.update!(finished_at: Time.current) end it 'filters deployments by finished_at' do expect(described_class.finished_before(1.hour.ago)).to eq([deployment]) end end describe '.finished_after' do before do deployment.update!(finished_at: 1.day.ago) deployment_2.update!(finished_at: Time.current) end it 'filters deployments by finished_at' do expect(described_class.finished_after(1.hour.ago)).to eq([deployment_2]) end end describe '.ordered' do before do deployment.update!(status: :running) deployment_2.update!(status: :success, finished_at: Time.current) deployment_3.update!(status: :canceled, finished_at: 1.day.ago) end let!(:deployment_4) { create(:deployment, status: :success, finished_at: 2.days.ago) } it 'sorts by finished at' do expect(described_class.ordered).to eq([deployment, deployment_2, deployment_3, deployment_4]) end end describe '.ordered_as_upcoming' do before do deployment.update!(status: :running) deployment_2.update!(status: :blocked) deployment_3.update!(status: :created) end it 'sorts by ID DESC' do expect(described_class.ordered_as_upcoming).to match_array([deployment_3, deployment_2, deployment]) end end describe '.visible' do subject { described_class.visible } it 'retrieves the visible deployments' do deployment1 = create(:deployment, status: :running) deployment2 = create(:deployment, status: :success) deployment3 = create(:deployment, status: :failed) deployment4 = create(:deployment, status: :canceled) deployment5 = create(:deployment, status: :blocked) create(:deployment, status: :skipped) is_expected.to contain_exactly(deployment1, deployment2, deployment3, deployment4, deployment5) end it 'has a corresponding database index' do index = ApplicationRecord.connection.indexes('deployments').find do |i| i.name == 'index_deployments_for_visible_scope' end scope_values = described_class::VISIBLE_STATUSES.map { |s| described_class.statuses[s] }.to_s expect(index.where).to include(scope_values) end end describe '.finished' do subject { described_class.finished } before do # unfinished deployments deployment.update!(status: :running) deployment_2.update!(status: :blocked) deployment_3.update!(status: :skipped) end # finished deployments let!(:successful_deployment) { create(:deployment, status: :success) } let!(:failed_deployment) { create(:deployment, status: :failed) } let!(:canceled_deployment) { create(:deployment, status: :canceled) } it 'retrieves the finished deployments' do is_expected.to contain_exactly(successful_deployment, failed_deployment, canceled_deployment) end end describe '.upcoming' do subject { described_class.upcoming } it 'retrieves the upcoming deployments' do deployment1 = create(:deployment, status: :running) deployment2 = create(:deployment, status: :blocked) create(:deployment, status: :success) create(:deployment, status: :failed) create(:deployment, status: :canceled) create(:deployment, status: :skipped) is_expected.to contain_exactly(deployment1, deployment2) end end describe '.last_finished_deployment_group_for_environment' do subject { described_class.last_finished_deployment_group_for_environment(environment) } context 'when there are no deployments and jobs' do it { is_expected.to eq(described_class.none) } end shared_examples_for 'find last finished deployment for environment' do context 'when there are no finished jobs' do before do job = create(processable_type, :created, project: project, pipeline: pipeline) create(:deployment, :created, environment: environment, project: project, deployable: job) end it { is_expected.to eq(described_class.none) } end context 'when there are deployments for multiple pipelines' do # finished deployments for pipeline let!(:deployment_a_success) do job = create(processable_type, :success, project: project, pipeline: pipeline) create(:deployment, :success, project: project, environment: environment, deployable: job) end let!(:deployment_a_failed) do job = create(processable_type, :failed, project: project, pipeline: pipeline) create(:deployment, :failed, project: project, environment: environment, deployable: job) end let!(:deployment_a_canceled) do job = create(processable_type, :canceled, project: project, pipeline: pipeline) create(:deployment, :canceled, project: project, environment: environment, deployable: job) end before do # running deployment for pipeline job_a_running = create(processable_type, :running, project: project, pipeline: pipeline) create(:deployment, :running, project: project, environment: environment, deployable: job_a_running) # running deployment for pipeline_b job_b_running = create(processable_type, :running, project: project, pipeline: pipeline_b) create(:deployment, :running, project: project, environment: environment, deployable: job_b_running) end it 'returns the finished deployments for the last finished pipeline' do expect(subject.pluck(:id)).to contain_exactly( deployment_a_success.id, deployment_a_failed.id, deployment_a_canceled.id) end end context 'when last finished deployment is a retried job' do before do job = create(processable_type, :success, project: project, pipeline: pipeline, environment: environment.name) create(:deployment, :success, project: project, environment: environment, deployable: job) # retry job job.update!(retried: true) # new successful job after retry. create( processable_type, status: :success, finished_at: Time.current, project: project, pipeline: pipeline, environment: environment.name ) end it { is_expected.not_to be_nil } end context 'when there are many environments' do def subject_method(env) described_class.last_finished_deployment_group_for_environment(env) end let_it_be(:environment_2) { create(:environment, project: project) } let_it_be(:pipeline_c) { create(:ci_pipeline, project: project) } let_it_be(:pipeline_d) { create(:ci_pipeline, project: project) } # stop jobs in pipeline let_it_be(:stop_job_a_success) do create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'stop_a_success') end let_it_be(:stop_job_a_failed) do create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'stop_a_failed') end let_it_be(:stop_job_a_canceled) do create(:ci_build, :manual, project: project, pipeline: pipeline, name: 'stop_a_canceled') end # stop jobs in pipeline_c let_it_be(:stop_job_c_success) do create(:ci_build, :manual, project: project, pipeline: pipeline_c, name: 'stop_c_success') end let_it_be(:stop_job_c_failed) do create(:ci_build, :manual, project: project, pipeline: pipeline_c, name: 'stop_c_failed') end let_it_be(:stop_job_c_canceled) do create(:ci_build, :manual, project: project, pipeline: pipeline_c, name: 'stop_c_canceled') end # finished deployments for 'environment' from pipeline let_it_be(:deployment_a_success) do job = create(processable_type, :success, project: project, pipeline: pipeline) create(:deployment, :success, project: project, environment: environment, deployable: job, on_stop: 'stop_a_success') end let_it_be(:deployment_a_failed) do job = create(processable_type, :failed, project: project, pipeline: pipeline) create(:deployment, :failed, project: project, environment: environment, deployable: job, on_stop: 'stop_a_failed') end let_it_be(:deployment_a_canceled) do job = create(processable_type, :canceled, project: project, pipeline: pipeline) create(:deployment, :canceled, project: project, environment: environment, deployable: job, on_stop: 'stop_a_canceled') end # finished deployments for 'environment_2' from pipeline_c let_it_be(:deployment_c_success) do job = create(processable_type, :success, project: project, pipeline: pipeline_c) create(:deployment, :success, project: project, environment: environment_2, deployable: job, on_stop: 'stop_c_success') end let_it_be(:deployment_c_failed) do job = create(processable_type, :failed, project: project, pipeline: pipeline_c) create(:deployment, :failed, project: project, environment: environment_2, deployable: job, on_stop: 'stop_c_failed') end let_it_be(:deployment_c_canceled) do job = create(processable_type, :canceled, project: project, pipeline: pipeline_c) create(:deployment, :canceled, project: project, environment: environment_2, deployable: job, on_stop: 'stop_c_canceled') end before_all do # running deployments job_a_running = create(processable_type, :running, project: project, pipeline: pipeline) create(:deployment, :running, project: project, environment: environment, deployable: job_a_running) job_b_running = create(processable_type, :running, project: project, pipeline: pipeline_b) create(:deployment, :running, project: project, environment: environment, deployable: job_b_running) job_c_running = create(processable_type, :running, project: project, pipeline: pipeline_c) create(:deployment, :running, project: project, environment: environment_2, deployable: job_c_running) job_d_running = create(processable_type, :running, project: project, pipeline: pipeline_d) create(:deployment, :running, project: project, environment: environment_2, deployable: job_d_running) end it 'batch loads for environments' do # Loads Batch loader subject_method(environment) subject_method(environment_2) expect(subject_method(environment.reload).pluck(:id)) .to contain_exactly(deployment_a_success.id, deployment_a_failed.id, deployment_a_canceled.id) expect { subject_method(environment_2).pluck(:id) }.not_to exceed_query_limit(0) expect(subject_method(environment_2).pluck(:id)) .to contain_exactly(deployment_c_success.id, deployment_c_failed.id, deployment_c_canceled.id) expect(subject_method(environment).filter_map(&:stop_action)) .to contain_exactly(stop_job_a_success, stop_job_a_failed, stop_job_a_canceled) expect { subject_method(environment_2).map(&:stop_action) } .not_to exceed_query_limit(0) expect(subject_method(environment_2).filter_map(&:stop_action)) .to contain_exactly(stop_job_c_success, stop_job_c_failed, stop_job_c_canceled) end end end it_behaves_like 'find last finished deployment for environment' do let_it_be(:processable_type) { :ci_build } end it_behaves_like 'find last finished deployment for environment' do let_it_be(:processable_type) { :ci_bridge } end end describe '.latest_for_sha' do subject { described_class.latest_for_sha(sha) } let_it_be(:commits) { project.repository.commits('master', limit: 2) } let_it_be(:deployments) { commits.reverse.map { |commit| create(:deployment, project: project, sha: commit.id) } } let(:sha) { commits.map(&:id) } it 'finds the latest deployment with sha' do is_expected.to eq(deployments.last) end context 'when sha is old' do let(:sha) { commits.last.id } it 'finds the latest deployment with sha' do is_expected.to eq(deployments.first) end end context 'when sha is nil' do let(:sha) { nil } it { is_expected.to be_nil } end end end describe '#includes_commit?' do before do deployment.update!(environment: environment, sha: project.commit.id) end context 'when there is no project commit' do it 'returns false' do commit = project.commit('feature') expect(deployment.includes_commit?(commit.id)).to be false end end context 'when they share the same tree branch' do it 'returns true' do commit = project.commit expect(deployment.includes_commit?(commit.id)).to be true end end context 'when the SHA for the deployment does not exist in the repo' do it 'returns false' do deployment.update!(sha: Gitlab::Git::SHA1_BLANK_SHA) commit = project.commit expect(deployment.includes_commit?(commit.id)).to be false end end end describe '#stop_action' do subject { deployment.stop_action } shared_examples_for 'stop action for a job' do let(:job) { create(factory_type) } # rubocop:disable Rails/SaveBang -- It is for FactoryBot.save context 'when no other actions' do let(:deployment) { FactoryBot.build(:deployment, deployable: job) } it { is_expected.to be_nil } end context 'with other actions' do let!(:close_action) { create(factory_type, :manual, pipeline: job.pipeline, name: 'close_app') } context 'when matching action is defined' do let(:deployment) { FactoryBot.build(:deployment, deployable: job, on_stop: 'close_other_app') } it { is_expected.to be_nil } end context 'when no matching action is defined' do let(:deployment) { FactoryBot.build(:deployment, deployable: job, on_stop: 'close_app') } it { is_expected.to eq(close_action) } end end end it_behaves_like 'stop action for a job' do let(:factory_type) { :ci_build } end it_behaves_like 'stop action for a job' do let(:factory_type) { :ci_bridge } end end describe '#deployed_by' do it 'returns the deployment user if there is no deployable' do deployment_user = create(:user) deployment = create(:deployment, deployable: nil, user: deployment_user) expect(deployment.deployed_by).to eq(deployment_user) end it 'returns the deployment user if the deployable is build and have no user' do deployment_user = create(:user) job = create(:ci_build, user: nil) deployment = create(:deployment, deployable: job, user: deployment_user) expect(deployment.deployed_by).to eq(deployment_user) end it 'returns the deployment user if the deployable is bridge and have no user' do deployment_user = create(:user) job = create(:ci_bridge, user: nil) deployment = create(:deployment, deployable: job, user: deployment_user) expect(deployment.deployed_by).to eq(deployment_user) end it 'returns the deployable user if there is one' do build_user = create(:user) deployment_user = create(:user) job = create(:ci_build, user: build_user) deployment = create(:deployment, deployable: job, user: deployment_user) expect(deployment.deployed_by).to eq(build_user) end end describe '#triggered_by?' do subject { deployment.triggered_by?(user) } let(:user) { create(:user) } let(:deployment) { create(:deployment, user: user) } it { is_expected.to eq(true) } context 'when deployment triggerer is different' do let(:deployment) { create(:deployment) } it { is_expected.to eq(false) } end end describe '#job' do subject { deployment.job } it { is_expected.to eq(deployment.deployable) } it 'returns nil when the associated job is not found' do deployment.update!(deployable_id: nil, deployable_type: nil) is_expected.to be_nil end end describe '#previous_deployment' do using RSpec::Parameterized::TableSyntax let_it_be(:production_deployment_1) { create(:deployment, :success, project: project, environment: production) } let_it_be(:production_deployment_2) { create(:deployment, :success, project: project, environment: production) } let_it_be(:production_deployment_3) { create(:deployment, :failed, project: project, environment: production) } let_it_be(:production_deployment_4) { create(:deployment, :canceled, project: project, environment: production) } let_it_be(:staging_deployment_1) { create(:deployment, :failed, project: project, environment: staging) } let_it_be(:staging_deployment_2) { create(:deployment, :success, project: project, environment: staging) } let_it_be(:production_deployment_5) { create(:deployment, :success, project: project, environment: production) } let_it_be(:staging_deployment_3) { create(:deployment, :success, project: project, environment: staging) } where(:pointer, :expected_previous_deployment) do 'production_deployment_1' | nil 'production_deployment_2' | 'production_deployment_1' 'production_deployment_3' | 'production_deployment_2' 'production_deployment_4' | 'production_deployment_2' 'staging_deployment_1' | nil 'staging_deployment_2' | nil 'production_deployment_5' | 'production_deployment_2' 'staging_deployment_3' | 'staging_deployment_2' end with_them do it 'returns the previous deployment' do if expected_previous_deployment.nil? expect(send(pointer).previous_deployment).to eq(expected_previous_deployment) else expect(send(pointer).previous_deployment).to eq(send(expected_previous_deployment)) end end end end describe '#link_merge_requests' do it 'links merge requests with a deployment' do deploy = create(:deployment) mr1 = create( :merge_request, :merged, target_project: deploy.project, source_project: deploy.project ) mr2 = create( :merge_request, :merged, target_project: deploy.project, source_project: deploy.project ) deploy.link_merge_requests(deploy.project.merge_requests) expect(deploy.merge_requests).to include(mr1, mr2) end it 'ignores already linked merge requests' do deploy = create(:deployment) mr1 = create( :merge_request, :merged, target_project: deploy.project, source_project: deploy.project ) deploy.link_merge_requests(deploy.project.merge_requests) mr2 = create( :merge_request, :merged, target_project: deploy.project, source_project: deploy.project ) deploy.link_merge_requests(deploy.project.merge_requests) expect(deploy.merge_requests).to include(mr1, mr2) end end describe '#create_ref' do let(:deployment) { build(:deployment) } subject(:create_ref) { deployment.create_ref } it 'creates a ref using the sha' do expect(deployment.project.repository).to receive(:create_ref).with( deployment.sha, "refs/environments/#{deployment.environment.name}/deployments/#{deployment.iid}" ) create_ref end end describe '#playable_job' do subject(:playable_job) { deployment.playable_job } context 'when there is a deployable job' do let(:deployment) { create(:deployment, deployable: job) } context 'when the deployable job is build and playable' do let(:job) { create(:ci_build, :playable) } it { is_expected.to eq(job) } end context 'when the deployable job is bridge and playable' do let(:job) { create(:ci_bridge, :playable) } it { is_expected.to eq(job) } end context 'when the deployable job is not playable' do let(:job) { create(:ci_build) } it { is_expected.to be_nil } end end context 'when there is no deployable job' do it { is_expected.to be_nil } end end describe '#update_status' do let(:deploy) { create(:deployment, status: :running) } it 'changes the status' do deploy.update_status('success') expect(deploy).to be_success end it 'schedules workers when finishing a deploy' do expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async) expect(Deployments::ArchiveInProjectWorker).to receive(:perform_async) expect(Deployments::HooksWorker).to receive(:perform_async) expect(deploy.update_status('success')).to eq(true) end it 'updates finished_at when transitioning to a finished status' do freeze_time do deploy.update_status('success') expect(deploy.read_attribute(:finished_at)).to eq(Time.current) end end context 'when an invalid status transition is detected' do it 'tracks an exception' do expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with(instance_of(described_class::StatusUpdateError), deployment_id: deploy.id) expect(deploy.update_status('running')).to eq(false) end it 'tracks an exception' do deploy.update_status('success') expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with(instance_of(described_class::StatusUpdateError), deployment_id: deploy.id) expect(deploy.update_status('created')).to eq(false) end end it 'tracks an exception if an invalid argument' do expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with(instance_of(described_class::StatusUpdateError), deployment_id: deploy.id) expect(deploy.update_status('recreate')).to eq(false) end context 'when mapping status to event' do using RSpec::Parameterized::TableSyntax where(:status, :method) do 'running' | :run! 'success' | :succeed! 'failed' | :drop! 'canceling' | nil 'canceled' | :cancel! 'skipped' | :skip! 'blocked' | :block! end with_them do it 'calls the correct method for the given status' do expect(deploy).to receive(method) if method deploy.update_status(status) end end context 'for created status update' do let(:deploy) { create(:deployment, status: :created) } it 'calls the correct method' do expect(deploy).to receive(:create!) deploy.update_status('created') end end end context 'when each job status is passed' do Deployment.statuses.each do |starting_status, _| Ci::HasStatus::AVAILABLE_STATUSES.each do |status| it "#{starting_status} to #{status} does not cause an error" do deploy.update!(status: starting_status) expect { deploy.update_status(status) }.not_to raise_error end end end end end describe '#sync_status_with' do subject { deployment.sync_status_with(job) } shared_examples_for 'sync status with a job' do let(:deployment) { create(:deployment, project: project, status: deployment_status) } let(:job) { create(factory_type, project: project, status: job_status) } shared_examples_for 'synchronizing deployment' do let(:expected_deployment_status) { job_status.to_s } it 'changes deployment status' do expect(Gitlab::ErrorTracking).not_to receive(:track_exception) is_expected.to eq(true) expect(deployment.status).to eq(expected_deployment_status) expect(deployment.errors).to be_empty end end shared_examples_for 'gracefully handling error' do it 'tracks an exception' do expect(Gitlab::ErrorTracking).to( receive(:track_exception).with( instance_of(described_class::StatusSyncError), deployment_id: deployment.id, job_id: job.id ) do |error| expect(error.backtrace).to be_present end ) is_expected.to eq(false) expect(deployment.status).to eq(deployment_status.to_s) expect(deployment.errors.full_messages).to include(error_message) end end shared_examples_for 'ignoring job' do it 'does not change deployment status' do expect(Gitlab::ErrorTracking).not_to receive(:track_exception) is_expected.to eq(false) expect(deployment.status).to eq(deployment_status.to_s) expect(deployment.errors).to be_empty end end context 'with created deployment' do let(:deployment_status) { :created } context 'with created job' do let(:job_status) { :created } it_behaves_like 'ignoring job' end context 'with manual job' do let(:job_status) { :manual } it_behaves_like 'synchronizing deployment' do let(:expected_deployment_status) { 'blocked' } end end context 'with running job' do let(:job_status) { :running } it_behaves_like 'synchronizing deployment' end context 'with finished job' do let(:job_status) { :success } it_behaves_like 'synchronizing deployment' end context 'with unrelated job' do let(:job_status) { :waiting_for_resource } it_behaves_like 'ignoring job' end end context 'with running deployment' do let(:deployment_status) { :running } context 'with created job' do let(:job_status) { :created } it_behaves_like 'gracefully handling error' do let(:error_message) { %(Status cannot transition via \"create\") } end end context 'with manual job' do let(:job_status) { :manual } it_behaves_like 'gracefully handling error' do let(:error_message) { %(Status cannot transition via \"block\") } end end context 'with running job' do let(:job_status) { :running } it_behaves_like 'ignoring job' end context 'with finished job' do let(:job_status) { :success } it_behaves_like 'synchronizing deployment' end context 'with unrelated job' do let(:job_status) { :waiting_for_resource } it_behaves_like 'ignoring job' end end context 'with finished deployment' do let(:deployment_status) { :success } context 'with created job' do let(:job_status) { :created } it_behaves_like 'gracefully handling error' do let(:error_message) { %(Status cannot transition via \"create\") } end end context 'with manual job' do let(:job_status) { :manual } it_behaves_like 'gracefully handling error' do let(:error_message) { %(Status cannot transition via \"block\") } end end context 'with running job' do let(:job_status) { :running } it_behaves_like 'gracefully handling error' do let(:error_message) { %(Status cannot transition via \"run\") } end end context 'with finished job' do let(:job_status) { :success } it_behaves_like 'ignoring job' end context 'with failed job' do let(:job_status) { :failed } it_behaves_like 'synchronizing deployment' end context 'with unrelated job' do let(:job_status) { :waiting_for_resource } it_behaves_like 'ignoring job' end end end it_behaves_like 'sync status with a job' do let(:factory_type) { :ci_build } end it_behaves_like 'sync status with a job' do let(:factory_type) { :ci_bridge } end context 'when each job status is passed' do Deployment.statuses.each do |starting_status, _| Ci::HasStatus::AVAILABLE_STATUSES.each do |status| it "#{starting_status} to #{status} does not cause an error" do deployment.update!(status: starting_status) job = create(:ci_build, status: status) expect { deployment.sync_status_with(job) }.not_to raise_error end end end end end describe '#tags' do let(:deployment) { build(:deployment, project: project) } subject { deployment.tags } it 'will return tags related to this deployment' do expect(project.repository).to receive(:refs_by_oid).with( oid: deployment.sha, limit: 100, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX] ).and_return(["#{Gitlab::Git::TAG_REF_PREFIX}test"]) is_expected.to match_array(['refs/tags/test']) end end describe '#valid_sha' do it 'does not add errors for a valid SHA' do deploy = build(:deployment, project: project) expect(deploy).to be_valid end it 'adds an error for an invalid SHA' do deploy = build(:deployment, sha: 'foo') expect(deploy).not_to be_valid expect(deploy.errors[:sha]).not_to be_empty end end describe '#valid_ref' do it 'does not add errors for a valid ref' do deploy = build(:deployment, project: project) expect(deploy).to be_valid end it 'adds an error for an invalid ref' do deploy = build(:deployment, ref: 'does-not-exist') expect(deploy).not_to be_valid expect(deploy.errors[:ref]).not_to be_empty end end describe '#tier_in_yaml' do let(:deployment) { build(:deployment) } subject(:tier_in_yaml) { deployment.tier_in_yaml } context 'when deployable is nil' do before do deployment.deployable = nil end it { is_expected.to be_nil } end context 'when deployable is present' do context 'when tier is specified' do let(:deployable) { build(:ci_build, :success, :environment_with_deployment_tier) } before do deployment.deployable = deployable end it { is_expected.to eq('testing') } context 'when deployable is a bridge job' do let(:deployable) { build(:ci_bridge, :success, :environment_with_deployment_tier) } it { is_expected.to eq('testing') } end context 'when tier is not specified' do let(:deployable) { build(:ci_build, :success) } it { is_expected.to be_nil } end end end end describe '.fast_destroy_all' do it 'cleans path_refs for destroyed environments' do destroyed_deployments = create_list(:deployment, 2, :success, environment: environment, project: project) other_deployments = create_list(:deployment, 2, :success, environment: environment, project: project) (destroyed_deployments + other_deployments).each(&:create_ref) described_class.where(id: destroyed_deployments.map(&:id)).fast_destroy_all destroyed_deployments.each do |deployment| expect(project.commit(deployment.ref_path)).to be_nil end other_deployments.each do |deployment| expect(project.commit(deployment.ref_path)).not_to be_nil end end it 'does not trigger N+1 queries' do create(:deployment, environment: environment, project: project) control = ActiveRecord::QueryRecorder.new { project.deployments.fast_destroy_all } create_list(:deployment, 2, environment: environment, project: project) expect { project.deployments.fast_destroy_all }.not_to exceed_query_limit(control) end context 'when repository was already removed' do it 'removes deployment without any errors' do deployment = create(:deployment, environment: environment, project: project) ::Repositories::DestroyService.new(project.repository).execute project.save! # to trigger a repository removal expect { described_class.where(id: deployment).fast_destroy_all } .to change { Deployment.count }.by(-1) end end end describe '#update_merge_request_metrics!' do let_it_be(:merge_request) { create(:merge_request, :simple, :merged_last_month, project: project) } context 'with production environment' do before do deployment.update!(status: :success, finished_at: Time.current, environment: production) end it 'updates merge request metrics for production-grade environment' do expect { deployment.update_merge_request_metrics! } .to change { merge_request.reload.metrics.first_deployed_to_production_at } .from(nil).to(deployment.reload.finished_at) end end context 'with staging environment' do before do deployment.update!(status: :success, finished_at: Time.current, environment: staging) end it 'updates merge request metrics for production-grade environment' do expect { deployment.update_merge_request_metrics! } .not_to change { merge_request.reload.metrics.first_deployed_to_production_at } end end end end