Commit aae86bd2 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu Committed by Mayra Cabrera

Sync commits and branches to Jira Dev Panel

Adds hooks on push that checks if the project is subscribed
by Jira and sends dev info that references Jira issues
parent f33bb5fc
......@@ -113,3 +113,4 @@
- [elastic_namespace_indexer, 1]
- [export_csv, 1]
- [incident_management, 2]
- [jira_connect, 1]
......@@ -11,4 +11,12 @@ class JiraConnectInstallation < ApplicationRecord
validates :client_key, presence: true, uniqueness: true
validates :shared_secret, presence: true
validates :base_url, presence: true, public_url: true
scope :for_project, -> (project) {
distinct
.joins(:subscriptions)
.where(jira_connect_subscriptions: {
id: JiraConnectSubscription.for_project(project)
})
}
end
......@@ -8,4 +8,5 @@ class JiraConnectSubscription < ApplicationRecord
validates :namespace, presence: true, uniqueness: { scope: :jira_connect_installation_id, message: 'has already been added' }
scope :preload_namespace_route, -> { preload(namespace: :route) }
scope :for_project, -> (project) { where(namespace_id: project.namespace.self_and_ancestors) }
end
......@@ -7,6 +7,26 @@ module EE
private
override :branch_change_hooks
def branch_change_hooks
super
return unless jira_subscription_exists?
branch_to_sync = branch_name if Atlassian::JiraIssueKeyExtractor.has_keys?(branch_name)
commits_to_sync = limited_commits.select { |commit| Atlassian::JiraIssueKeyExtractor.has_keys?(commit.safe_message) }.map(&:hash)
if branch_to_sync || commits_to_sync.any?
JiraConnect::SyncBranchWorker.perform_async(project.id, branch_to_sync, commits_to_sync)
end
end
def jira_subscription_exists?
::Feature.enabled?(:jira_connect_app) &&
project.feature_available?(:jira_dev_panel_integration) &&
JiraConnectSubscription.for_project(project).exists?
end
override :pipeline_options
def pipeline_options
mirror_update = project.mirror? &&
......
......@@ -3,10 +3,30 @@
module EE
module MergeRequests
module BaseService
extend ::Gitlab::Utils::Override
override :execute_hooks
def execute_hooks(merge_request, action = 'open', old_rev: nil, old_associations: {})
super
return unless merge_request.project && jira_subscription_exists?
if Atlassian::JiraIssueKeyExtractor.has_keys?(merge_request.title, merge_request.description)
JiraConnect::SyncMergeRequestWorker.perform_async(merge_request.id)
end
end
private
attr_accessor :blocking_merge_requests_params
def jira_subscription_exists?
::Feature.enabled?(:jira_connect_app) &&
project.feature_available?(:jira_dev_panel_integration) &&
JiraConnectSubscription.for_project(project).exists?
end
override :filter_params
def filter_params(merge_request)
unless current_user.can?(:update_approvers, merge_request)
params.delete(:approvals_before_merge)
......
# frozen_string_literal: true
module JiraConnect
class SyncService
def initialize(project)
self.project = project
end
def execute(commits: nil, branches: nil, merge_requests: nil)
JiraConnectInstallation.for_project(project).each do |installation|
client = Atlassian::JiraConnect::Client.new(installation.base_url, installation.shared_secret)
response = client.store_dev_info(project: project, commits: commits, branches: branches, merge_requests: merge_requests)
log_response(response)
end
end
private
attr_accessor :project
def log_response(response)
message = {
integration: 'JiraConnect',
project_id: project.id,
project_path: project.full_path,
response: response
}
if response && response['errorMessages']
logger.error(message)
else
logger.info(message)
end
end
def logger
Gitlab::ProjectServiceLogger
end
end
end
......@@ -51,6 +51,9 @@
- incident_management:incident_management_process_alert
- jira_connect:jira_connect_sync_branch
- jira_connect:jira_connect_sync_merge_request
- admin_emails
- create_github_webhook
- elastic_batch_project_indexer
......
# frozen_string_literal: true
module JiraConnect
class SyncBranchWorker
include ApplicationWorker
queue_namespace :jira_connect
def perform(project_id, branch_name, commit_shas)
project = Project.find_by_id(project_id)
return unless project
branches = [project.repository.find_branch(branch_name)] if branch_name.present?
commits = project.commits_by(oids: commit_shas) if commit_shas.present?
JiraConnect::SyncService.new(project).execute(commits: commits, branches: branches)
end
end
end
# frozen_string_literal: true
module JiraConnect
class SyncMergeRequestWorker
include ApplicationWorker
queue_namespace :jira_connect
def perform(merge_request_id)
merge_request = MergeRequest.find_by_id(merge_request_id)
return unless merge_request && merge_request.project
JiraConnect::SyncService.new(merge_request.project).execute(merge_requests: [merge_request])
end
end
end
......@@ -4,7 +4,7 @@ module Atlassian
module JiraConnect
module Serializers
class BranchEntity < BaseEntity
expose :target, as: :id
expose :name, as: :id
expose :issueKeys do |branch|
JiraIssueKeyExtractor.new(branch.name).issue_keys
end
......
......@@ -2,6 +2,10 @@
module Atlassian
class JiraIssueKeyExtractor
def self.has_keys?(*text)
new(*text).issue_keys.any?
end
def initialize(*text)
@text = text.join(' ')
end
......
......@@ -3,6 +3,22 @@
require 'fast_spec_helper'
describe Atlassian::JiraIssueKeyExtractor do
describe '.has_keys?' do
subject { described_class.has_keys?(string) }
context 'when string contains Jira issue keys' do
let(:string) { 'Test some string TEST-01 with keys' }
it { is_expected.to eq(true) }
end
context 'when string does not contain Jira issue keys' do
let(:string) { 'string with no jira issue keys' }
it { is_expected.to eq(false) }
end
end
describe '#issue_keys' do
subject { described_class.new('TEST-01 Some A-100 issue title OTHER-02 ABC!-1 that mentions Jira issue').issue_keys }
......
......@@ -16,4 +16,30 @@ describe JiraConnectInstallation do
it { is_expected.to allow_value('https://test.atlassian.net').for(:base_url) }
it { is_expected.not_to allow_value('not/a/url').for(:base_url) }
end
describe '.for_project' do
let(:other_group) { create(:group) }
let(:parent_group) { create(:group) }
let(:group) { create(:group, parent: parent_group) }
let(:project) { create(:project, group: group) }
subject { described_class.for_project(project) }
it 'returns installations with subscriptions for project' do
sub_on_project_namespace = create(:jira_connect_subscription, namespace: group)
sub_on_ancestor_namespace = create(:jira_connect_subscription, namespace: parent_group)
# Subscription on other group that shouldn't be returned
create(:jira_connect_subscription, namespace: other_group)
expect(subject).to contain_exactly(sub_on_project_namespace.installation, sub_on_ancestor_namespace.installation)
end
it 'returns distinct installations' do
subscription = create(:jira_connect_subscription, namespace: group)
create(:jira_connect_subscription, namespace: parent_group, installation: subscription.installation)
expect(subject).to contain_exactly(subscription.installation)
end
end
end
......@@ -9,13 +9,13 @@ describe Git::BranchPushService do
let(:newrev) { sample_commit.id }
let(:ref) { 'refs/heads/master' }
subject do
described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
context 'with pull project' do
set(:project) { create(:project, :repository, :mirror) }
subject do
described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
before do
allow(project.repository).to receive(:commit).and_call_original
allow(project.repository).to receive(:commit).with("master").and_return(nil)
......@@ -109,6 +109,82 @@ describe Git::BranchPushService do
end
end
context 'Jira Connect hooks' do
set(:project) { create(:project, :repository) }
shared_examples 'enqueues Jira sync worker' do
it do
Sidekiq::Testing.fake! do
expect { subject.execute }.to change(JiraConnect::SyncBranchWorker.jobs, :size).by(1)
end
end
end
shared_examples 'does not enqueue Jira sync worker' do
it do
Sidekiq::Testing.fake! do
expect { subject.execute }.not_to change(JiraConnect::SyncBranchWorker.jobs, :size)
end
end
end
context 'when feature is enabled' do
before do
stub_feature_flags(jira_connect_app: true)
end
context 'has Jira dev panel integration license' do
before do
stub_licensed_features(jira_dev_panel_integration: true)
end
context 'with a Jira subscription' do
before do
create(:jira_connect_subscription, namespace: project.namespace)
end
context 'branch name contains Jira issue key' do
let(:ref) { 'refs/heads/branch-JIRA-123' }
it_behaves_like 'enqueues Jira sync worker'
end
context 'commit message contains Jira issue key' do
before do
allow_any_instance_of(Commit).to receive(:safe_message).and_return('Commit with key JIRA-123')
end
it_behaves_like 'enqueues Jira sync worker'
end
context 'branch name and commit message does not contain Jira issue key' do
it_behaves_like 'does not enqueue Jira sync worker'
end
end
context 'without a Jira subscription' do
it_behaves_like 'does not enqueue Jira sync worker'
end
end
context 'does not have Jira dev panel integration license' do
before do
stub_licensed_features(jira_dev_panel_integration: false)
end
it_behaves_like 'does not enqueue Jira sync worker'
end
end
context 'when feature is disabled' do
before do
stub_feature_flags(jira_connect_app: false)
end
it_behaves_like 'does not enqueue Jira sync worker'
end
end
def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
service.execute
......
......@@ -5,20 +5,88 @@ require 'spec_helper'
describe MergeRequests::BaseService do
include ProjectForksHelper
subject { MergeRequests::CreateService.new(project, project.owner, params) }
let(:project) { create(:project, :repository) }
let(:params_filtering_service) { double(:params_filtering_service) }
set(:project) { create(:project, :repository) }
let(:title) { 'Awesome merge_request' }
let(:params) do
{
title: 'Awesome merge_request',
title: title,
description: 'please fix',
source_branch: 'feature',
target_branch: 'master'
}
end
subject { MergeRequests::CreateService.new(project, project.owner, params) }
describe '#execute_hooks' do
shared_examples 'enqueues Jira sync worker' do
it do
Sidekiq::Testing.fake! do
expect { subject.execute }.to change(JiraConnect::SyncMergeRequestWorker.jobs, :size).by(1)
end
end
end
shared_examples 'does not enqueue Jira sync worker' do
it do
Sidekiq::Testing.fake! do
expect { subject.execute }.not_to change(JiraConnect::SyncMergeRequestWorker.jobs, :size)
end
end
end
context 'when feature is enabled' do
before do
stub_feature_flags(jira_connect_app: true)
end
context 'has Jira dev panel integration license' do
before do
stub_licensed_features(jira_dev_panel_integration: true)
end
context 'with a Jira subscription' do
before do
create(:jira_connect_subscription, namespace: project.namespace)
end
context 'MR contains Jira issue key' do
let(:title) { 'Awesome merge_request with issue JIRA-123' }
it_behaves_like 'enqueues Jira sync worker'
end
context 'MR does not contain Jira issue key' do
it_behaves_like 'does not enqueue Jira sync worker'
end
end
context 'without a Jira subscription' do
it_behaves_like 'does not enqueue Jira sync worker'
end
end
context 'does not have Jira dev panel integration license' do
before do
stub_licensed_features(jira_dev_panel_integration: false)
end
it_behaves_like 'does not enqueue Jira sync worker'
end
end
context 'when feature is disabled' do
before do
stub_feature_flags(jira_connect_app: false)
end
it_behaves_like 'does not enqueue Jira sync worker'
end
end
describe '#filter_params' do
let(:params_filtering_service) { double(:params_filtering_service) }
context 'filter users and groups' do
before do
allow(subject).to receive(:execute_hooks)
......
# frozen_string_literal: true
require 'spec_helper'
describe JiraConnect::SyncService do
describe '#execute' do
set(:project) { create(:project, :repository) }
let(:branches) { [project.repository.find_branch('master')] }
let(:commits) { project.commits_by(oids: %w[b83d6e3 5a62481]) }
let(:merge_requests) { [create(:merge_request, source_project: project, target_project: project)] }
subject do
described_class.new(project).execute(commits: commits, branches: branches, merge_requests: merge_requests)
end
before do
create(:jira_connect_subscription, namespace: project.namespace)
end
def expect_jira_client_call(return_value = { 'status': 'success' })
expect_any_instance_of(Atlassian::JiraConnect::Client)
.to receive(:store_dev_info).with(
project: project,
commits: commits,
branches: [instance_of(Gitlab::Git::Branch)],
merge_requests: merge_requests
).and_return(return_value)
end
def expect_log(type, message)
expect(Gitlab::ProjectServiceLogger)
.to receive(type).with(
integration: 'JiraConnect',
project_id: project.id,
project_path: project.full_path,
response: message
)
end
it 'calls Atlassian::JiraConnect::Client#store_dev_info and logs the response' do
expect_jira_client_call
expect_log(:info, { 'status': 'success' })
subject
end
context 'when request returns an error' do
it 'logs the response as an error' do
expect_jira_client_call({
'errorMessages' => ['some error message']
})
expect_log(:error, { 'errorMessages' => ['some error message'] })
subject
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe JiraConnect::SyncBranchWorker do
describe '#perform' do
set(:project) { create(:project, :repository) }
let(:project_id) { project.id }
let(:branch_name) { 'master' }
let(:commit_shas) { %w(b83d6e3 5a62481) }
subject { described_class.new.perform(project_id, branch_name, commit_shas) }
def expect_jira_sync_service_execute(args)
expect_any_instance_of(JiraConnect::SyncService)
.to receive(:execute).with(args)
end
it 'calls JiraConnect::SyncService#execute' do
expect_jira_sync_service_execute(
branches: [instance_of(Gitlab::Git::Branch)],
commits: project.commits_by(oids: commit_shas)
)
subject
end
context 'without branch name' do
let(:branch_name) { nil }
it 'calls JiraConnect::SyncService#execute' do
expect_jira_sync_service_execute(
branches: nil,
commits: project.commits_by(oids: commit_shas)
)
subject
end
end
context 'without commits' do
let(:commit_shas) { nil }
it 'calls JiraConnect::SyncService#execute' do
expect_jira_sync_service_execute(
branches: [instance_of(Gitlab::Git::Branch)],
commits: nil
)
subject
end
end
context 'when project no longer exists' do
let(:project_id) { Project.maximum(:id).to_i + 1 }
it 'does not call JiraConnect::SyncService' do
expect(JiraConnect::SyncService).not_to receive(:new)
subject
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe JiraConnect::SyncMergeRequestWorker do
describe '#perform' do
let(:merge_request) { create(:merge_request) }
let(:merge_request_id) { merge_request.id }
subject { described_class.new.perform(merge_request_id) }
it 'calls JiraConnect::SyncService#execute' do
expect_next_instance_of(JiraConnect::SyncService) do |service|
expect(service).to receive(:execute).with(merge_requests: [merge_request])
end
subject
end
context 'when MR no longer exists' do
let(:merge_request_id) { MergeRequest.maximum(:id).to_i + 1 }
it 'does not call JiraConnect::SyncService' do
expect(JiraConnect::SyncService).not_to receive(:new)
subject
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