Commit ddf6889a authored by Douwe Maan's avatar Douwe Maan

Merge branch 'jej/github-project-service-for-ci' into 'master'

GitHub Service sends status updates for pipelines

Closes #3836

See merge request gitlab-org/gitlab-ee!4591
parents 1c065f39 57202a27
......@@ -3,7 +3,8 @@ class Projects::ServicesController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :service, only: [:edit, :update, :test]
before_action :ensure_service_enabled
before_action :service
respond_to :html
......@@ -23,26 +24,30 @@ class Projects::ServicesController < Projects::ApplicationController
end
def test
message = {}
if @service.can_test?
render json: service_test_response, status: :ok
else
render json: {}, status: :not_found
end
end
if @service.can_test? && @service.update_attributes(service_params[:service])
private
def service_test_response
if @service.update_attributes(service_params[:service])
data = @service.test_data(project, current_user)
outcome = @service.test(data)
unless outcome[:success]
message = { error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
if outcome[:success]
{}
else
{ error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
end
status = :ok
else
status = :not_found
{ error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
end
render json: message, status: status
end
private
def success_message
if @service.active?
"#{@service.title} activated."
......@@ -54,4 +59,8 @@ class Projects::ServicesController < Projects::ApplicationController
def service
@service ||= @project.find_or_initialize_service(params[:id])
end
def ensure_service_enabled
render_404 unless service
end
end
# To add new service you should build a class inherited from Service
# and implement a set of methods
class Service < ActiveRecord::Base
prepend EE::Service
include Sortable
include Importable
......@@ -129,6 +130,17 @@ class Service < ActiveRecord::Base
fields
end
def configurable_events
events = self.class.supported_events
# No need to disable individual triggers when there is only one
if events.count == 1
[]
else
events
end
end
def supported_events
self.class.supported_events
end
......@@ -242,8 +254,6 @@ class Service < ActiveRecord::Base
gemnasium
hipchat
irker
jenkins
jenkins_deprecated
jira
kubernetes
mattermost_slash_commands
......
......@@ -5,6 +5,9 @@
= boolean_to_icon @service.activated?
%p= @service.description
- if @service.respond_to?(:detailed_description)
%p= @service.detailed_description
.col-lg-9
= form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, subject: @service
......
......@@ -13,12 +13,12 @@
.col-sm-10
= form.check_box :active, disabled: disable_fields_service?(@service)
- if @service.supported_events.present?
- if @service.configurable_events.present?
.form-group
= form.label :url, "Trigger", class: 'control-label'
.col-sm-10
- @service.supported_events.each do |event|
- @service.configurable_events.each do |event|
%div
= form.check_box service_event_field_name(event), class: 'pull-left'
.prepend-left-20
......
......@@ -69,7 +69,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
resources :services, constraints: { id: %r{[^/]+} }, only: [:index, :edit, :update] do
resources :services, constraints: { id: %r{[^/]+} }, only: [:edit, :update] do
member do
put :test
end
......
# GitHub Project Integration
GitLab provides integration for updating pipeline statuses on GitHub. This is especially useful if using GitLab for CI/CD only.
![Pipeline status update on GitHub](img/github_status_check_pipeline_update.png)
## Configuration
### Complete these steps on GitHub
This integration requires a [GitHub API token](https://github.com/settings/tokens) with `repo:status` access granted:
1. Go to your "Personal access tokens" page at https://github.com/settings/tokens
1. Click "Generate New Token"
1. Ensure that `repo:status` is checked and click "Generate token"
1. Copy the generated token to use on GitLab
### Complete these steps on GitLab
1. Navigate to the project you want to configure.
1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
1. Click "GitHub".
1. Select the "Active" checkbox.
1. Paste the token you've generated on GitHub
1. Enter the path to your project on GitHub, such as "https://github.com/your-name/YourProject/"
1. Save or optionally click "Test Settings".
![Configure GitHub Project Integration](img/github_configuration.png)
......@@ -35,6 +35,7 @@ Click on the service links to see further configuration instructions and details
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
| Flowdock | Flowdock is a collaboration web app for technical teams |
| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
| [GitHub](github.md) | Sends pipeline notifications to GitHub |
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
......
......@@ -4,7 +4,8 @@ module EE
:jenkins_url,
:multiproject_enabled,
:pass_unstable,
:project_name
:project_name,
:repository_url
].freeze
def allowed_service_params
......
......@@ -29,6 +29,7 @@ module EE
has_one :index_status
has_one :jenkins_service
has_one :jenkins_deprecated_service
has_one :github_service
has_many :approvers, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :approver_groups, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......@@ -456,6 +457,10 @@ module EE
disabled_services.push('jenkins', 'jenkins_deprecated')
end
unless feature_available?(:github_project_service_integration)
disabled_services.push('github')
end
disabled_services
end
end
......
module EE
module Service
extend ActiveSupport::Concern
module ClassMethods
extend ::Gitlab::Utils::Override
override :available_services_names
def available_services_names
ee_service_names = %w[
github
jenkins
jenkins_deprecated
]
(super + ee_service_names).sort_by(&:downcase)
end
end
end
end
......@@ -42,6 +42,7 @@ class License < ActiveRecord::Base
extended_audit_events
file_locks
geo
github_project_service_integration
jira_dev_panel_integration
ldap_group_sync_filter
multiple_clusters
......
class GithubService < Service
include Gitlab::Routing
include ActionView::Helpers::UrlHelper
prop_accessor :token, :repository_url
validates :token, presence: true, if: :activated?
validates :repository_url, url: true, allow_blank: true
default_value_for :pipeline_events, true
def title
'GitHub'
end
def description
"See pipeline statuses on GitHub for your commits and pull requests"
end
def detailed_description
mirror_path = project_settings_repository_path(project)
mirror_link = link_to('mirroring your GitHub repository', mirror_path)
"This requires #{mirror_link} to this project.".html_safe
end
def self.to_param
'github'
end
def fields
[
{ type: 'text', name: "token", required: true, placeholder: "e.g. 8d3f016698e...", help: 'Create a <a href="https://github.com/settings/tokens">personal access token</a> with <code>repo:status</code> access granted and paste it here.'.html_safe },
{ type: 'text', name: "repository_url", title: 'Repository URL', required: true, placeholder: 'e.g. https://github.com/owner/repository' }
]
end
def self.supported_events
%w(pipeline)
end
def can_test?
project.pipelines.any?
end
def disabled_title
'Please setup a pipeline on your repository.'
end
def execute(data)
return if disabled?
status_message = StatusMessage.from_pipeline_data(project, data)
update_status(status_message)
end
def test_data(project, user)
pipeline = project.pipelines.newest_first.first
raise disabled_title unless pipeline
Gitlab::DataBuilder::Pipeline.build(pipeline)
end
def test(data)
begin
result = execute(data)
context = result[:context]
by_user = result.dig(:creator, :login)
result = "Status for #{context} updated by #{by_user}" if context && by_user
rescue StandardError => error
return { success: false, result: error }
end
{ success: true, result: result }
end
def api_url
if github_host == 'github.com'
'https://api.github.com'
else
"#{repository_uri.scheme}://#{github_host}/api/v3"
end
end
def owner
repository_uri.path.split('/')[1]
end
def repository_name
repository_uri.path.split('/')[2]
end
private
def github_host
repository_uri.host
end
def repository_uri
URI.parse(repository_url)
end
def disabled?
project.disabled_services.include?(to_param)
end
def update_status(status_message)
notifier.notify(status_message.sha,
status_message.status,
status_message.status_options)
end
def notifier
StatusNotifier.new(token, remote_repo_path, api_endpoint: api_url)
end
def remote_repo_path
"#{owner}/#{repository_name}"
end
end
class GithubService
class StatusMessage
include Gitlab::Routing
attr_reader :sha
def initialize(project, params)
@project = project
@gitlab_status = params[:status]
@detailed_status = params[:detailed_status]
@pipeline_id = params[:id]
@sha = params[:sha]
@ref_name = params[:ref]
end
def context
"ci/gitlab/#{@ref_name}".truncate(255)
end
def description
"Pipeline #{@detailed_status} on GitLab".truncate(140)
end
def target_url
project_pipeline_url(@project, @pipeline_id)
end
def status
case @gitlab_status.to_s
when 'created',
'pending',
'running',
'manual'
:pending
when 'success',
'skipped'
:success
when 'failed'
:failure
when 'canceled'
:error
end
end
def status_options
{
context: context,
description: description,
target_url: target_url
}
end
def self.from_pipeline_data(project, data)
new(project, data[:object_attributes])
end
end
end
class GithubService
class StatusNotifier
def initialize(access_token, repo_path, api_endpoint: nil)
@access_token = access_token
@repo_path = repo_path
@api_endpoint = api_endpoint.presence
end
def notify(ref, state, params = {})
client.create_status(@repo_path, ref, state, params)
end
private
def client
@client ||= Octokit::Client.new(access_token: @access_token,
api_endpoint: @api_endpoint)
end
end
end
---
title: Adds GitHub Service to send status updates for pipelines
merge_request: 4591
author:
type: added
require 'spec_helper'
describe 'User activates GitHub Service' do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
end
context 'without a license' do
it "is excluded from the integrations index" do
visit project_settings_integrations_path(project)
expect(page).not_to have_link('GitHub')
end
it 'renders 404 when trying to access service settings directly' do
visit edit_project_service_path(project, :github)
expect(page).to have_gitlab_http_status(404)
end
end
context 'with valid license' do
before do
stub_licensed_features(github_project_service_integration: true)
visit project_settings_integrations_path(project)
click_link('GitHub')
fill_in_details
end
def fill_in_details
check('Active')
fill_in "Token", with: "aaaaaaaaaa"
fill_in "Repository URL", with: 'https://github.com/h5bp/html5-boilerplate'
end
it 'activates service' do
click_button('Save')
expect(page).to have_content('GitHub activated.')
end
context 'with pipelines', :js do
let(:pipeline) { create(:ci_pipeline) }
let(:project) { create(:project, pipelines: [pipeline])}
it 'tests service before save' do
click_button 'Test settings and save changes'
wait_for_requests
expect(page).to have_content('GitHub activated.')
end
end
end
end
require 'spec_helper'
describe GithubService::StatusMessage do
include Rails.application.routes.url_helpers
let(:project) { double(:project, namespace: "me", to_s: 'example_project') }
describe '#description' do
it 'includes human readable gitlab status' do
subject = described_class.new(project, detailed_status: 'passed')
expect(subject.description).to eq "Pipeline passed on GitLab"
end
it 'gets truncated to 140 chars' do
dummy_text = 'a' * 500
subject = described_class.new(project, detailed_status: dummy_text)
expect(subject.description.length).to eq 140
end
end
describe '#status' do
using RSpec::Parameterized::TableSyntax
where(:gitlab_status, :github_status) do
'pending' | :pending
'created' | :pending
'running' | :pending
'manual' | :pending
'success' | :success
'skipped' | :success
'failed' | :failure
'canceled' | :error
end
with_them do
it 'transforms status' do
subject = described_class.new(project, status: gitlab_status)
expect(subject.status).to eq github_status
end
end
end
describe '#status_options' do
let(:subject) { described_class.new(project, id: 1) }
it 'includes context' do
expect(subject.status_options[:context]).to be_a String
end
it 'includes target_url' do
expect(subject.status_options[:target_url]).to be_a String
end
it 'includes description' do
expect(subject.status_options[:description]).to be_a String
end
end
describe '.from_pipeline_data' do
let(:pipeline) { create(:ci_pipeline) }
let(:project) { pipeline.project }
let(:sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
let(:subject) { described_class.from_pipeline_data(project, sample_data) }
it 'builds an instance of GithubService::StatusMessage' do
expect(subject).to be_a described_class
end
describe 'builds an object with' do
specify 'sha' do
expect(subject.sha).to eq pipeline.sha
end
specify 'status' do
expect(subject.status).to eq :pending
end
specify 'target_url' do
expect(subject.target_url).to end_with pipeline_path(pipeline)
end
specify 'description' do
expect(subject.description).to eq "Pipeline pending on GitLab"
end
specify 'context' do
expect(subject.context).to eq "ci/gitlab/#{pipeline.ref}"
end
context 'blocked pipeline' do
let(:pipeline) { create(:ci_pipeline, :blocked) }
it 'uses human readable status which can be used in a sentence' do
expect(subject.description). to eq 'Pipeline waiting for manual action on GitLab'
end
end
end
end
end
require 'spec_helper'
describe GithubService::StatusNotifier do
let(:access_token) { 'aaaaa' }
let(:repo_path) { 'myself/my-project' }
subject { described_class.new(access_token, repo_path) }
describe '#notify' do
let(:ref) { 'master' }
let(:state) { 'pending' }
let(:params) { { context: 'Gitlab' } }
let(:github_status_api) { "https://api.github.com/repos/#{repo_path}/statuses/#{ref}" }
it 'uses GitHub API to update status' do
stub_request(:post, github_status_api)
subject.notify(ref, state)
expect(a_request(:post, github_status_api)).to have_been_made.once
end
context 'with blank api_endpoint' do
let(:api_endpoint) { '' }
subject { described_class.new(access_token, repo_path, api_endpoint: api_endpoint) }
it 'defaults to using GitHub.com API' do
github_status_api = "https://api.github.com/repos/#{repo_path}/statuses/#{ref}"
stub_request(:post, github_status_api)
subject.notify(ref, state)
expect(a_request(:post, github_status_api)).to have_been_made.once
end
end
context 'with custom api_endpoint' do
let(:api_endpoint) { 'https://my.code.repo' }
subject { described_class.new(access_token, repo_path, api_endpoint: api_endpoint) }
it 'uses provided API for requests' do
custom_status_api = "https://my.code.repo/repos/#{repo_path}/statuses/#{ref}"
stub_request(:post, custom_status_api)
subject.notify(ref, state)
expect(a_request(:post, custom_status_api)).to have_been_made.once
end
end
it 'passes optional params' do
expect_context = hash_including(context: 'My Context')
stub_request(:post, github_status_api).with(body: expect_context)
subject.notify(ref, state, context: 'My Context')
end
it 'uses access token' do
auth_header = { 'Authorization' => 'token aaaaa' }
stub_request(:post, github_status_api).with(headers: auth_header)
subject.notify(ref, state)
end
end
end
require 'spec_helper'
describe GithubService do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:pipeline_sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
let(:owner) { 'my-user' }
let(:token) { 'aaaaaaaaa' }
let(:repository_name) { 'my-project' }
let(:base_url) { 'https://github.com' }
let(:repository_url) { "#{base_url}/#{owner}/#{repository_name}" }
let(:service_params) do
{
active: true,
project: project,
properties: {
token: token,
repository_url: repository_url
}
}
end
subject { described_class.create(service_params) }
before do
stub_licensed_features(github_project_service_integration: true)
end
describe "Associations" do
it { is_expected.to belong_to :project }
end
describe "#owner" do
it 'is determined from the repo URL' do
expect(subject.owner).to eq owner
end
end
describe "#repository_name" do
it 'is determined from the repo URL' do
expect(subject.repository_name).to eq repository_name
end
end
describe "#api_url" do
it 'uses github.com by default' do
expect(subject.api_url).to eq "https://api.github.com"
end
context "with GitHub Enterprise repo URL" do
let(:base_url) { 'https://my.code-repo.com' }
it 'is set to the Enterprise API URL' do
expect(subject.api_url).to eq "https://my.code-repo.com/api/v3"
end
end
end
describe '#detailed_description' do
it 'links to mirroring settings' do
expect(subject.detailed_description).to match(/href=.*mirroring/)
end
end
describe '#execute' do
let(:remote_repo_path) { "#{owner}/#{repository_name}" }
let(:sha) { pipeline.sha }
let(:status_options) { { context: 'security', target_url: 'https://localhost.pipeline.example.com', description: "SAST passed" } }
let(:status_message) { double(sha: sha, status: :success, status_options: status_options) }
let(:notifier) { instance_double(GithubService::StatusNotifier) }
it 'notifies GitHub of a status change' do
expect(notifier).to receive(:notify)
expect(GithubService::StatusNotifier).to receive(:new).with(token, remote_repo_path, anything)
.and_return(notifier)
subject.execute(pipeline_sample_data)
end
it 'uses StatusMessage to build message' do
allow(subject).to receive(:update_status)
expect(GithubService::StatusMessage).to receive(:from_pipeline_data).with(project, pipeline_sample_data).and_return(status_message)
subject.execute(pipeline_sample_data)
end
describe 'passes StatusMessage values to StatusNotifier' do
before do
allow(GithubService::StatusNotifier).to receive(:new).and_return(notifier)
allow(GithubService::StatusMessage).to receive(:from_pipeline_data).and_return(status_message)
end
specify 'sha' do
expect(notifier).to receive(:notify).with(sha, anything, anything)
subject.execute(pipeline_sample_data)
end
specify 'status' do
expected_status = status_message.status
expect(notifier).to receive(:notify).with(anything, expected_status, anything)
subject.execute(pipeline_sample_data)
end
specify 'context' do
expected_context = status_options[:context]
expect(notifier).to receive(:notify).with(anything, anything, hash_including(context: expected_context))
subject.execute(pipeline_sample_data)
end
specify 'target_url' do
expected_target_url = status_options[:target_url]
expect(notifier).to receive(:notify).with(anything, anything, hash_including(target_url: expected_target_url))
subject.execute(pipeline_sample_data)
end
specify 'description' do
expected_description = status_options[:description]
expect(notifier).to receive(:notify).with(anything, anything, hash_including(description: expected_description))
subject.execute(pipeline_sample_data)
end
end
it 'uses GitHub API to update status' do
github_status_api = "https://api.github.com/repos/#{owner}/#{repository_name}/statuses/#{sha}"
stub_request(:post, github_status_api)
subject.execute(pipeline_sample_data)
expect(a_request(:post, github_status_api)).to have_been_made.once
end
context 'with custom api endpoint' do
let(:api_url) { 'https://my.code.repo' }
before do
allow(subject).to receive(:api_url).and_return(api_url)
end
it 'hands custom api url to StatusNotifier' do
allow(notifier).to receive(:notify)
expect(GithubService::StatusNotifier).to receive(:new).with(anything, anything, api_endpoint: api_url)
.and_return(notifier)
subject.execute(pipeline_sample_data)
end
end
context 'without a license' do
it 'does nothing' do
stub_licensed_features(github_project_service_integration: false)
result = subject.execute(pipeline_sample_data)
expect(result).to be_nil
end
end
end
describe '#can_test?' do
it 'is false if there are no pipelines' do
project.pipelines.delete_all
expect(subject.can_test?).to eq false
end
it 'is true if the project has a pipeline' do
pipeline
expect(subject.can_test?).to eq true
end
end
describe '#test_data' do
let(:user) { project.owner }
let(:test_data) { subject.test_data(project, user) }
it 'raises error if no pipeline found' do
project.pipelines.delete_all
expect { test_data }.to raise_error 'Please setup a pipeline on your repository.'
end
it 'generates data for latest pipeline' do
pipeline
expect(test_data[:object_kind]).to eq 'pipeline'
end
end
describe '#test' do
it 'mentions creator in success message' do
dummy_response = { context: "default", creator: { login: "YourUser" } }
allow(subject).to receive(:update_status).and_return(dummy_response)
result = subject.test(pipeline_sample_data)
expect(result[:success]).to eq true
expect(result[:result].to_s).to eq('Status for default updated by YourUser')
end
it 'forwards failure message on error' do
error_response = { method: :post, status: 401, url: 'https://api.github.com/repos/my-user/my-project/statuses/master', body: 'Bad credentials' }
allow(subject).to receive(:update_status).and_raise(Octokit::Unauthorized, error_response)
result = subject.test(pipeline_sample_data)
expect(result[:success]).to eq false
expect(result[:result].to_s).to end_with('401 - Bad credentials')
end
context 'without a license' do
it 'fails gracefully' do
stub_licensed_features(github_project_service_integration: false)
result = subject.test(pipeline_sample_data)
expect(result[:success]).to eq false
end
end
end
end
......@@ -1095,7 +1095,7 @@ describe Project do
describe '#disabled_services' do
let(:namespace) { create(:group, :private) }
let(:project) { create(:project, :private, namespace: namespace) }
let(:disabled_services) { %w(jenkins jenkins_deprecated) }
let(:disabled_services) { %w(jenkins jenkins_deprecated github) }
context 'without a license key' do
before do
......@@ -1106,6 +1106,10 @@ describe Project do
end
context 'with a license key' do
before do
allow_any_instance_of(License).to receive(:plan).and_return(License::PREMIUM_PLAN)
end
context 'when checking of namespace plan is enabled' do
before do
stub_application_setting_on_object(project, should_check_namespace_plan: true)
......@@ -1116,7 +1120,7 @@ describe Project do
end
context 'and namespace has a plan' do
let(:namespace) { create(:group, :private, plan: :bronze_plan) }
let(:namespace) { create(:group, :private, plan: :silver_plan) }
it_behaves_like 'project without disabled services'
end
......
......@@ -673,6 +673,20 @@ module API
}
],
# EE-specific services
'github' => [
{
required: true,
name: :token,
type: String,
desc: 'GitHub API token with repo:status OAuth scope'
},
{
required: true,
name: :repository_url,
type: String,
desc: "GitHub repository URL"
}
],
'jenkins' => [
{
required: true,
......@@ -734,6 +748,7 @@ module API
ExternalWikiService,
FlowdockService,
GemnasiumService,
GithubService,
HipchatService,
IrkerService,
JiraService,
......
......@@ -561,6 +561,20 @@ module API
}
],
# EE-specific services
'github' => [
{
required: true,
name: :token,
type: String,
desc: 'GitHub API token with repo:status OAuth scope'
},
{
required: true,
name: :repository_name,
type: String,
desc: "GitHub repository URL"
}
],
'jenkins' => [
{
required: true,
......
......@@ -22,6 +22,7 @@ module Gitlab
sha: pipeline.sha,
before_sha: pipeline.before_sha,
status: pipeline.status,
detailed_status: pipeline.detailed_status(nil).label,
stages: pipeline.stages_names,
created_at: pipeline.created_at,
finished_at: pipeline.finished_at,
......
......@@ -23,6 +23,18 @@ describe Projects::ServicesController do
end
end
context 'when validations fail' do
let(:service_params) { { active: 'true', token: '' } }
it 'returns error messages in JSON response' do
put :test, namespace_id: project.namespace, project_id: project, id: :hipchat, service: service_params
expect(json_response['message']).to eq "Validations failed."
expect(json_response['service_response']).to eq "Token can't be blank"
expect(response).to have_gitlab_http_status(200)
end
end
context 'success' do
context 'with empty project' do
let(:project) { create(:project) }
......
......@@ -26,6 +26,7 @@ describe Gitlab::DataBuilder::Pipeline do
it { expect(attributes[:tag]).to eq(pipeline.tag) }
it { expect(attributes[:id]).to eq(pipeline.id) }
it { expect(attributes[:status]).to eq(pipeline.status) }
it { expect(attributes[:detailed_status]).to eq('passed') }
it { expect(build_data).to be_a(Hash) }
it { expect(build_data[:id]).to eq(build.id) }
......
......@@ -210,6 +210,7 @@ project:
- mattermost_slash_commands_service
- slack_slash_commands_service
- gitlab_slack_application_service
- github_service
- irker_service
- packagist_service
- pivotaltracker_service
......
......@@ -44,6 +44,7 @@ describe Project do
it { is_expected.to have_one(:bamboo_service) }
it { is_expected.to have_one(:teamcity_service) }
it { is_expected.to have_one(:jira_service) }
it { is_expected.to have_one(:github_service) }
it { is_expected.to have_one(:redmine_service) }
it { is_expected.to have_one(:custom_issue_tracker_service) }
it { is_expected.to have_one(:bugzilla_service) }
......
......@@ -26,6 +26,14 @@ Service.available_services_names.each do |service|
end
end
before do
if service == 'github'
stub_licensed_features(github_project_service_integration: true)
project.clear_memoization(:disabled_services)
project.clear_memoization(:licensed_feature_available)
end
end
def initialize_service(service)
service_item = project.find_or_initialize_service(service)
service_item.properties = service_attrs
......
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