diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 3245cd22e73..918a1b32612 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -19,6 +19,25 @@ class TeamcityService < CiService after_save :compose_service_hook, if: :activated? before_update :reset_password + class << self + def to_param + 'teamcity' + end + + def supported_events + %w(push merge_request) + end + + def event_description(event) + case event + when 'push', 'push_events' + 'TeamCity CI will be triggered after every push to the repository except branch delete' + when 'merge_request', 'merge_request_events' + 'TeamCity CI will be triggered after a merge request has been created or updated' + end + end + end + def compose_service_hook hook = service_hook || build_service_hook hook.save @@ -43,10 +62,6 @@ class TeamcityService < CiService 'requests build, that setting is in the vsc root advanced settings.' end - def self.to_param - 'teamcity' - end - def fields [ { type: 'text', name: 'teamcity_url', @@ -76,21 +91,14 @@ class TeamcityService < CiService def execute(data) return unless supported_events.include?(data[:object_kind]) - auth = { - username: username, - password: password - } - - branch = Gitlab::Git.ref_name(data[:ref]) - - Gitlab::HTTP.post( - build_url('httpAuth/app/rest/buildQueue'), - body: ""\ - ""\ - '', - headers: { 'Content-type' => 'application/xml' }, - basic_auth: auth - ) + case data[:object_kind] + when 'push' + branch = Gitlab::Git.ref_name(data[:ref]) + post_to_build_queue(data, branch) if push_valid?(data) + when 'merge_request' + branch = data[:object_attributes][:source_branch] + post_to_build_queue(data, branch) if merge_request_valid?(data) + end end private @@ -134,10 +142,44 @@ class TeamcityService < CiService end def get_path(path) - Gitlab::HTTP.get(build_url(path), verify: false, - basic_auth: { - username: username, - password: password - }) + Gitlab::HTTP.get(build_url(path), verify: false, basic_auth: basic_auth) + end + + def post_to_build_queue(data, branch) + Gitlab::HTTP.post( + build_url('httpAuth/app/rest/buildQueue'), + body: ""\ + ""\ + '', + headers: { 'Content-type' => 'application/xml' }, + basic_auth: basic_auth + ) + end + + def basic_auth + { username: username, password: password } + end + + def push_valid?(data) + data[:total_commits_count] > 0 && + !branch_removed?(data) && + no_open_merge_requests?(data) + end + + def merge_request_valid?(data) + data.dig(:object_attributes, :state) == 'opened' && + MergeRequest.state_machines[:merge_status].check_state?(data.dig(:object_attributes, :merge_status)) + end + + def branch_removed?(data) + Gitlab::Git.blank_ref?(data[:after]) + end + + def no_open_merge_requests?(data) + !project.merge_requests + .opened + .from_project(project) + .from_source_branches(Gitlab::Git.ref_name(data[:ref])) + .exists? end end diff --git a/changelogs/unreleased/17690-Protect-TeamCity-builds-for-triggering-when-a-branch-is-deleted-And-add-MR-option.yml b/changelogs/unreleased/17690-Protect-TeamCity-builds-for-triggering-when-a-branch-is-deleted-And-add-MR-option.yml new file mode 100644 index 00000000000..560deec6902 --- /dev/null +++ b/changelogs/unreleased/17690-Protect-TeamCity-builds-for-triggering-when-a-branch-is-deleted-And-add-MR-option.yml @@ -0,0 +1,5 @@ +--- +title: Protect TeamCity builds from triggering when a branch has been deleted. And a MR-option +merge_request: 29836 +author: Nikolay Novikov +type: fixed diff --git a/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb b/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb index 28d83a8b961..c50fd93e4cb 100644 --- a/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb +++ b/spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb @@ -15,6 +15,8 @@ describe 'User activates JetBrains TeamCity CI' do it 'activates service' do check('Active') + check('Push') + check('Merge request') fill_in('Teamcity url', with: 'http://teamcity.example.com') fill_in('Build type', with: 'GitlabTest_Build') fill_in('Username', with: 'user') diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index 1c434b25205..1edb17932e5 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -207,6 +207,91 @@ describe TeamcityService, :use_clean_rails_memory_store_caching do end end + describe '#execute' do + context 'when push' do + let(:data) do + { + object_kind: 'push', + ref: 'refs/heads/dev-123_branch', + after: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e', + total_commits_count: 1 + } + end + + it 'handles push request correctly' do + stub_post_to_build_queue(branch: 'dev-123_branch') + + expect(service.execute(data)).to include('Ok') + end + + it 'returns nil when ref is blank' do + data[:after] = "0000000000000000000000000000000000000000" + + expect(service.execute(data)).to be_nil + end + + it 'returns nil when there is no content' do + data[:total_commits_count] = 0 + + expect(service.execute(data)).to be_nil + end + end + + context 'when merge_request' do + let(:data) do + { + object_kind: 'merge_request', + ref: 'refs/heads/dev-123_branch', + after: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e', + total_commits_count: 1, + object_attributes: { + state: 'opened', + source_branch: 'dev-123_branch', + merge_status: 'unchecked' + } + } + end + + it 'handles merge request correctly' do + stub_post_to_build_queue(branch: 'dev-123_branch') + + expect(service.execute(data)).to include('Ok') + end + + it 'returns nil when merge request is not opened' do + data[:object_attributes][:state] = 'closed' + + expect(service.execute(data)).to be_nil + end + + it 'returns nil when merge request is not unchecked or cannot_be_merged_recheck' do + data[:object_attributes][:merge_status] = 'checked' + + expect(service.execute(data)).to be_nil + end + end + + it 'returns nil when event is not supported' do + data = { object_kind: 'foo' } + + expect(service.execute(data)).to be_nil + end + end + + def stub_post_to_build_queue(branch:) + teamcity_full_url = 'http://gitlab.com/teamcity/httpAuth/app/rest/buildQueue' + body ||= %Q() + auth = %w(mic password) + + stub_full_request(teamcity_full_url, method: :post).with( + basic_auth: auth, + body: body, + headers: { + 'Content-Type' => 'application/xml' + } + ).to_return(status: 200, body: 'Ok', headers: {}) + end + def stub_request(status: 200, body: nil, build_status: 'success') teamcity_full_url = 'http://gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,revision:123' auth = %w(mic password)