Merge branch 'chatops-deploy-command' into 'master'
Add deploy chat command This adds a new ChatOps command: ``` /trigger deploy <environment> to <environment> ``` See merge request !7619
This commit is contained in:
commit
2449d787ad
10 changed files with 219 additions and 15 deletions
|
@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base
|
|||
allow_nil: true,
|
||||
addressable_url: true
|
||||
|
||||
delegate :stop_action, to: :last_deployment, allow_nil: true
|
||||
delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
|
||||
|
||||
scope :available, -> { with_state(:available) }
|
||||
scope :stopped, -> { with_state(:stopped) }
|
||||
|
@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base
|
|||
stop
|
||||
stop_action.play(current_user)
|
||||
end
|
||||
|
||||
def actions_for(environment)
|
||||
return [] unless manual_actions
|
||||
|
||||
manual_actions.select do |action|
|
||||
action.expanded_environment_name == environment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
4
changelogs/unreleased/chatops-deploy-command.yml
Normal file
4
changelogs/unreleased/chatops-deploy-command.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add deployment command to ChatOps
|
||||
merge_request: 7619
|
||||
author:
|
|
@ -4,6 +4,7 @@ module Gitlab
|
|||
COMMANDS = [
|
||||
Gitlab::ChatCommands::IssueShow,
|
||||
Gitlab::ChatCommands::IssueCreate,
|
||||
Gitlab::ChatCommands::Deploy,
|
||||
].freeze
|
||||
|
||||
def execute
|
||||
|
|
44
lib/gitlab/chat_commands/deploy.rb
Normal file
44
lib/gitlab/chat_commands/deploy.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
module Gitlab
|
||||
module ChatCommands
|
||||
class Deploy < BaseCommand
|
||||
def self.match(text)
|
||||
/\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text)
|
||||
end
|
||||
|
||||
def self.help_message
|
||||
'deploy <environment> to <target-environment>'
|
||||
end
|
||||
|
||||
def self.available?(project)
|
||||
project.builds_enabled?
|
||||
end
|
||||
|
||||
def self.allowed?(project, user)
|
||||
can?(user, :create_deployment, project)
|
||||
end
|
||||
|
||||
def execute(match)
|
||||
from = match[:from]
|
||||
to = match[:to]
|
||||
|
||||
actions = find_actions(from, to)
|
||||
return unless actions.present?
|
||||
|
||||
if actions.one?
|
||||
actions.first.play(current_user)
|
||||
else
|
||||
Result.new(:error, 'Too many actions defined')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_actions(from, to)
|
||||
environment = project.environments.find_by(name: from)
|
||||
return unless environment
|
||||
|
||||
environment.actions_for(to).select(&:starts_environment?)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
5
lib/gitlab/chat_commands/result.rb
Normal file
5
lib/gitlab/chat_commands/result.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
module Gitlab
|
||||
module ChatCommands
|
||||
Result = Struct.new(:type, :message)
|
||||
end
|
||||
end
|
|
@ -24,20 +24,22 @@ module Mattermost
|
|||
end
|
||||
end
|
||||
|
||||
def present(resource)
|
||||
return not_found unless resource
|
||||
def present(subject)
|
||||
return not_found unless subject
|
||||
|
||||
if resource.respond_to?(:count)
|
||||
if resource.count > 1
|
||||
return multiple_resources(resource)
|
||||
elsif resource.count == 0
|
||||
return not_found
|
||||
if subject.is_a?(Gitlab::ChatCommands::Result)
|
||||
show_result(subject)
|
||||
elsif subject.respond_to?(:count)
|
||||
if subject.many?
|
||||
multiple_resources(subject)
|
||||
elsif subject.none?
|
||||
not_found
|
||||
else
|
||||
resource = resource.first
|
||||
single_resource(subject)
|
||||
end
|
||||
else
|
||||
single_resource(subject)
|
||||
end
|
||||
|
||||
single_resource(resource)
|
||||
end
|
||||
|
||||
def access_denied
|
||||
|
@ -46,6 +48,10 @@ module Mattermost
|
|||
|
||||
private
|
||||
|
||||
def show_result(result)
|
||||
ephemeral_response(result.message)
|
||||
end
|
||||
|
||||
def not_found
|
||||
ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
|
||||
end
|
||||
|
@ -54,7 +60,7 @@ module Mattermost
|
|||
return error(resource) if resource.errors.any? || !resource.persisted?
|
||||
|
||||
message = "### #{title(resource)}"
|
||||
message << "\n\n#{resource.description}" if resource.description
|
||||
message << "\n\n#{resource.description}" if resource.try(:description)
|
||||
|
||||
in_channel_response(message)
|
||||
end
|
||||
|
@ -74,7 +80,10 @@ module Mattermost
|
|||
end
|
||||
|
||||
def title(resource)
|
||||
"[#{resource.to_reference} #{resource.title}](#{url(resource)})"
|
||||
reference = resource.try(:to_reference) || resource.try(:id)
|
||||
title = resource.try(:title) || resource.try(:name)
|
||||
|
||||
"[#{reference} #{title}](#{url(resource)})"
|
||||
end
|
||||
|
||||
def header_with_list(header, items)
|
||||
|
|
|
@ -55,6 +55,12 @@ FactoryGirl.define do
|
|||
self.when 'manual'
|
||||
end
|
||||
|
||||
trait :teardown_environment do
|
||||
options do
|
||||
{ environment: { action: 'stop' } }
|
||||
end
|
||||
end
|
||||
|
||||
trait :allowed_to_fail do
|
||||
allow_failure true
|
||||
end
|
||||
|
|
|
@ -4,9 +4,9 @@ describe Gitlab::ChatCommands::Command, service: true do
|
|||
let(:project) { create(:empty_project) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { described_class.new(project, user, params).execute }
|
||||
|
||||
describe '#execute' do
|
||||
subject { described_class.new(project, user, params).execute }
|
||||
|
||||
context 'when no command is available' do
|
||||
let(:params) { { text: 'issue show 1' } }
|
||||
let(:project) { create(:project, has_external_issue_tracker: true) }
|
||||
|
@ -51,5 +51,44 @@ describe Gitlab::ChatCommands::Command, service: true do
|
|||
expect(subject[:text]).to match(/\/issues\/\d+/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when trying to do deployment' do
|
||||
let(:params) { { text: 'deploy staging to production' } }
|
||||
let!(:build) { create(:ci_build, project: project) }
|
||||
let!(:staging) { create(:environment, name: 'staging', project: project) }
|
||||
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
|
||||
let!(:manual) do
|
||||
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
|
||||
end
|
||||
|
||||
context 'and user can not create deployment' do
|
||||
it 'returns action' do
|
||||
expect(subject[:response_type]).to be(:ephemeral)
|
||||
expect(subject[:text]).to start_with('Whoops! That action is not allowed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'and user does have deployment permission' do
|
||||
before do
|
||||
project.team << [user, :developer]
|
||||
end
|
||||
|
||||
it 'returns action' do
|
||||
expect(subject[:text]).to include(manual.name)
|
||||
expect(subject[:response_type]).to be(:in_channel)
|
||||
end
|
||||
|
||||
context 'when duplicate action exists' do
|
||||
let!(:manual2) do
|
||||
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
expect(subject[:response_type]).to be(:ephemeral)
|
||||
expect(subject[:text]).to include('Too many actions defined')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
76
spec/lib/gitlab/chat_commands/deploy_spec.rb
Normal file
76
spec/lib/gitlab/chat_commands/deploy_spec.rb
Normal file
|
@ -0,0 +1,76 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ChatCommands::Deploy, service: true do
|
||||
describe '#execute' do
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:regex_match) { described_class.match('deploy staging to production') }
|
||||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
end
|
||||
|
||||
subject do
|
||||
described_class.new(project, user).execute(regex_match)
|
||||
end
|
||||
|
||||
context 'if no environment is defined' do
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with environment' do
|
||||
let!(:staging) { create(:environment, name: 'staging', project: project) }
|
||||
let!(:build) { create(:ci_build, project: project) }
|
||||
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
|
||||
|
||||
context 'without actions' do
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with action' do
|
||||
let!(:manual1) do
|
||||
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
|
||||
end
|
||||
|
||||
it 'returns action' do
|
||||
expect(subject).to eq(manual1)
|
||||
end
|
||||
|
||||
context 'when duplicate action exists' do
|
||||
let!(:manual2) do
|
||||
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
expect(subject.message).to eq('Too many actions defined')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when teardown action exists' do
|
||||
let!(:teardown) do
|
||||
create(:ci_build, :manual, :teardown_environment,
|
||||
project: project, pipeline: build.pipeline,
|
||||
name: 'teardown', environment: 'production')
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
expect(subject).to eq(manual1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'self.match' do
|
||||
it 'matches the environment' do
|
||||
match = described_class.match('deploy staging to production')
|
||||
|
||||
expect(match[:from]).to eq('staging')
|
||||
expect(match[:to]).to eq('production')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ describe Environment, models: true do
|
|||
it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
|
||||
|
||||
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
|
||||
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
|
||||
|
@ -187,4 +188,15 @@ describe Environment, models: true do
|
|||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#actions_for' do
|
||||
let(:deployment) { create(:deployment, environment: environment) }
|
||||
let(:pipeline) { deployment.deployable.pipeline }
|
||||
let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )}
|
||||
let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
|
||||
|
||||
it 'returns a list of actions with matching environment' do
|
||||
expect(environment.actions_for('review/master')).to contain_exactly(review_action)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue