Commit c2d78268 authored by Sashi's avatar Sashi Committed by Sashi Kumar

Execute project hooks and services when deployment starts

This commit changes the deployment hook feature to additionally
trigger an event when a deployment starts.
parent c260eed3
......@@ -24,7 +24,7 @@ module ServicesHelper
when "commit", "commit_events"
s_("ProjectService|Event will be triggered when a commit is created/updated")
when "deployment"
s_("ProjectService|Event will be triggered when a deployment finishes")
s_("ProjectService|Event will be triggered when a deployment starts or finishes")
when "alert"
s_("ProjectService|Event will be triggered when a new, unique alert is recorded")
end
......
......@@ -46,6 +46,8 @@ class Deployment < ApplicationRecord
scope :older_than, -> (deployment) { where('id < ?', deployment.id) }
scope :with_deployable, -> { includes(:deployable).where('deployable_id IS NOT NULL') }
FINISHED_STATUSES = %i[success failed canceled].freeze
state_machine :status, initial: :created do
event :run do
transition created: :running
......@@ -63,27 +65,41 @@ class Deployment < ApplicationRecord
transition any - [:canceled] => :canceled
end
before_transition any => [:success, :failed, :canceled] do |deployment|
before_transition any => FINISHED_STATUSES do |deployment|
deployment.finished_at = Time.current
end
after_transition any => :success do |deployment|
after_transition any => :running do |deployment|
next unless deployment.project.forward_deployment_enabled?
deployment.run_after_commit do
Deployments::SuccessWorker.perform_async(id)
Deployments::ForwardDeploymentWorker.perform_async(id)
end
end
after_transition any => [:success, :failed, :canceled] do |deployment|
after_transition any => :running do |deployment|
deployment.run_after_commit do
Deployments::FinishedWorker.perform_async(id)
next unless Feature.enabled?(:ci_send_deployment_hook_when_start, deployment.project)
Deployments::ExecuteHooksWorker.perform_async(id)
end
end
after_transition any => :running do |deployment|
next unless deployment.project.forward_deployment_enabled?
after_transition any => :success do |deployment|
deployment.run_after_commit do
Deployments::UpdateEnvironmentWorker.perform_async(id)
end
end
after_transition any => FINISHED_STATUSES do |deployment|
deployment.run_after_commit do
Deployments::ForwardDeploymentWorker.perform_async(id)
Deployments::LinkMergeRequestWorker.perform_async(id)
end
end
after_transition any => FINISHED_STATUSES do |deployment|
deployment.run_after_commit do
Deployments::ExecuteHooksWorker.perform_async(id)
end
end
end
......@@ -273,7 +289,7 @@ class Deployment < ApplicationRecord
SQL
end
# Changes the status of a deployment and triggers the correspinding state
# Changes the status of a deployment and triggers the corresponding state
# machine events.
def update_status(status)
case status
......
......@@ -38,7 +38,11 @@ module ChatMessage
private
def message
"Deploy to #{environment} #{humanized_status}"
if running?
"Starting deploy to #{environment}"
else
"Deploy to #{environment} #{humanized_status}"
end
end
def color
......@@ -73,5 +77,9 @@ module ChatMessage
def humanized_status
status == 'success' ? 'succeeded' : status
end
def running?
status == 'running'
end
end
end
# frozen_string_literal: true
module Deployments
class AfterCreateService
class UpdateEnvironmentService
attr_reader :deployment
attr_reader :deployable
......@@ -64,4 +64,4 @@ module Deployments
end
end
Deployments::AfterCreateService.prepend_if_ee('EE::Deployments::AfterCreateService')
Deployments::UpdateEnvironmentService.prepend_if_ee('EE::Deployments::UpdateEnvironmentService')
......@@ -77,7 +77,7 @@
= form.label :deployment_events, class: 'list-label form-check-label ml-1' do
%strong= s_('Webhooks|Deployment events')
%p.text-muted.ml-1
= s_('Webhooks|This URL will be triggered when a deployment is finished/failed/canceled')
= s_('Webhooks|This URL is triggered when a deployment starts, finishes, fails, or is canceled')
.form-group
= form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
.form-check
......
......@@ -427,6 +427,14 @@
:weight: 1
:idempotent: true
:tags: []
- :name: deployment:deployments_execute_hooks
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 3
:idempotent:
:tags: []
- :name: deployment:deployments_finished
:feature_category: :continuous_delivery
:has_external_dependencies:
......@@ -443,6 +451,14 @@
:weight: 3
:idempotent:
:tags: []
- :name: deployment:deployments_link_merge_request
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 3
:idempotent: true
:tags: []
- :name: deployment:deployments_success
:feature_category: :continuous_delivery
:has_external_dependencies:
......@@ -451,6 +467,14 @@
:weight: 3
:idempotent:
:tags: []
- :name: deployment:deployments_update_environment
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 3
:idempotent: true
:tags: []
- :name: gcp_cluster:cluster_configure_istio
:feature_category: :kubernetes_management
:has_external_dependencies: true
......
# frozen_string_literal: true
module Deployments
class ExecuteHooksWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
queue_namespace :deployment
feature_category :continuous_delivery
worker_resource_boundary :cpu
def perform(deployment_id)
if (deploy = Deployment.find_by_id(deployment_id))
deploy.execute_hooks
end
end
end
end
# frozen_string_literal: true
module Deployments
class LinkMergeRequestWorker
include ApplicationWorker
queue_namespace :deployment
idempotent!
feature_category :continuous_delivery
worker_resource_boundary :cpu
def perform(deployment_id)
if (deploy = Deployment.find_by_id(deployment_id))
LinkMergeRequestsService.new(deploy).execute
end
end
end
end
......@@ -12,7 +12,7 @@ module Deployments
Deployment.find_by_id(deployment_id).try do |deployment|
break unless deployment.success?
Deployments::AfterCreateService.new(deployment).execute
Deployments::UpdateEnvironmentService.new(deployment).execute
end
end
end
......
# frozen_string_literal: true
module Deployments
class UpdateEnvironmentWorker
include ApplicationWorker
queue_namespace :deployment
idempotent!
feature_category :continuous_delivery
worker_resource_boundary :cpu
def perform(deployment_id)
Deployment.find_by_id(deployment_id).try do |deployment|
break unless deployment.success?
Deployments::UpdateEnvironmentService.new(deployment).execute
end
end
end
end
---
title: Send chat notification when deployment starts
merge_request: 41214
author: Sashi Kumar
type: added
---
name: ci_send_deployment_hook_when_start
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41214
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247137
group: group::progressive delivery
type: development
default_enabled: false
......@@ -1303,7 +1303,12 @@ Note that `commit.id` is the ID of the pipeline, not the ID of the commit.
### Deployment events
Triggered when deployment is finished/failed/canceled.
Triggered when a deployment:
- Starts ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41214) in GitLab 13.5.)
- Succeeds
- Fails
- Is cancelled
**Request Header**:
......
......@@ -2,7 +2,7 @@
module EE
module Deployments
module AfterCreateService
module UpdateEnvironmentService
extend ::Gitlab::Utils::Override
override :execute
......
......@@ -35,7 +35,8 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
context 'when user does not have access to the environment' do
it 'fails the build' do
allow(Deployments::FinishedWorker).to receive(:perform_async)
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
allow(Deployments::ExecuteHooksWorker).to receive(:perform_async)
subject
expect(ci_build.failed?).to be_truthy
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Deployments::AfterCreateService do
RSpec.describe Deployments::UpdateEnvironmentService do
include ::EE::GeoHelpers
let(:primary) { create(:geo_node, :primary) }
......
......@@ -20,8 +20,8 @@ module Gitlab
environment: deployment.environment.name,
project: deployment.project.hook_attrs,
short_sha: deployment.short_sha,
user: deployment.user.hook_attrs,
user_url: Gitlab::UrlBuilder.build(deployment.user),
user: deployment.deployed_by.hook_attrs,
user_url: Gitlab::UrlBuilder.build(deployment.deployed_by),
commit_url: Gitlab::UrlBuilder.build(deployment.commit),
commit_title: deployment.commit.title
}
......
......@@ -20015,7 +20015,7 @@ msgstr ""
msgid "ProjectService|Event will be triggered when a confidential issue is created/updated/closed"
msgstr ""
msgid "ProjectService|Event will be triggered when a deployment finishes"
msgid "ProjectService|Event will be triggered when a deployment starts or finishes"
msgstr ""
msgid "ProjectService|Event will be triggered when a merge request is created/updated/merged"
......@@ -28851,13 +28851,13 @@ msgstr ""
msgid "Webhooks|Tag push events"
msgstr ""
msgid "Webhooks|This URL will be triggered by a push to the repository"
msgid "Webhooks|This URL is triggered when a deployment starts, finishes, fails, or is canceled"
msgstr ""
msgid "Webhooks|This URL will be triggered when a confidential issue is created/updated/merged"
msgid "Webhooks|This URL will be triggered by a push to the repository"
msgstr ""
msgid "Webhooks|This URL will be triggered when a deployment is finished/failed/canceled"
msgid "Webhooks|This URL will be triggered when a confidential issue is created/updated/merged"
msgstr ""
msgid "Webhooks|This URL will be triggered when a merge request is created/updated/merged"
......
......@@ -19,7 +19,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project)
deployable = deployment.deployable
expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable)
expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.user)
expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.deployed_by)
expected_commit_url = Gitlab::UrlBuilder.build(commit)
data = described_class.build(deployment)
......@@ -30,7 +30,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
expect(data[:environment]).to eq("somewhere")
expect(data[:project]).to eq(project.hook_attrs)
expect(data[:short_sha]).to eq(deployment.short_sha)
expect(data[:user]).to eq(deployment.user.hook_attrs)
expect(data[:user]).to eq(deployment.deployed_by.hook_attrs)
expect(data[:user_url]).to eq(expected_user_url)
expect(data[:commit_url]).to eq(expected_commit_url)
expect(data[:commit_title]).to eq(commit.title)
......
......@@ -1109,7 +1109,8 @@ RSpec.describe Ci::Build do
let(:environment) { deployment.environment }
before do
allow(Deployments::FinishedWorker).to receive(:perform_async)
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
allow(Deployments::ExecuteHooksWorker).to receive(:perform_async)
end
it 'has deployments record with created status' do
......@@ -1129,7 +1130,8 @@ RSpec.describe Ci::Build do
context 'when transits to success' do
before do
allow(Deployments::SuccessWorker).to receive(:perform_async)
allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
allow(Deployments::ExecuteHooksWorker).to receive(:perform_async)
build.success!
end
......
......@@ -98,16 +98,36 @@ RSpec.describe Deployment do
context 'when deployment runs' do
let(:deployment) { create(:deployment) }
before do
deployment.run!
end
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::ExecuteHooksWorker asynchronously' do
expect(Deployments::ExecuteHooksWorker)
.to receive(:perform_async).with(deployment.id)
deployment.run!
end
it 'does not execute Deployments::ExecuteHooksWorker when feature is disabled' do
stub_feature_flags(ci_send_deployment_hook_when_start: false)
expect(Deployments::ExecuteHooksWorker)
.not_to receive(:perform_async).with(deployment.id)
deployment.run!
end
it 'executes Deployments::ForwardDeploymentWorker asynchronously' do
expect(Deployments::ForwardDeploymentWorker)
.to receive(:perform_async).once.with(deployment.id)
deployment.run!
end
end
context 'when deployment succeeded' do
......@@ -122,15 +142,15 @@ RSpec.describe Deployment do
end
end
it 'executes Deployments::SuccessWorker asynchronously' do
expect(Deployments::SuccessWorker)
it 'executes Deployments::UpdateEnvironmentWorker asynchronously' do
expect(Deployments::UpdateEnvironmentWorker)
.to receive(:perform_async).with(deployment.id)
deployment.succeed!
end
it 'executes Deployments::FinishedWorker asynchronously' do
expect(Deployments::FinishedWorker)
it 'executes Deployments::ExecuteHooksWorker asynchronously' do
expect(Deployments::ExecuteHooksWorker)
.to receive(:perform_async).with(deployment.id)
deployment.succeed!
......@@ -149,12 +169,19 @@ RSpec.describe Deployment do
end
end
it 'executes Deployments::FinishedWorker asynchronously' do
expect(Deployments::FinishedWorker)
it 'executes Deployments::LinkMergeRequestWorker asynchronously' do
expect(Deployments::LinkMergeRequestWorker)
.to receive(:perform_async).with(deployment.id)
deployment.drop!
end
it 'executes Deployments::ExecuteHooksWorker asynchronously' do
expect(Deployments::ExecuteHooksWorker)
.to receive(:perform_async).with(deployment.id)
deployment.drop!
end
end
context 'when deployment was canceled' do
......@@ -169,12 +196,19 @@ RSpec.describe Deployment do
end
end
it 'executes Deployments::FinishedWorker asynchronously' do
expect(Deployments::FinishedWorker)
it 'executes Deployments::LinkMergeRequestWorker asynchronously' do
expect(Deployments::LinkMergeRequestWorker)
.to receive(:perform_async).with(deployment.id)
deployment.cancel!
end
it 'executes Deployments::ExecuteHooksWorker asynchronously' do
expect(Deployments::ExecuteHooksWorker)
.to receive(:perform_async).with(deployment.id)
deployment.cancel!
end
end
end
......@@ -580,9 +614,10 @@ RSpec.describe Deployment do
expect(deploy).to be_success
end
it 'schedules SuccessWorker and FinishedWorker when finishing a deploy' do
expect(Deployments::SuccessWorker).to receive(:perform_async)
expect(Deployments::FinishedWorker).to receive(:perform_async)
it 'schedules workers when finishing a deploy' do
expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
expect(Deployments::ExecuteHooksWorker).to receive(:perform_async)
deploy.update_status('success')
end
......
......@@ -70,6 +70,17 @@ RSpec.describe ChatMessage::DeploymentMessage do
expect(message.pretext).to eq('Deploy to staging unknown')
end
it 'returns a message for a running deployment' do
data = {
status: 'running',
environment: 'production'
}
message = described_class.new(data)
expect(message.pretext).to eq('Starting deploy to production')
end
end
describe '#attachments' do
......
......@@ -19,8 +19,9 @@ RSpec.describe Deployments::CreateService do
status: 'success'
)
expect(Deployments::SuccessWorker).to receive(:perform_async)
expect(Deployments::FinishedWorker).to receive(:perform_async)
expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
expect(Deployments::ExecuteHooksWorker).to receive(:perform_async)
expect(service.execute).to be_persisted
end
......@@ -34,8 +35,9 @@ RSpec.describe Deployments::CreateService do
tag: false
)
expect(Deployments::SuccessWorker).not_to receive(:perform_async)
expect(Deployments::FinishedWorker).not_to receive(:perform_async)
expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async)
expect(Deployments::ExecuteHooksWorker).not_to receive(:perform_async)
expect(service.execute).to be_persisted
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Deployments::AfterCreateService do
RSpec.describe Deployments::UpdateEnvironmentService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:options) { { name: 'production' } }
......@@ -31,7 +31,8 @@ RSpec.describe Deployments::AfterCreateService do
subject(:service) { described_class.new(deployment) }
before do
allow(Deployments::FinishedWorker).to receive(:perform_async)
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
allow(Deployments::ExecuteHooksWorker).to receive(:perform_async)
job.success! # Create/Succeed deployment
end
......@@ -100,8 +101,8 @@ RSpec.describe Deployments::AfterCreateService do
end
before do
environment.update(name: 'review-apps/master')
job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME')
environment.update!(name: 'review-apps/master')
job.update!(environment: 'review-apps/$CI_COMMIT_REF_NAME')
end
it 'does not create a new environment' do
......@@ -241,7 +242,7 @@ RSpec.describe Deployments::AfterCreateService do
end
it 'does not raise errors if the merge request does not have a metrics record' do
merge_request.metrics.destroy
merge_request.metrics.destroy!
expect(merge_request.reload.metrics).to be_nil
expect { service.execute }.not_to raise_error
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Deployments::ExecuteHooksWorker do
let(:worker) { described_class.new }
describe '#perform' do
before do
allow(ProjectServiceWorker).to receive(:perform_async)
end
it 'executes project services for deployment_hooks' do
deployment = create(:deployment, :running)
project = deployment.project
service = create(:service, type: 'SlackService', project: project, deployment_events: true, active: true)
expect(ProjectServiceWorker).to receive(:perform_async).with(service.id, an_instance_of(Hash))
worker.perform(deployment.id)
end
it 'does not execute an inactive service' do
deployment = create(:deployment, :running)
project = deployment.project
create(:service, type: 'SlackService', project: project, deployment_events: true, active: false)
expect(ProjectServiceWorker).not_to receive(:perform_async)
worker.perform(deployment.id)
end
it 'does not execute if a deployment does not exist' do
expect(ProjectServiceWorker).not_to receive(:perform_async)
worker.perform(non_existing_record_id)
end
it 'execute webhooks' do
deployment = create(:deployment, :running)
project = deployment.project
web_hook = create(:project_hook, deployment_events: true, project: project)
expect_next_instance_of(WebHookService, web_hook, an_instance_of(Hash), "deployment_hooks") do |service|
expect(service).to receive(:async_execute)
end
worker.perform(deployment.id)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Deployments::LinkMergeRequestWorker do
subject(:worker) { described_class.new }
describe '#perform' do
it 'links merge requests to the deployment' do
deployment = create(:deployment)
service = instance_double(Deployments::LinkMergeRequestsService)
expect(Deployments::LinkMergeRequestsService)
.to receive(:new)
.with(deployment)
.and_return(service)
expect(service).to receive(:execute)
worker.perform(deployment.id)
end
it 'does not link merge requests when the deployment is not found' do
expect(Deployments::LinkMergeRequestsService).not_to receive(:new)
worker.perform(non_existing_record_id)
end
end
context 'idempotent' do
include_examples 'an idempotent worker' do
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
let(:deployment) { create(:deployment, :success, project: project, environment: environment) }
let(:job_args) { deployment.id }
it 'links merge requests to deployment' do
mr1 = create(
:merge_request,
:merged,
source_project: project,
target_project: project,
source_branch: 'source1',
target_branch: deployment.ref
)
mr2 = create(
:merge_request,
:merged,
source_project: project,
target_project: project,
source_branch: 'source2',
target_branch: deployment.ref
)
mr3 = create(
:merge_request,
:merged,
source_project: project,
target_project: project,
target_branch: 'foo'
)
subject
expect(deployment.merge_requests).to include(mr1, mr2)
expect(deployment.merge_requests).not_to include(mr3)
end
end
end
end
......@@ -8,8 +8,8 @@ RSpec.describe Deployments::SuccessWorker do
context 'when successful deployment' do
let(:deployment) { create(:deployment, :success) }
it 'executes Deployments::AfterCreateService' do
expect(Deployments::AfterCreateService)
it 'executes Deployments::UpdateEnvironmentService' do
expect(Deployments::UpdateEnvironmentService)
.to receive(:new).with(deployment).and_call_original
subject
......@@ -19,8 +19,8 @@ RSpec.describe Deployments::SuccessWorker do
context 'when canceled deployment' do
let(:deployment) { create(:deployment, :canceled) }
it 'does not execute Deployments::AfterCreateService' do
expect(Deployments::AfterCreateService).not_to receive(:new)
it 'does not execute Deployments::UpdateEnvironmentService' do
expect(Deployments::UpdateEnvironmentService).not_to receive(:new)
subject
end
......@@ -29,8 +29,8 @@ RSpec.describe Deployments::SuccessWorker do
context 'when deploy record does not exist' do
let(:deployment) { nil }
it 'does not execute Deployments::AfterCreateService' do
expect(Deployments::AfterCreateService).not_to receive(:new)
it 'does not execute Deployments::UpdateEnvironmentService' do
expect(Deployments::UpdateEnvironmentService).not_to receive(:new)
subject
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Deployments::UpdateEnvironmentWorker do
subject(:worker) { described_class.new }
context 'when successful deployment' do
let(:deployment) { create(:deployment, :success) }
it 'executes Deployments::UpdateEnvironmentService' do
service = instance_double(Deployments::UpdateEnvironmentService)
expect(Deployments::UpdateEnvironmentService)
.to receive(:new)
.with(deployment)
.and_return(service)
expect(service).to receive(:execute)
worker.perform(deployment.id)
end
end
context 'when canceled deployment' do
let(:deployment) { create(:deployment, :canceled) }
it 'does not execute Deployments::UpdateEnvironmentService' do
expect(Deployments::UpdateEnvironmentService).not_to receive(:new)
worker.perform(deployment.id)
end
end
context 'when deploy record does not exist' do
it 'does not execute Deployments::UpdateEnvironmentService' do
expect(Deployments::UpdateEnvironmentService).not_to receive(:new)
worker.perform(non_existing_record_id)
end
end
context 'idempotent' do
include_examples 'an idempotent worker' do
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, name: 'production') }
let(:deployment) { create(:deployment, :success, project: project, environment: environment) }
let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) }
let(:job_args) { deployment.id }
before do
merge_request.metrics.update!(merged_at: 1.hour.ago)
end
it 'updates merge requests metrics' do
subject
expect(merge_request.reload.metrics.first_deployed_to_production_at)
.to be_like_time(deployment.finished_at)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment