Skip to content

Commit

Permalink
git: merge pull request #2 from juliendms/develop
Browse files Browse the repository at this point in the history
Extend Issue class
  • Loading branch information
juliendms authored Dec 9, 2020
2 parents 02dbae2 + 755f351 commit ea5306f
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 52 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
danger-yajp (0.1.2)
danger-yajp (0.1.3)
danger-plugin-api
jira-ruby

Expand Down
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Gem](https://img.shields.io/gem/v/danger-yajp)](https://rubygems.org/gems/danger-yajp)
[![Dependencies](https://img.shields.io/librariesio/release/rubygems/danger-yajp)](https://libraries.io/rubygems/danger-yajp)

Yet Another Jira Plugin (in short: yajp) is a [Danger](https://danger.systems/ruby/) plugin that 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.
Yet Another Jira Plugin (in short: yajp) is a [Danger](https://danger.systems/ruby/) plugin that 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. It also does that by expanding the Issue class from `jira-ruby`.

Inspired by [danger-jira](https://github.com/RestlessThinker/danger-jira), from which I borrowed the issue search, and by [danger-jira_sync](https://github.com/roverdotcom/danger-jira_sync) for their usage of the awesome [jira-ruby](https://github.com/sumoheavy/jira-ruby) gem.

Expand Down Expand Up @@ -45,17 +45,23 @@ end

### Transition / update issues

yajp allows to easily transition and update issues without the hassle of building custom json in the Dangerfile. The inputs are:
yajp allows to easily transition and update issues without the hassle of building custom json in the Dangerfile. The methods are available in the issue object, or to handle multiple issues in the plugin object. The inputs are:

* An issue (from `jira-ruby`) or an array of issues
* For the transition action, the ID of the transition
* Any number of fields to be updated in the form: `key: value`
* When using the methods from the plugin object, the issues to handled, which is by default the issues found when the command `find_issues` was last run.
* Any number of fields to be updated in a hash: `key: value`

Example 1: transition all the issues found after running `find_issues`:
```rb
jira.transition(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
jira.transition_all(10, assignee: { name: 'username' }, customfield_11005: 'example')
```

The `transition` method only takes fields available in the transition screen. Use the `split_transition_fields` method to separate the fields available in the transition screen, or use the `transition_and_update` method to transition and update issues (and automatically dispatch the fields to the correct action).
Example 2: update a single issue:
```rb
issue.update(assignee: { name: 'username' }, customfield_11005: 'example')
```

The `transition` and `transition_all` methods only take fields available in the transition screen. Use the `split_transition_fields` method to separate the fields available in the transition screen, or use the `transition_and_update_all` method to transition and update issues (and automatically dispatch the fields to the correct action).

> Transition IDs can be found in Jira under Project Workflow > Edit Workflow in Text Mode.
Expand All @@ -69,10 +75,10 @@ jira.pr_as_remotelink(issue, false)

### Issue URL

Use `issue_link` to retrieve the browse URL of the Jira issue.
Use `link` to retrieve the browse URL of the Jira issue.

```rb
message "<a href='#{jira.issue_link(issue)}'>#{issue.key} - #{issue.summary}</a>"
message "<a href='#{issue.link}'>#{issue.key} - #{issue.summary}</a>"
```

### API
Expand All @@ -92,13 +98,13 @@ if issues.empty?
warn 'This PR does not contain any Jira issue.'
else
issues.each do |issue|
message "<a href='#{jira.issue_link(issue)}'>#{issue.key} - #{issue.summary}</a>"
message "<a href='#{issue.link}'>#{issue.key} - #{issue.summary}</a>"

case issue.status.name
when 'In Progress'
jira.transition_and_update(issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
jira.transition_and_update_all(10, issue: issue, assignee: { name: 'username' }, customfield_11005: 'example')
when 'To Do', 'Blocked'
warn "Issue <a href='#{jira.issue_link(issue)}'>#{issue.key}</a> is not in Dev status, please make sure the issue you're working on is in the correct status"
warn "Issue <a href='#{issue.link}'>#{issue.key}</a> is not in Dev status, please make sure the issue you're working on is in the correct status"
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/yajp/gem_version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Yajp
VERSION = '0.1.2'
VERSION = '0.1.3'
end
50 changes: 50 additions & 0 deletions lib/yajp/issue.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require 'jira-ruby'

module Danger
# This class extends (aka monkey patch) the `JIRA::Resource::Issue` class with straightforward methods to easily transition and update issues.
#
class JIRA::Resource::Issue
# Get the browse URL of the issue.
#
# @return [String] the URL of the issue
#
def link
"#{ENV['DANGER_JIRA_URL']}/browse/#{key}"
end

# Update the issue.
#
# @example Update the fields `assignee` and `customfield_11005`
# issue.update(assignee: { name: 'username' }, customfield_11005: 'example')
#
# @param [Hash] fields Fields to update
#
# @return [Boolean] `true` if the issue was updated successfully, `false` otherwise.
#
def update(**fields)
return if fields.empty?

save({ fields: fields })
end

# Transition the issue 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 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 [Integer] transition_id
# @param [Hash] fields Fields that can be updated on the transition screen
#
# @return [Boolean] `true` if the issue was transitioned successfully, `false` otherwise.
#
def transition(transition_id, **fields)
data = { transition: { id: transition_id.to_s } }
data[:fields] = fields unless fields.empty?

transitions.build.save(data)
end
end
end
72 changes: 42 additions & 30 deletions lib/yajp/plugin.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require 'jira-ruby'
require_relative 'issue'

module Danger
# Yet Another Jira Plugin (in short: yajp) provides methods to easily find and manipulate issues from within the Dangerfile.
Expand All @@ -18,13 +18,13 @@ module Danger
# warn 'This PR does not contain any Jira issue.'
# else
# issues.each do |issue|
# message "<a href='#{jira.issue_link(issue)}'>#{issue.key} - #{issue.summary}</a>"
# message "<a href='#{issue.link}'>#{issue.key} - #{issue.summary}</a>"
#
# case issue.status.name
# when 'In Progress'
# jira.transition_and_update(issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
# issue.transition(10, assignee: { name: 'username' }, customfield_11005: 'example')
# when 'To Do', 'Blocked'
# warn "Issue <a href='#{jira.issue_link(issue)}'>#{issue.key}</a> is not in Dev status, please make sure the issue you're working on is in the correct status"
# warn "Issue <a href='#{issue.link}'>#{issue.key}</a> is not in Dev status, please make sure the issue you're working on is in the correct status"
# end
# end
# end
Expand Down Expand Up @@ -73,7 +73,7 @@ def self.instance_name
# @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<JIRA::Issue>] An array containing all the unique issues found in the PR.
# @return [Array<JIRA::Resource::Issue>] 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)
Expand All @@ -84,29 +84,27 @@ def find_issues(key, search_title: true, search_commits: false, search_branch: f
jira_issues.concat(search_branch(regexp)) if search_branch
jira_issues.concat(search_pr_body(regexp)) if jira_issues.empty?

jira_issues.uniq(&:downcase).map { |issue_key| @api.Issue.find(issue_key) }
@issues = jira_issues.uniq(&:downcase).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')
# jira.transition_all(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
#
# @param [Array<JIRA::Issue>] issue An array of issues, or a single `JIRA::Issue`
# @param [Integer] transition_id
# @param [Array<JIRA::Resource::Issue>, 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(issue, transition_id, **fields)
def transition_all(transition_id, issue: @issues, **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)
result &= key.transition(transition_id, **fields)
end

return result
Expand All @@ -115,29 +113,29 @@ def transition(issue, transition_id, **fields)
# 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')
# jira.update_all(my_issue, assignee: { name: 'username' }, customfield_11005: 'example')
#
# @param [Array<JIRA::Issue>] issue An array of issue, or a single `JIRA::Issue`
# @param [Array<JIRA::Resource::Issue>, 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(issue, **fields)
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.save({ fields: fields })
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::Issue] issue
# @param [JIRA::Resource::Issue] issue
# @param [Integer] transition_id
# @param [Hash] fields Fields to split
#
Expand All @@ -161,39 +159,53 @@ def split_transition_fields(issue, transition_id, **fields)
# 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')
# jira.transition_and_update_all(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
#
# @param [Array<JIRA::Issue>] issue An array of issues, or a single `JIRA::Issue`
# @param [Integer] transition_id
# @param [Array<JIRA::Resource::Issue>, 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(issue, transition_id, **fields)
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(issues, transition_id, **transition_fields)
result & update(issues, **fields)
result = transition(transition_id, issue: issues, **transition_fields)
result & update(issue: issues, **fields)
end

# Get the browse URL of a Jira issue.
#
# @param [JIRA::Issue] issue
#
# @return [String] the URL of the issue
#
# @deprecated Please use the new #{transition_and_update_all} method
def transition_and_update(issue, transition_id, **fields)
Warning.warn('Deprecated use of the transition_and_update method, please use the new method definition')
transition_and_update_all(transition_id, issue: issue, **fields)
end

# @deprecated Please use the new #{update_all} method
def update(issue, **fields)
Warning.warn('Deprecated use of the update method, please use the new method definition')
update_all(issue: issue, **fields)
end

# @deprecated Please use the new #{transition_all} method
def transition(issue, transition_id, **fields)
Warning.warn('Deprecated use of the transition method, please use the new method definition')
transition_all(transition_id, issue: issue, **fields)
end

# @deprecated Please use the method available on the issue directly [#JIRA::Resource::Issue.link]
def issue_link(issue)
Warning.warn('Deprecated use of the issue_link method, please use the same method available in the Issue class')
"#{ENV['DANGER_JIRA_URL']}/browse/#{issue.key}"
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::Issue>] issue An array of issues, or a single `JIRA::Issue`
# @param [Array<JIRA::Resource::Issue>, JIRA::Resource::Issue] issue An array of issues, or a single issue
# @param [<String>] relation Option to set the relationship of the remote link
# @param [<Hash>] status Option to set the status property of the remote link, it can be <Hash> or a <Boolean> that will set the value of the property `resolved`
# @param [<Hash>, <Boolean>] status Option to set the status property of the remote link, it can be <Hash> or a <Boolean> that will set the value of the property `resolved`
#
# @return [Boolean] `true` if all the remote links were added successfully, `false` otherwise.
#
Expand Down
16 changes: 7 additions & 9 deletions spec/yajp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ def issue.find(key)

it 'can transition an issue' do
expected_json = '{"transition":{"id":"2"},"fields":{"assignee":{"name":"username"},"customfield_11005":"example"}}'
url = "#{ENV['DANGER_JIRA_URL']}/rest/api/2/issue/WEB-131/transitions"
issue = plugin.api.Issue.build
issue_id = Random.rand(1000)
url = "#{ENV['DANGER_JIRA_URL']}/rest/api/2/issue/#{issue_id}/transitions"
issue = plugin.api.Issue.build({ 'id' => issue_id, 'key' => 'WEB-131' })

allow(issue).to receive(:key_value).and_return('WEB-131')
stub = stub_request(:post, url).
with(body: expected_json)
result = plugin.transition(issue, 2, assignee: { name: 'username' }, customfield_11005: 'example')
result = plugin.transition_all(2, issue: issue, assignee: { name: 'username' }, customfield_11005: 'example')

expect(stub).to have_been_requested.once
expect(result).to be true
Expand All @@ -107,14 +107,12 @@ def issue.find(key)
it 'can update issues' do
expected_json = '{"fields":{"assignee":{"name":"username"},"customfield_11005":"example"}}'
uri_template = Addressable::Template.new "#{ENV['DANGER_JIRA_URL']}/rest/api/2/issue/{issue}"
issue1 = plugin.api.Issue.build
issue2 = plugin.api.Issue.build
issue1 = plugin.api.Issue.build({ 'id' => Random.rand(1000), 'self' => "#{ENV['DANGER_JIRA_URL']}/rest/api/2/issue/WEB-132", 'key' => 'WEB-132' })
issue2 = plugin.api.Issue.build({ 'id' => Random.rand(1000), 'self' => "#{ENV['DANGER_JIRA_URL']}/rest/api/2/issue/WEB-133", 'key' => 'WEB-133' })

allow(issue1).to receive(:key_value).and_return('WEB-132')
allow(issue2).to receive(:key_value).and_return('WEB-133')
stub = stub_request(:put, uri_template).
with(body: expected_json)
result = plugin.update([issue1, issue2], assignee: { name: 'username' }, customfield_11005: 'example')
result = plugin.update_all(issue: [issue1, issue2], assignee: { name: 'username' }, customfield_11005: 'example')

expect(stub).to have_been_requested.twice
expect(result).to be true
Expand Down

0 comments on commit ea5306f

Please sign in to comment.