mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-25 16:03:48 +00:00
1657 lines
52 KiB
Ruby
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
|