# frozen_string_literal: true
require_relative 'issue'
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'
# issue.transition(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
# @param [Boolean] search_commits Option to search Jira issues from from commit messages
# @param [Boolean] search_branch Option to search Jira issues from the name of the PR branch
#
# @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?
@issues = jira_issues.uniq(&:downcase).map { |issue_key| @api.Issue.find(issue_key) }
end
# Transition the given Jira issue(s) using the ID or name of the transition. Transition IDs can be found in Jira under Project Workflow > Edit Workflow in Text Mode.
# The transition name is the text that appears on the issue screen to transition it.
# 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` using the transition 'done' and set the fields `assignee` and `customfield_11005` available on the transition screens
# jira.transition_all(my_issue, 'done', assignee: { name: 'username' }, customfield_11005: 'example')
#
# @param [Integer, String] transition_id ID or name of the transition
# @param [Array, JIRA::Resource::Issue] issue An array of issues, or a single issue
# @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_all(transition_id, issue: @issues, **fields)
issues = issue.kind_of?(Array) ? issue : [] << issue
result = true
issues.each do |key|
result &= key.transition(transition_id, **fields)
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_all(my_issue, assignee: { name: 'username' }, customfield_11005: 'example')
#
# @param [Array, JIRA::Resource::Issue] issue An array of issue, or a single issue
# @param [Hash] fields Fields to update
#
# @return [Boolean] `true` if all the issues were updated successfully, `false` otherwise.
#
def update_all(issue: @issues, **fields)
return if fields.empty?
issues = issue.kind_of?(Array) ? issue : [] << issue
result = true
issues.each do |key|
result &= key.update(**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::Resource::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_all(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
#
# @param [Integer] transition_id
# @param [Array, JIRA::Resource::Issue] issue An array of issues, or a single issue
# @param [Hash] fields Fields to update
#
# @return [Boolean] `true` if all the issues were transitioned and updated successfully, `false` otherwise.
#
def transition_and_update_all(transition_id, issue: @issues, **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(transition_id, issue: issues, **transition_fields)
result & update(issue: issues, **fields)
end
# Add a remote link to the PR in the given Jira issues. It uses the link of the PR as the `globalId` of the remote link, thus avoiding to create duplicates each time the PR is updated.
#
# @param [Array, JIRA::Resource::Issue] issue An array of issues, or a single issue
# @param [] relation Option to set the relationship of the remote link
# @param [, ] status Option to set the status property of the remote link, it can be or a that will set the value of the property `resolved`
#
# @return [Boolean] `true` if all the remote links were added successfully, `false` otherwise.
#
def pr_as_remotelink(issue, relation: 'relates to', status: nil)
issues = issue.kind_of?(Array) ? issue : [] << issue
result = true
remote_link_prop = { object: { url: pr_link, title: vcs_host.pr_title, icon: link_icon } }
remote_link_prop[:globalId] = pr_link
remote_link_prop[:relationship] = relation
if status.kind_of?(Hash)
remote_link_prop[:object][:status] = status
elsif !status.nil?
remote_link_prop[:object][:status] = { resolved: status }
end
issues.each do |key|
result &= key.remotelink.build.save(remote_link_prop)
end
return result
end
private
def vcs_host
return gitlab if defined? @dangerfile.gitlab
github
end
def pr_link
return @pr_link unless @pr_link.nil?
if defined? @dangerfile.gitlab
@pr_link = vcs_host.pr_json['web_url']
else
@pr_link = vcs_host.pr_json['html_url']
end
return @pr_link
end
def link_icon
return { title: 'Gitlab', url16x16: 'https://gitlab.com/favicon.ico' } if defined? @dangerfile.gitlab
{ title: 'Github', url16x16: 'https://github.com/favicon.ico' }
end
def build_regexp_from_key(key)
keys = key.kind_of?(Array) ? key.join('|') : key
return /((?:#{keys})-[0-9]+)/i
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