Files
gitlab-foss/spec/models/deployment_spec.rb
2025-06-03 12:19:01 +00:00

1657 lines
52 KiB
Ruby

# 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