# frozen_string_literal: true require 'jira-ruby' module Danger # Yet Another Jira Plugin (in short: yajp) provides methods to easily find and manipulate issues from within the Dangerfile. # The major difference with the existing Jira plugins is the ability to transition and update issues with the same feeling as manipulating PR data from Danger. # This plugin was build in the same mind as Danger, meaning that you will find methods to easily manipulate Jira data, but no predefined warning and/or message. # Like Danger, it requires environment variables to work: # * DANGER_JIRA_URL: the URL of the Jira server (ex: `https://jira.company.com/jira`) # * DANGER_JIRA_USER: the Jira user that will use the Jira API # * DANGER_JIRA_API_TOKEN: the token associated to the user (Jira Cloud) or the password of the user (Jira Server) # # @example Full example of a Dangerfile # issues = jira.find_issues('KEY') # # if issues.empty? # warn 'This PR does not contain any Jira issue.' # else # issues.each do |issue| # message "#{issue.key} - #{issue.summary}" # # case issue.status.name # when 'In Progress' # jira.transition_and_update(issue, 10, assignee: { name: 'username' }, customfield_11005: 'example') # when 'To Do', 'Blocked' # warn "Issue #{issue.key} is not in Dev status, please make sure the issue you're working on is in the correct status" # end # end # end # # @example Access the Jira client of `jira-ruby` and list all Jira projects # jira.api.Project.all # # @see juliendms/danger-yajp # @tags jira, danger, gitlab, github # class DangerYajp < Plugin # Give access to the Jira API via `jira-ruby` client. # # @return [JIRA::Client] Jira API client from `jira-ruby` # attr_reader :api def initialize(dangerfile) throw Error('The environment variable DANGER_JIRA_URL is required') if ENV['DANGER_JIRA_URL'].nil? super(dangerfile) url_parser = %r{(?https?://[^/]+)(?/.+)}.match(ENV['DANGER_JIRA_URL']) options = { username: ENV['DANGER_JIRA_USER'], password: ENV['DANGER_JIRA_API_TOKEN'], site: url_parser[:site], context_path: url_parser[:context_path], auth_type: :basic } @api = JIRA::Client.new(options) end def self.instance_name return 'jira' end # Find Jira issues (keys) in the specified parameters of the PR. # # @example Find issues in project KEY from the name of the PR branch # jira.find_issues('KEY', search_title: false, search_branch: true) # # @param [Array] key An array of Jira project keys like `['KEY', 'JIRA']`, or a single `String` with a Jira project key # @param [Boolean] search_title Option to search Jira issues from PR title, default `true` # @param [Boolean] search_commits Option to search Jira issues from from commit messages, default `false` # @param [Boolean] search_branch Option to search Jira issues from the name of the PR branch, default `false` # # @return [Array] An array containing all the unique issues found in the PR. # def find_issues(key, search_title: true, search_commits: false, search_branch: false) regexp = build_regexp_from_key(key) jira_issues = [] jira_issues.concat(search_title(regexp)) if search_title jira_issues.concat(search_commits(regexp)) if search_commits jira_issues.concat(search_branch(regexp)) if search_branch jira_issues.concat(search_pr_body(regexp)) if jira_issues.empty? jira_issues.uniq.map { |issue_key| @api.Issue.find(issue_key) } end # Transition the given Jira issue(s) using the ID of the transition. Transition IDs can be found in Jira under Project Workflow > Edit Workflow in Text Mode. # The fields that can be updated with this method are only the fields available in the transition screen of the transition. Otherwise use `transition_and_update`. # # @example Transition the issue `my_issue` and set the fields `assignee` and `customfield_11005` available on the transition screens # jira.transition(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example') # # @param [Array] issue An array of issues, or a single `JIRA::Issue` # @param [Integer] transition_id # @param [Hash] fields Fields that can be updated on the transition screen # # @return [Boolean] `true` if all the issues were transitioned successfully, `false` otherwise. # def transition(issue, transition_id, **fields) issues = issue.kind_of?(Array) ? issue : [] << issue data = { transition: { id: transition_id.to_s } } data[:fields] = fields unless fields.empty? result = true issues.each do |key| result &= key.transitions.build.save(data) end return result end # Update the given Jira issue(s). # # @example Update the issue `my_issue` and set the fields `assignee` and `customfield_11005` # jira.update(my_issue, assignee: { name: 'username' }, customfield_11005: 'example') # # @param [Array] issue An array of issue, or a single `JIRA::Issue` # @param [Hash] fields Fields to update # # @return [Boolean] `true` if all the issues were updated successfully, `false` otherwise. # def update(issue, **fields) return if fields.empty? issues = issue.kind_of?(Array) ? issue : [] << issue result = true issues.each do |key| result &= key.save({ fields: fields }) end return result end # Utility to split the given fields into fields that can be updated on the transition screen corresponding to the `transition_id` of the given `issue`. # # @param [JIRA::Issue] issue # @param [Integer] transition_id # @param [Hash] fields Fields to split # # @return [Hash] # * :transition_fields [Hash] A hash containing the fields available in the transition screens # * :other_fields [Hash] A hash containing the other fields # def split_transition_fields(issue, transition_id, **fields) transitions = issue.transitions.all.keep_if { |transition| transition.attrs['id'] == transition_id.to_s } transition_fields = transitions.first.attrs['fields'] transition_data = {} fields.each_key do |field| transition_data[field] = fields.delete(field) if transition_fields&.key?(field.to_s) end { transition_fields: transition_data, other_fields: fields } end # Transition and update the given issues. It will use the `split_transition_fields` method to provide the right fields for the transition action, # and use the other fields with the update action. # # @example Transition the issue `my_issue` and set the fields `assignee` and `customfield_11005` # jira.transition_and_update(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example') # # @param [Array] issue An array of issues, or a single `JIRA::Issue` # @param [Integer] transition_id # @param [Hash] fields Fields to update # # @return [Boolean] `true` if all the issues were transitioned and updated successfully, `false` otherwise. # def transition_and_update(issue, transition_id, **fields) issues = issue.kind_of?(Array) ? issue : [] << issue result = issues.first.split_transition_fields(transition_id, fields) transition_fields = result[:transition_fields] fields = result[:other_fields] result = transition(issues, transition_id, **transition_fields) result & update(issues, **fields) end # Get the browse URL of a Jira issue. # # @param [JIRA::Issue] issue # # @return [String] the URL of the issue def issue_link(issue) "#{ENV['DANGER_JIRA_URL']}/browse/#{issue.key}" end private def vcs_host return gitlab if defined? @dangerfile.gitlab github end def build_regexp_from_key(key) keys = key.kind_of?(Array) ? key.join('|') : key return /((?:#{keys})-[0-9]+)/ end def search_title(regexp) jira_issues = [] vcs_host.pr_title.gsub(regexp) do |match| jira_issues << match end jira_issues end def search_commits(regexp) jira_issues = [] git.commits.map do |commit| commit.message.gsub(regexp) do |match| jira_issues << match end end jira_issues end def search_branch(regexp) jira_issues = [] vcs_host.branch_for_head.gsub(regexp) do |match| jira_issues << match end jira_issues end def search_pr_body(regexp) jira_issues = [] vcs_host.pr_body.gsub(regexp) do |match| jira_issues << match end jira_issues end end end