Commit b9cc47a8 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'sk/235475-add-hook-on-deployment-create' into 'master'

Execute project hooks and services when deployment starts

See merge request gitlab-org/gitlab!41214
parents 32a796a9 c2d78268
......@@ -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"
......@@ -28857,13 +28857,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