Commit 52dcdfdd authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'ee-issue-42692-deployment-chat-notifications' into 'master'

Port of issue-42692-deployment-chat-notifications to EE

See merge request gitlab-org/gitlab-ee!11693
parents 41172099 12eb5e20
...@@ -47,6 +47,12 @@ class Deployment < ApplicationRecord ...@@ -47,6 +47,12 @@ class Deployment < ApplicationRecord
Deployments::SuccessWorker.perform_async(id) Deployments::SuccessWorker.perform_async(id)
end end
end end
after_transition any => [:success, :failed, :canceled] do |deployment|
deployment.run_after_commit do
Deployments::FinishedWorker.perform_async(id)
end
end
end end
enum status: { enum status: {
...@@ -82,6 +88,11 @@ class Deployment < ApplicationRecord ...@@ -82,6 +88,11 @@ class Deployment < ApplicationRecord
project.deployment_platform(environment: environment.name)&.cluster project.deployment_platform(environment: environment.name)&.cluster
end end
def execute_hooks
deployment_data = Gitlab::DataBuilder::Deployment.build(self)
project.execute_services(deployment_data, :deployment_hooks)
end
def last? def last?
self == environment.last_deployment self == environment.last_deployment
end end
......
# frozen_string_literal: true
module ChatMessage
class DeploymentMessage < BaseMessage
attr_reader :commit_url
attr_reader :deployable_id
attr_reader :deployable_url
attr_reader :environment
attr_reader :short_sha
attr_reader :status
def initialize(data)
super
@commit_url = data[:commit_url]
@deployable_id = data[:deployable_id]
@deployable_url = data[:deployable_url]
@environment = data[:environment]
@short_sha = data[:short_sha]
@status = data[:status]
end
def attachments
[{
text: "#{project_link}\n#{deployment_link}, SHA #{commit_link}, by #{user_combined_name}",
color: color
}]
end
def activity
{}
end
private
def message
"Deploy to #{environment} #{humanized_status}"
end
def color
case status
when 'success'
'good'
when 'canceled'
'warning'
when 'failed'
'danger'
else
'#334455'
end
end
def project_link
link(project_name, project_url)
end
def deployment_link
link("Job ##{deployable_id}", deployable_url)
end
def commit_link
link(short_sha, commit_url)
end
def humanized_status
status == 'success' ? 'succeeded' : status
end
end
end
...@@ -33,7 +33,7 @@ class ChatNotificationService < Service ...@@ -33,7 +33,7 @@ class ChatNotificationService < Service
def self.supported_events def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push %w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page] pipeline wiki_page deployment]
end end
def fields def fields
...@@ -122,6 +122,8 @@ class ChatNotificationService < Service ...@@ -122,6 +122,8 @@ class ChatNotificationService < Service
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page" when "wiki_page"
ChatMessage::WikiPageMessage.new(data) ChatMessage::WikiPageMessage.new(data)
when "deployment"
ChatMessage::DeploymentMessage.new(data)
end end
end end
......
...@@ -33,6 +33,11 @@ class DiscordService < ChatNotificationService ...@@ -33,6 +33,11 @@ class DiscordService < ChatNotificationService
# No-op. # No-op.
end end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields def default_fields
[ [
{ type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" }, { type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" },
......
...@@ -35,6 +35,11 @@ class HangoutsChatService < ChatNotificationService ...@@ -35,6 +35,11 @@ class HangoutsChatService < ChatNotificationService
'https://chat.googleapis.com/v1/spaces…' 'https://chat.googleapis.com/v1/spaces…'
end end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" }, { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
......
...@@ -33,6 +33,11 @@ class MicrosoftTeamsService < ChatNotificationService ...@@ -33,6 +33,11 @@ class MicrosoftTeamsService < ChatNotificationService
def default_channel_placeholder def default_channel_placeholder
end end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" }, { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
......
...@@ -50,6 +50,7 @@ class Service < ApplicationRecord ...@@ -50,6 +50,7 @@ class Service < ApplicationRecord
scope :job_hooks, -> { where(job_events: true, active: true) } scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') } scope :deployment, -> { where(category: 'deployment') }
...@@ -335,6 +336,8 @@ class Service < ApplicationRecord ...@@ -335,6 +336,8 @@ class Service < ApplicationRecord
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events" when "commit", "commit_events"
"Event will be triggered when a commit is created/updated" "Event will be triggered when a commit is created/updated"
when "deployment"
"Event will be triggered when a deployment finishes"
end end
end end
......
...@@ -83,6 +83,7 @@ ...@@ -83,6 +83,7 @@
- pipeline_processing:ci_build_schedule - pipeline_processing:ci_build_schedule
- deployment:deployments_success - deployment:deployments_success
- deployment:deployments_finished
- repository_check:repository_check_clear - repository_check:repository_check_clear
- repository_check:repository_check_batch - repository_check:repository_check_batch
......
# frozen_string_literal: true
module Deployments
class FinishedWorker
include ApplicationWorker
queue_namespace :deployment
def perform(deployment_id)
Deployment.find_by_id(deployment_id).try(:execute_hooks)
end
end
end
---
title: Add deployment events to chat notification services
merge_request: 27338
author:
type: added
# frozen_string_literal: true
class AddDeploymentEventsToServices < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:services, :deployment_events, :boolean, default: false, allow_null: false)
end
def down
remove_column(:services, :deployment_events)
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190423124640) do ActiveRecord::Schema.define(version: 20190426180107) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -2893,6 +2893,7 @@ ActiveRecord::Schema.define(version: 20190423124640) do ...@@ -2893,6 +2893,7 @@ ActiveRecord::Schema.define(version: 20190423124640) do
t.boolean "commit_events", default: true, null: false t.boolean "commit_events", default: true, null: false
t.boolean "job_events", default: false, null: false t.boolean "job_events", default: false, null: false
t.boolean "confidential_note_events", default: true t.boolean "confidential_note_events", default: true
t.boolean "deployment_events", default: false, null: false
t.index ["project_id"], name: "index_services_on_project_id", using: :btree t.index ["project_id"], name: "index_services_on_project_id", using: :btree
t.index ["template"], name: "index_services_on_template", using: :btree t.index ["template"], name: "index_services_on_template", using: :btree
t.index ["type"], name: "index_services_on_type", using: :btree t.index ["type"], name: "index_services_on_type", using: :btree
......
...@@ -35,6 +35,7 @@ describe Ci::ProcessBuildService, '#execute' do ...@@ -35,6 +35,7 @@ describe Ci::ProcessBuildService, '#execute' do
context 'when user does not have access to the environment' do context 'when user does not have access to the environment' do
it 'fails the build' do it 'fails the build' do
allow(Deployments::FinishedWorker).to receive(:perform_async)
subject subject
expect(ci_build.failed?).to be_truthy expect(ci_build.failed?).to be_truthy
......
# frozen_string_literal: true
module Gitlab
module DataBuilder
module Deployment
extend self
def build(deployment)
{
object_kind: 'deployment',
status: deployment.status,
deployable_id: deployment.deployable_id,
deployable_url: Gitlab::UrlBuilder.build(deployment.deployable),
environment: deployment.environment.name,
project: deployment.project.hook_attrs,
short_sha: deployment.short_sha,
user: deployment.user.hook_attrs,
commit_url: Gitlab::UrlBuilder.build(deployment.commit)
}
end
end
end
end
...@@ -30,6 +30,8 @@ module Gitlab ...@@ -30,6 +30,8 @@ module Gitlab
snippet_url(object) snippet_url(object)
when Milestone when Milestone
milestone_url(object) milestone_url(object)
when ::Ci::Build
project_job_url(object.project, object)
else else
raise NotImplementedError.new("No URL builder defined for #{object.class}") raise NotImplementedError.new("No URL builder defined for #{object.class}")
end end
......
FactoryBot.define do FactoryBot.define do
factory :deployment, class: Deployment do factory :deployment, class: Deployment do
sha '97de212e80737a608d939f648d959671fb0a0142' sha 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
ref 'master' ref 'master'
tag false tag false
user nil user nil
......
...@@ -255,6 +255,13 @@ describe 'Admin updates settings' do ...@@ -255,6 +255,13 @@ describe 'Admin updates settings' do
expect(find_field('Username').value).to eq 'test_user' expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel' expect(find('#service_push_channel').value).to eq '#test_channel'
end end
it 'defaults Deployment events to false for chat notification template settings' do
first(:link, 'Service Templates').click
click_link 'Slack notifications'
expect(find_field('Deployment')).not_to be_checked
end
end end
context 'CI/CD page' do context 'CI/CD page' do
...@@ -398,10 +405,14 @@ describe 'Admin updates settings' do ...@@ -398,10 +405,14 @@ describe 'Admin updates settings' do
def check_all_events def check_all_events
page.check('Active') page.check('Active')
page.check('Push') page.check('Push')
page.check('Tag push')
page.check('Note')
page.check('Issue') page.check('Issue')
page.check('Confidential issue')
page.check('Merge request') page.check('Merge request')
page.check('Note')
page.check('Confidential note')
page.check('Tag push')
page.check('Pipeline') page.check('Pipeline')
page.check('Wiki page')
page.check('Deployment')
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::DataBuilder::Deployment do
describe '.build' do
it 'returns the object kind for a deployment' do
deployment = build(:deployment)
data = described_class.build(deployment)
expect(data[:object_kind]).to eq('deployment')
end
it 'returns data for the given build' do
environment = create(:environment, name: "somewhere")
project = create(:project, :repository, name: 'myproj')
commit = project.commit('HEAD')
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_commit_url = Gitlab::UrlBuilder.build(commit)
data = described_class.build(deployment)
expect(data[:status]).to eq('failed')
expect(data[:deployable_id]).to eq(deployable.id)
expect(data[:deployable_url]).to eq(expected_deployable_url)
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[:commit_url]).to eq(expected_commit_url)
end
end
end
...@@ -426,6 +426,7 @@ Service: ...@@ -426,6 +426,7 @@ Service:
- wiki_page_events - wiki_page_events
- confidential_issues_events - confidential_issues_events
- confidential_note_events - confidential_note_events
- deployment_events
ProjectHook: ProjectHook:
- id - id
- url - url
......
...@@ -856,6 +856,10 @@ describe Ci::Build do ...@@ -856,6 +856,10 @@ describe Ci::Build do
let(:deployment) { build.deployment } let(:deployment) { build.deployment }
let(:environment) { deployment.environment } let(:environment) { deployment.environment }
before do
allow(Deployments::FinishedWorker).to receive(:perform_async)
end
it 'has deployments record with created status' do it 'has deployments record with created status' do
expect(deployment).to be_created expect(deployment).to be_created
expect(environment.name).to eq('review/master') expect(environment.name).to eq('review/master')
......
...@@ -102,6 +102,13 @@ describe Deployment do ...@@ -102,6 +102,13 @@ describe Deployment do
deployment.succeed! deployment.succeed!
end end
it 'executes Deployments::FinishedWorker asynchronously' do
expect(Deployments::FinishedWorker)
.to receive(:perform_async).with(deployment.id)
deployment.succeed!
end
end end
context 'when deployment failed' do context 'when deployment failed' do
...@@ -115,6 +122,13 @@ describe Deployment do ...@@ -115,6 +122,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now) expect(deployment.finished_at).to be_like_time(Time.now)
end end
end end
it 'executes Deployments::FinishedWorker asynchronously' do
expect(Deployments::FinishedWorker)
.to receive(:perform_async).with(deployment.id)
deployment.drop!
end
end end
context 'when deployment was canceled' do context 'when deployment was canceled' do
...@@ -128,6 +142,13 @@ describe Deployment do ...@@ -128,6 +142,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now) expect(deployment.finished_at).to be_like_time(Time.now)
end end
end end
it 'executes Deployments::FinishedWorker asynchronously' do
expect(Deployments::FinishedWorker)
.to receive(:perform_async).with(deployment.id)
deployment.cancel!
end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe ChatMessage::DeploymentMessage do
describe '#pretext' do
it 'returns a message with the data returned by the deployment data builder' do
environment = create(:environment, name: "myenvironment")
project = create(:project, :repository)
commit = project.commit('HEAD')
deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha)
data = Gitlab::DataBuilder::Deployment.build(deployment)
message = described_class.new(data)
expect(message.pretext).to eq("Deploy to myenvironment succeeded")
end
it 'returns a message for a successful deployment' do
data = {
status: 'success',
environment: 'production'
}
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to production succeeded')
end
it 'returns a message for a failed deployment' do
data = {
status: 'failed',
environment: 'production'
}
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to production failed')
end
it 'returns a message for a canceled deployment' do
data = {
status: 'canceled',
environment: 'production'
}
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to production canceled')
end
it 'returns a message for a deployment to another environment' do
data = {
status: 'success',
environment: 'staging'
}
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to staging succeeded')
end
it 'returns a message for a deployment with any other status' do
data = {
status: 'unknown',
environment: 'staging'
}
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to staging unknown')
end
end
describe '#attachments' do
def deployment_data(params)
{
object_kind: "deployment",
status: "success",
deployable_id: 3,
deployable_url: "deployable_url",
environment: "sandbox",
project: {
name: "greatproject",
web_url: "project_web_url",
path_with_namespace: "project_path_with_namespace"
},
user: {
name: "Jane Person",
username: "jane"
},
short_sha: "12345678",
commit_url: "commit_url"
}.merge(params)
end
it 'returns attachments with the data returned by the deployment data builder' do
user = create(:user, name: "John Smith", username: "smith")
namespace = create(:namespace, name: "myspace")
project = create(:project, :repository, namespace: namespace, name: "myproject")
commit = project.commit('HEAD')
environment = create(:environment, name: "myenvironment", project: project)
ci_build = create(:ci_build, project: project)
deployment = create(:deployment, :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha)
job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build)
commit_url = Gitlab::UrlBuilder.build(deployment.commit)
data = Gitlab::DataBuilder::Deployment.build(deployment)
message = described_class.new(data)
expect(message.attachments).to eq([{
text: "[myspace/myproject](#{project.web_url})\n[Job ##{ci_build.id}](#{job_url}), SHA [#{deployment.short_sha}](#{commit_url}), by John Smith (smith)",
color: "good"
}])
end
it 'returns attachments for a failed deployment' do
data = deployment_data(status: 'failed')
message = described_class.new(data)
expect(message.attachments).to eq([{
text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
color: "danger"
}])
end
it 'returns attachments for a canceled deployment' do
data = deployment_data(status: 'canceled')
message = described_class.new(data)
expect(message.attachments).to eq([{
text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
color: "warning"
}])
end
it 'uses a neutral color for a deployment with any other status' do
data = deployment_data(status: 'some-new-status-we-make-in-the-future')
message = described_class.new(data)
expect(message.attachments).to eq([{
text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
color: "#334455"
}])
end
end
end
...@@ -30,6 +30,12 @@ describe MicrosoftTeamsService do ...@@ -30,6 +30,12 @@ describe MicrosoftTeamsService do
end end
end end
describe '.supported_events' do
it 'does not support deployment_events' do
expect(described_class.supported_events).not_to include('deployment')
end
end
describe "#execute" do describe "#execute" do
let(:user) { create(:user) } let(:user) { create(:user) }
set(:project) { create(:project, :repository, :wiki_repo) } set(:project) { create(:project, :repository, :wiki_repo) }
......
...@@ -22,6 +22,7 @@ describe UpdateDeploymentService do ...@@ -22,6 +22,7 @@ describe UpdateDeploymentService do
subject(:service) { described_class.new(deployment) } subject(:service) { described_class.new(deployment) }
before do before do
allow(Deployments::FinishedWorker).to receive(:perform_async)
job.success! # Create/Succeed deployment job.success! # Create/Succeed deployment
end end
......
...@@ -25,6 +25,12 @@ shared_examples_for "chat service" do |service_name| ...@@ -25,6 +25,12 @@ shared_examples_for "chat service" do |service_name|
end end
end end
describe '.supported_events' do
it 'does not support deployment_events' do
expect(described_class.supported_events).not_to include('deployment')
end
end
describe "#execute" do describe "#execute" do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
......
...@@ -106,6 +106,14 @@ RSpec.shared_examples 'slack or mattermost notifications' do ...@@ -106,6 +106,14 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
it "calls Slack/Mattermost API for deployment events" do
deployment_event_data = { object_kind: 'deployment' }
chat_service.execute(deployment_event_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
it 'uses the username as an option for slack when configured' do it 'uses the username as an option for slack when configured' do
allow(chat_service).to receive(:username).and_return(username) allow(chat_service).to receive(:username).and_return(username)
......
...@@ -15,6 +15,7 @@ describe BuildSuccessWorker do ...@@ -15,6 +15,7 @@ describe BuildSuccessWorker do
let!(:build) { create(:ci_build, :deploy_to_production) } let!(:build) { create(:ci_build, :deploy_to_production) }
before do before do
allow(Deployments::FinishedWorker).to receive(:perform_async)
Deployment.delete_all Deployment.delete_all
build.reload build.reload
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Deployments::FinishedWorker 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)
project = deployment.project
service = create(:service, type: 'SlackService', project: project, deployment_events: true, active: true)
worker.perform(deployment.id)
expect(ProjectServiceWorker).to have_received(:perform_async).with(service.id, an_instance_of(Hash))
end
it 'does not execute an inactive service' do
deployment = create(:deployment)
project = deployment.project
create(:service, type: 'SlackService', project: project, deployment_events: true, active: false)
worker.perform(deployment.id)
expect(ProjectServiceWorker).not_to have_received(:perform_async)
end
it 'does nothing if a deployment with the given id does not exist' do
worker.perform(0)
expect(ProjectServiceWorker).not_to have_received(:perform_async)
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