diff --git a/Gemfile b/Gemfile index 46245ab62d1162f4aeab67e080902e2d0328bbeb..033bbe91296ee90807cef745db23bc6d68e06707 100644 --- a/Gemfile +++ b/Gemfile @@ -161,6 +161,9 @@ gem 'connection_pool', '~> 2.0' # HipChat integration gem 'hipchat', '~> 1.5.0' +# JIRA integration +gem 'jira-ruby', '~> 0.1.17' + # Flowdock integration gem 'gitlab-flowdock-git-hook', '~> 1.0.1' diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 4cb3be410645e4aa3cca8c6b0a15706175adc076..c33d7eecb9f7941ba7f8d29d9436ee1abc652480 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -18,7 +18,7 @@ module ServiceParams :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id] + :jira_issue_transition_id, :url, :project_key] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index f81b66fd21971080007043ea5eb2db82c5f9466f..580adcfba991ff36a7fd5ee647af1acdc3a29fe2 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,15 +1,35 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null +# +require 'jira' + class JiraService < IssueTrackerService include HTTParty - include Gitlab::Routing.url_helpers + include Gitlab::Application.routes.url_helpers DEFAULT_API_VERSION = 2 - prop_accessor :username, :password, :api_url, :jira_issue_transition_id, - :title, :description, :project_url, :issues_url, :new_issue_url - - validates :api_url, presence: true, url: true, if: :activated? + prop_accessor :username, :password, :url, :project_key, + :jira_issue_transition_id, :title, :description - before_validation :set_api_url, :set_jira_issue_transition_id + before_validation :set_jira_issue_transition_id before_update :reset_password @@ -20,14 +40,34 @@ class JiraService < IssueTrackerService def reset_password # don't reset the password if a new one is provided - if api_url_changed? && !password_touched? + if url_changed? && !password_touched? self.password = nil end end + def options + url = URI.parse(self.url) + { + :username => self.username, + :password => self.password, + :site => URI.join(url, '/').to_s, + :context_path => url.path, + :auth_type => :basic, + :read_timeout => 120, + :use_ssl => url.scheme == 'https' + } + end + + def client + @client ||= ::JIRA::Client.new(options) + end + + def jira_project + @jira_project ||= client.Project.find(project_key) + end + def help - 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ - 'allow a user to easily navigate to the Jira issue tracker. See the '\ + 'See the ' \ '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\ 'for details.' end @@ -53,12 +93,25 @@ class JiraService < IssueTrackerService end def fields - super.push( - { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' }, + [ + { type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' }, + { type: 'text', name: 'project_key', placeholder: 'PROJ' }, { type: 'text', name: 'username', placeholder: '' }, { type: 'password', name: 'password', placeholder: '' }, { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } - ) + ] + end + + def project_url + "#{url}/issues/?jql=project=#{project_key}" + end + + def issues_url + "#{url}/browse/:id" + end + + def new_issue_url + "#{url}/secure/CreateIssue.jspa" end def execute(push, issue = nil) @@ -72,7 +125,7 @@ class JiraService < IssueTrackerService end def create_cross_reference_note(mentioned, noteable, author) - issue_name = mentioned.id + issue_key = mentioned.id project = self.project noteable_name = noteable.class.name.underscore.downcase noteable_id = if noteable.is_a?(Commit) @@ -94,53 +147,25 @@ class JiraService < IssueTrackerService }, entity: { name: noteable_name.humanize.downcase, - url: entity_url, - title: noteable.title + url: entity_url } } - add_comment(data, issue_name) + add_comment(data, issue_key) end def test_settings - return unless api_url.present? - result = JiraService.get( - jira_api_test_url, - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Basic #{auth}" - } - ) + return unless api_utrl.present? + # Test settings by getting the project + jira_project - case result.code - when 201, 200 - Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.") - true - else - Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}") - false - end - rescue Errno::ECONNREFUSED => e - Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}." + rescue Errno::ECONNREFUSED, JIRA::HTTPError => e + Rails.logger.info "#{self.class.name} Test ERROR: #{url} - #{e.message}" false end private - def build_api_url_from_project_url - server = URI(project_url) - default_ports = [["http", 80], ["https", 443]].include?([server.scheme, server.port]) - server_url = "#{server.scheme}://#{server.host}" - server_url.concat(":#{server.port}") unless default_ports - "#{server_url}/rest/api/#{DEFAULT_API_VERSION}" - rescue - "" # looks like project URL was not valid - end - - def set_api_url - self.api_url = build_api_url_from_project_url if self.api_url.blank? - end - def set_jira_issue_transition_id self.jira_issue_transition_id ||= "2" end @@ -149,7 +174,7 @@ class JiraService < IssueTrackerService commit_id = if entity.is_a?(Commit) entity.id elsif entity.is_a?(MergeRequest) - entity.diff_head_sha + entity.last_commit.id end commit_url = build_entity_url(:commit, commit_id) @@ -161,69 +186,47 @@ class JiraService < IssueTrackerService end def transition_issue(issue) - message = { - transition: { - id: jira_issue_transition_id - } - } - send_message(close_issue_url(issue.iid), message.to_json) + issue = client.Issue.find(issue.iid) + issue.transitions.build.save(transition: { id: jira_issue_transition_id }) end def add_issue_solved_comment(issue, commit_id, commit_url) - comment = { - body: "Issue solved with [#{commit_id}|#{commit_url}]." - } - - send_message(comment_url(issue.iid), comment.to_json) + comment = "Issue solved with [#{commit_id}|#{commit_url}]." + send_message(issue.iid, comment) end - def add_comment(data, issue_name) - url = comment_url(issue_name) + def add_comment(data, issue_key) user_name = data[:user][:name] user_url = data[:user][:url] entity_name = data[:entity][:name] entity_url = data[:entity][:url] - entity_title = data[:entity][:title] project_name = data[:project][:name] - message = { - body: %Q{[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'} - } - - unless existing_comment?(issue_name, message[:body]) - send_message(url, message.to_json) - end - end + message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]." - def auth - require 'base64' - Base64.urlsafe_encode64("#{self.username}:#{self.password}") + # unless existing_comment?(issue_name, message[:body]) + send_message(issue_key, message) + # end end - def send_message(url, message) + def send_message(issue_key, message) return unless api_url.present? - result = JiraService.post( - url, - body: message, - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Basic #{auth}" - } - ) - - message = case result.code - when 201, 200, 204 - "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}." - when 401 - "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again." - else - "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}" - end + issue = client.Issue.find(issue_key) + issue.comments.build.save!(body: message) + + # message = case result.code + # when 201, 200, 204 + # "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}." + # when 401 + # "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again." + # else + # "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}" + # end Rails.logger.info(message) message rescue URI::InvalidURIError, Errno::ECONNREFUSED => e - Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}." + Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}" end def existing_comment?(issue_name, new_comment) @@ -267,16 +270,4 @@ class JiraService < IssueTrackerService ) ) end - - def close_issue_url(issue_name) - "#{self.api_url}/issue/#{issue_name}/transitions" - end - - def comment_url(issue_name) - "#{self.api_url}/issue/#{issue_name}/comment" - end - - def jira_api_test_url - "#{self.api_url}/myself" - end end diff --git a/db/migrate/20160122231710_migrate_jira_to_gem.rb b/db/migrate/20160122231710_migrate_jira_to_gem.rb new file mode 100644 index 0000000000000000000000000000000000000000..972aea323d95729119ccdcd1c5fa86e649fe523c --- /dev/null +++ b/db/migrate/20160122231710_migrate_jira_to_gem.rb @@ -0,0 +1,52 @@ +class MigrateJiraToGem < ActiveRecord::Migration + def change + reversible do |dir| + select_all("SELECT id, properties FROM services WHERE services.type IN ('JiraService')").each do |service| + id = service['id'] + properties = JSON.parse(service['properties']) + properties_was = properties.clone + + dir.up do + # Migrate `project_url` to `project_key` + # Ignore if `project_url` doesn't have jql project query with project key + if properties['project_url'].present? + jql = properties['project_url'].match('project=([A-Za-z]*)') + properties['project_key'] = jql.captures.first if jql + end + + # Migrate `api_url` to `url` + if properties['api_url'].present? + url = properties['api_url'].match('(.*)\/rest\/api') + properties['url'] = url.captures.first if url + end + + # Delete now unnecessary properties + properties.delete('api_url') + properties.delete('project_url') + properties.delete('new_issue_url') + properties.delete('issues_url') + end + + dir.down do + # Rebuild old properties based on sane defaults + if properties['url'].present? + properties['api_url'] = "#{properties['url']}/rest/api/2" + properties['project_url'] = + "#{properties['url']}/issues/?jql=project=#{properties['project_key']}" + properties['issues_url'] = "#{properties['url']}/browse/:id" + properties['new_issue_url'] = "#{properties['url']}/secure/CreateIssue.jspa" + end + + # Delete the new properties + properties.delete('url') + properties.delete('project_key') + end + + # Update changes properties + if properties != properties_was + execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}") + end + end + end + end +end diff --git a/doc/integration/README.md b/doc/integration/README.md index c2fd299db07a1240770433265f83ba42f2e73cca..0fc0be4ffceddc7eaea05a483a798d7b1df47e52 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,57 +5,36 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker +- [JIRA](jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS +- [Slack](slack.md) Integrate with the Slack chat service - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users -- [Akismet](akismet.md) Configure Akismet to stop spam -- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. -[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html - - ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a [Project Service][]. +You can find these within GitLab in the Services page under Project Settings if +you are at least a master on the project. +Project Services are a bit like plugins in that they allow a lot of freedom in +adding functionality to GitLab. For example there is also a service that can +send an email every time someone pushes new commits. -[Project Service]: ../project_services/project_services.md - -## SSL certificate errors - -When trying to integrate GitLab with services that are using self-signed certificates, -it is very likely that SSL certificate errors will occur on different parts of the -application, most likely Sidekiq. There are 2 approaches you can take to solve this: - -1. Add the root certificate to the trusted chain of the OS. -1. If using Omnibus, you can add the certificate to GitLab's trusted certificates. - -**OS main trusted chain** +Because GitLab is open source we can ship with the code and tests for all +plugins. This allows the community to keep the plugins up to date so that they +always work in newer GitLab versions. -This [resource](http://kb.kerio.com/product/kerio-connect/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html) -has all the information you need to add a certificate to the main trusted chain. +For an overview of what projects services are available without logging in, +please see the [project_services directory][projects-code]. -This [answer](http://superuser.com/questions/437330/how-do-you-add-a-certificate-authority-ca-to-ubuntu) -at SuperUser also has relevant information. - -**Omnibus Trusted Chain** - -It is enough to concatenate the certificate to the main trusted certificate: - -```bash -cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem -``` - -After that restart GitLab with: - -```bash -sudo gitlab-ctl restart -``` +[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html +[Project Service]: ../project_services/project_services.md +[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png new file mode 100644 index 0000000000000000000000000000000000000000..0cc160bebe2fda31497afd077b7cd0f5b0930dec Binary files /dev/null and b/doc/integration/img/jira_service_page.png differ diff --git a/doc/integration/jira.md b/doc/integration/jira.md index 78aa66341161d9f9966355477460ecee98becfd5..de373bd94ff370f738a65d0283c7900e4e3029ad 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,3 +1,146 @@ -# GitLab JIRA integration +# GitLab Jira integration -This document was moved under [project_services/jira](../project_services/jira.md). +GitLab can be configured to interact with Jira. Configuration happens via +username and password. Connecting to a Jira server via CAS is not possible. + +Each project can be configured to connect to a different Jira instance, see the +[configuration](#configuration) section. If you have one Jira instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. + +Once the project is connected to Jira, you can reference and close the issues +in Jira directly from GitLab. + +## Table of Contents + +* [Referencing Jira Issues from GitLab](#referencing-jira-issues) +* [Closing Jira Issues from GitLab](#closing-jira-issues) +* [Configuration](#configuration) + +### Referencing Jira Issues + +When GitLab project has Jira issue tracker configured and enabled, mentioning +Jira issue in GitLab will automatically add a comment in Jira issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, eg. `PROJECT-7`, will add a comment in Jira issue in the +format: + +``` + USER mentioned this issue in LINK_TO_THE_MENTION +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_MENTION` Link to the origin of mention with a name of the entity where Jira issue was mentioned. +Can be commit or merge request. + + + +--- + +### Closing Jira Issues + +Jira issues can be closed directly from GitLab by using trigger words, eg. +`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and +merge requests. When a commit which contains the trigger word in the commit +message is pushed, GitLab will add a comment in the mentioned Jira issue. + +For example, for project named `PROJECT` in Jira, we implemented a new feature +and created a merge request in GitLab. + +This feature was requested in Jira issue `PROJECT-7`. Merge request in GitLab +contains the improvement and in merge request description we say that this +merge request `Closes PROJECT-7` issue. + +Once this merge request is merged, the Jira issue will be automatically closed +with a link to the commit that resolved the issue. + + + +--- + + + +--- + +## Configuration + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `jira-developers` +group. + +**It is important that the user `gitlab` has write-access to projects in JIRA** + +### Configuring GitLab + +JIRA configuration in GitLab is done via a project's **Services**. + +#### GitLab 7.8 and up with JIRA v6.x + +See next section. + +#### GitLab 7.8 and up + +_The currently supported JIRA versions are v6.x and v7.x._ + +To enable JIRA integration in a project, navigate to the project's +**Settings > Services > JIRA**. + +Fill in the required details on the page as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://jira.example.com | +| `Project key` | The short, all capital letter identifier for your JIRA project. | +| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | +| `Jira issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). By default, this ID is `2` (in the example image, this is `2` as well) | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + + + +--- + +#### GitLab 6.x-7.7 with JIRA v6.x + +_**Note:** GitLab versions 7.8 and up contain various integration improvements. +We strongly recommend upgrading._ + +In `gitlab.yml` enable the JIRA issue tracker section by +[uncommenting these lines][jira-gitlab-yml]. This will make sure that all +issues within GitLab are pointing to the JIRA issue tracker. + +After you set this, you will be able to close issues in JIRA by a commit in +GitLab. + +Go to your project's **Settings** page and fill in the project name for the +JIRA project: + + + +--- + +You can also enable the JIRA service that will allow you to interact with JIRA +issues. Go to the **Settings > Services > JIRA** and: + +1. Tick the active check box to enable the service +1. Supply the URL to JIRA server, for example http://jira.example.com +1. Supply the username of a user we created under `Configuring JIRA` section, + for example `gitlab` +1. Supply the password of the user +1. Optional: supply the JIRA API version, default is version `2` +1. Optional: supply the JIRA issue transition ID (issue transition to closed). + This is dependent on JIRA settings, default is `2` +1. Hit save + + + + +[services-templates]: ../project_services/services_templates.md +[jira-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115