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:
Rémy Coutable 2016-11-21 20:27:58 +00:00
commit 2449d787ad
10 changed files with 219 additions and 15 deletions

View file

@ -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

View file

@ -0,0 +1,4 @@
---
title: Add deployment command to ChatOps
merge_request: 7619
author:

View file

@ -4,6 +4,7 @@ module Gitlab
COMMANDS = [
Gitlab::ChatCommands::IssueShow,
Gitlab::ChatCommands::IssueCreate,
Gitlab::ChatCommands::Deploy,
].freeze
def execute

View 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

View file

@ -0,0 +1,5 @@
module Gitlab
module ChatCommands
Result = Struct.new(:type, :message)
end
end

View file

@ -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)

View file

@ -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

View file

@ -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

View 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

View file

@ -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