danger-yajp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A guardfile for making Danger Plugins
4
+ # For more info see https://github.com/guard/guard#readme
5
+
6
+ # To run, use `bundle exec guard`.
7
+
8
+ guard :rspec, cmd: 'bundle exec rspec' do
9
+ require 'guard/rspec/dsl'
10
+ dsl = Guard::RSpec::Dsl.new(self)
11
+
12
+ # RSpec files
13
+ rspec = dsl.rspec
14
+ watch(rspec.spec_helper) { rspec.spec_dir }
15
+ watch(rspec.spec_support) { rspec.spec_dir }
16
+ watch(rspec.spec_files)
17
+
18
+ # Ruby files
19
+ ruby = dsl.ruby
20
+ dsl.watch_spec_files_for(ruby.lib_files)
21
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 juliendms
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ # Yet Another Jira Plugin
2
+
3
+ [![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](LICENSE)
4
+
5
+ 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.
6
+
7
+ 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.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your Gemfile:
12
+
13
+ ```rb
14
+ gem 'danger-yajp'
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ You first need to define the environment variables `DANGER_JIRA_URL`, `DANGER_JIRA_USER` and `DANGER_JIRA_PASSWORD` in your CI environment, for example:
20
+
21
+ ```
22
+ DANGER_JIRA_URL: https://jira.company.com/jira
23
+ DANGER_JIRA_USER: username
24
+ DANGER_JIRA_PASSWORD: abcd12345
25
+ ```
26
+
27
+ ### Find issues
28
+
29
+ This methode returns an array of Jira issues. All the base fields of each issue are directly accessible thanks to the gem `jira-ruby`. Input can be one project key, or an array of project keys.
30
+
31
+ ```rb
32
+ issues = jira.find_issues(
33
+ ['PROJECTKEY','MP'],
34
+ search_title: true,
35
+ search_commits: false,
36
+ search_branch: false
37
+ )
38
+
39
+ issues.each do |issue|
40
+ message issue.summary
41
+ end
42
+ ```
43
+
44
+ ### Transition / update issues
45
+
46
+ yajp allows to easily transition and update issues without the hassle of building custom json in the Dangerfile. The inputs are:
47
+
48
+ * An issue (from `jira-ruby`) or an array of issues
49
+ * For the transition action, the ID of the transition
50
+ * Any number of fields to be updated in the form: `key: value`
51
+
52
+ ```rb
53
+ jira.transition(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
54
+ ```
55
+
56
+ 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).
57
+
58
+ > Transition IDs can be found in Jira under Project Workflow > Edit Workflow in Text Mode.
59
+
60
+ ### Issue URL
61
+
62
+ Use `issue_link` to retrieve the browse URL of the Jira issue.
63
+
64
+ ```rb
65
+ message "<a href='#{jira.issue_link(issue)}'>#{issue.key} - #{issue.summary}</a>"
66
+ ```
67
+
68
+ ### API
69
+
70
+ You can always access the Jira API client from the `jira-ruby` gem via `jira.api`.
71
+
72
+ ```rb
73
+ jira.api.Project.all
74
+ ```
75
+
76
+ ### Full example
77
+
78
+ ```rb
79
+ issues = jira.find_issues('KEY')
80
+
81
+ if issues.empty?
82
+ warn 'This PR does not contain any Jira issue.'
83
+ else
84
+ issues.each do |issue|
85
+ message "<a href='#{jira.issue_link(issue)}'>#{issue.key} - #{issue.summary}</a>"
86
+
87
+ case issue.status
88
+ when 'In Progress'
89
+ jira.transition_and_update(issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
90
+ when 'To Do', 'Blocked'
91
+ 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"
92
+ end
93
+ end
94
+ end
95
+ ```
96
+
97
+ ## Development
98
+
99
+ 1. Clone this repo
100
+ 2. Run `bundle install` to setup dependencies.
101
+ 3. Run `bundle exec rake spec` to run the tests.
102
+ 4. Use `bundle exec guard` to automatically have tests run as you make changes.
103
+ 5. Make your changes.
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:specs)
8
+
9
+ task default: :specs
10
+
11
+ task :spec do
12
+ Rake::Task['specs'].invoke
13
+ Rake::Task['rubocop'].invoke
14
+ Rake::Task['spec_docs'].invoke
15
+ end
16
+
17
+ desc 'Run RuboCop on the lib/specs directory'
18
+ RuboCop::RakeTask.new(:rubocop) do |task|
19
+ task.patterns = ['lib/**/*.rb', 'spec/**/*.rb']
20
+ end
21
+
22
+ desc 'Ensure that the plugin passes `danger plugins lint`'
23
+ task :spec_docs do
24
+ sh 'bundle exec danger plugins lint'
25
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'yajp/gem_version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'danger-yajp'
9
+ spec.version = Yajp::VERSION
10
+ spec.authors = ['juliendms']
11
+ spec.email = ['j-dumas@live.fr']
12
+ spec.description = 'Synchronize your Jira issues with your PR/MR, and more.'
13
+ spec.summary = 'Yet Another Jira Plugin is a danger plugin to find issues, access their parameters and perform operations on them.'
14
+ spec.homepage = 'https://github.com/juliendms/danger-yajp'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.required_ruby_version = '>= 2.6.0'
23
+
24
+ spec.add_runtime_dependency 'danger-plugin-api'
25
+ spec.add_runtime_dependency 'jira-ruby'
26
+
27
+ # General ruby development
28
+ spec.add_development_dependency 'bundler'
29
+ spec.add_development_dependency 'rake', '~> 13.0'
30
+
31
+ # Testing support
32
+ spec.add_development_dependency 'rspec', '~> 3.9'
33
+ spec.add_development_dependency 'webmock', '~> 3.9'
34
+
35
+ # Linting code and docs
36
+ spec.add_development_dependency 'rubocop', '~> 1.0.0'
37
+ spec.add_development_dependency 'yard', '~> 0.9.11'
38
+
39
+ # Makes testing easy via `bundle exec guard`
40
+ spec.add_development_dependency 'guard', '~> 2.16'
41
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
42
+
43
+ # This gives you the chance to run a REPL inside your tests
44
+ # via:
45
+ #
46
+ # require 'pry'
47
+ # binding.pry
48
+ #
49
+ # This will stop test execution and let you inspect the results
50
+ spec.add_development_dependency 'pry'
51
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yajp/plugin'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yajp/gem_version'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yajp
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jira-ruby'
4
+
5
+ module Danger
6
+ # Yet Another Jira Plugin (in short: yajp) provides methods to easily find and manipulate issues from within the Dangerfile.
7
+ # 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.
8
+ # 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.
9
+ # Like Danger, it requires environment variables to work:
10
+ # * DANGER_JIRA_URL: the URL of the Jira server (ex: `https://jira.company.com/jira`)
11
+ # * DANGER_JIRA_USER: the Jira user that will use the Jira API
12
+ # * DANGER_JIRA_API_TOKEN: the token associated to the user (Jira Cloud) or the password of the user (Jira Server)
13
+ #
14
+ # @example Full example of a Dangerfile
15
+ # issues = jira.find_issues('KEY')
16
+ #
17
+ # if issues.empty?
18
+ # warn 'This PR does not contain any Jira issue.'
19
+ # else
20
+ # issues.each do |issue|
21
+ # message "<a href='#{jira.issue_link(issue)}'>#{issue.key} - #{issue.summary}</a>"
22
+ #
23
+ # case issue.status
24
+ # when 'In Progress'
25
+ # jira.transition_and_update(issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
26
+ # when 'To Do', 'Blocked'
27
+ # 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"
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # @example Access the Jira client of `jira-ruby` and list all Jira projects
33
+ # jira.api.Project.all
34
+ #
35
+ # @see juliendms/danger-yajp
36
+ # @tags jira, danger, gitlab, github
37
+ #
38
+ class DangerYajp < Plugin
39
+ # Give access to the Jira API via `jira-ruby` client.
40
+ #
41
+ # @return [JIRA::Client] Jira API client from `jira-ruby`
42
+ #
43
+ attr_reader :api
44
+
45
+ def initialize(dangerfile)
46
+ throw Error('The environment variable DANGER_JIRA_URL is required') if ENV['DANGER_JIRA_URL'].nil?
47
+
48
+ super(dangerfile)
49
+ url_parser = %r{(?<site>https?://[^/]+)(?<context_path>/.+)}.match(ENV['DANGER_JIRA_URL'])
50
+
51
+ options = {
52
+ username: ENV['DANGER_JIRA_USER'],
53
+ password: ENV['DANGER_JIRA_API_TOKEN'],
54
+ site: url_parser[:site],
55
+ context_path: url_parser[:context_path],
56
+ auth_type: :basic
57
+ }
58
+
59
+ @api = JIRA::Client.new(options)
60
+ end
61
+
62
+ def self.instance_name
63
+ return 'jira'
64
+ end
65
+
66
+ # Find Jira issues (keys) in the specified parameters of the PR.
67
+ #
68
+ # @example Find issues in project KEY from the name of the PR branch
69
+ # jira.find_issues('KEY', search_title: false, search_branch: true)
70
+ #
71
+ # @param [Array<String>] key An array of Jira project keys like `['KEY', 'JIRA']`, or a single `String` with a Jira project key
72
+ # @param [Boolean] search_title Option to search Jira issues from PR title, default `true`
73
+ # @param [Boolean] search_commits Option to search Jira issues from from commit messages, default `false`
74
+ # @param [Boolean] search_branch Option to search Jira issues from the name of the PR branch, default `false`
75
+ #
76
+ # @return [Array<JIRA::Issue>] An array containing all the unique issues found in the PR.
77
+ #
78
+ def find_issues(key, search_title: true, search_commits: false, search_branch: false)
79
+ regexp = build_regexp_from_key(key)
80
+ jira_issues = []
81
+
82
+ jira_issues.concat(search_title(regexp)) if search_title
83
+ jira_issues.concat(search_commits(regexp)) if search_commits
84
+ jira_issues.concat(search_branch(regexp)) if search_branch
85
+ jira_issues.concat(search_pr_body(regexp)) if jira_issues.empty?
86
+
87
+ jira_issues.uniq.map { |issue_key| @api.Issue.find(issue_key) }
88
+ end
89
+
90
+ # 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.
91
+ # 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`.
92
+ #
93
+ # @example Transition the issue `my_issue` and set the fields `assignee` and `customfield_11005` available on the transition screens
94
+ # jira.transition(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
95
+ #
96
+ # @param [Array<JIRA::Issue>] issue An array of issues, or a single `JIRA::Issue`
97
+ # @param [Integer] transition_id
98
+ # @param [Hash] fields Fields that can be updated on the transition screen
99
+ #
100
+ # @return [Boolean] `true` if all the issues were transitioned successfully, `false` otherwise.
101
+ #
102
+ def transition(issue, transition_id, **fields)
103
+ issues = issue.kind_of?(Array) ? issue : [] << issue
104
+ data = { transition: { id: transition_id.to_s } }
105
+ data[:fields] = fields unless fields.empty?
106
+ result = true
107
+
108
+ issues.each do |key|
109
+ result &= key.transitions.build.save(data)
110
+ end
111
+
112
+ return result
113
+ end
114
+
115
+ # Update the given Jira issue(s).
116
+ #
117
+ # @example Update the issue `my_issue` and set the fields `assignee` and `customfield_11005`
118
+ # jira.update(my_issue, assignee: { name: 'username' }, customfield_11005: 'example')
119
+ #
120
+ # @param [Array<JIRA::Issue>] issue An array of issue, or a single `JIRA::Issue`
121
+ # @param [Hash] fields Fields to update
122
+ #
123
+ # @return [Boolean] `true` if all the issues were updated successfully, `false` otherwise.
124
+ #
125
+ def update(issue, **fields)
126
+ return if fields.empty?
127
+
128
+ issues = issue.kind_of?(Array) ? issue : [] << issue
129
+ result = true
130
+
131
+ issues.each do |key|
132
+ result &= key.save(fields)
133
+ end
134
+ end
135
+
136
+ # 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`.
137
+ #
138
+ # @param [JIRA::Issue] issue
139
+ # @param [Integer] transition_id
140
+ # @param [Hash] fields Fields to split
141
+ #
142
+ # @return [Hash]
143
+ # * :transition_fields [Hash] A hash containing the fields available in the transition screens
144
+ # * :other_fields [Hash] A hash containing the other fields
145
+ #
146
+ def split_transition_fields(issue, transition_id, **fields)
147
+ transitions = issue.transitions.all.keep_if { |transition| transition.attrs['id'] == transition_id.to_s }
148
+ transition_fields = transitions.first.attrs['fields']
149
+ transition_data = {}
150
+
151
+ fields.each_key do |field|
152
+ transition_data[field] = fields.delete(field) if transition_fields&.key?(field.to_s)
153
+ end
154
+
155
+ { transition_fields: transition_data, other_fields: fields }
156
+ end
157
+
158
+ # Transition and update the given issues. It will use the `split_transition_fields` method to provide the right fields for the transition action,
159
+ # and use the other fields with the update action.
160
+ #
161
+ # @example Transition the issue `my_issue` and set the fields `assignee` and `customfield_11005`
162
+ # jira.transition_and_update(my_issue, 10, assignee: { name: 'username' }, customfield_11005: 'example')
163
+ #
164
+ # @param [Array<JIRA::Issue>] issue An array of issues, or a single `JIRA::Issue`
165
+ # @param [Integer] transition_id
166
+ # @param [Hash] fields Fields to update
167
+ #
168
+ # @return [Boolean] `true` if all the issues were transitioned and updated successfully, `false` otherwise.
169
+ #
170
+ def transition_and_update(issue, transition_id, **fields)
171
+ issues = issue.kind_of?(Array) ? issue : [] << issue
172
+ result = issues.first.split_transition_fields(transition_id, fields)
173
+ transition_fields = result[:transition_fields]
174
+ fields = result[:other_fields]
175
+
176
+ result = transition(issues, transition_id, **transition_fields)
177
+ result & update(issues, **fields)
178
+ end
179
+
180
+ # Get the browse URL of a Jira issue.
181
+ #
182
+ # @param [JIRA::Issue] issue
183
+ #
184
+ # @return [String] the URL of the issue
185
+ def issue_link(issue)
186
+ "#{ENV['DANGER_JIRA_URL']}/browse/#{issue.key}"
187
+ end
188
+
189
+ private
190
+
191
+ def vcs_host
192
+ return gitlab if defined? @dangerfile.gitlab
193
+
194
+ github
195
+ end
196
+
197
+ def build_regexp_from_key(key)
198
+ keys = key.kind_of?(Array) ? key.join('|') : key
199
+ return /((?:#{keys})-[0-9]+)/
200
+ end
201
+
202
+ def search_title(regexp)
203
+ jira_issues = []
204
+
205
+ vcs_host.pr_title.gsub(regexp) do |match|
206
+ jira_issues << match
207
+ end
208
+
209
+ jira_issues
210
+ end
211
+
212
+ def search_commits(regexp)
213
+ jira_issues = []
214
+
215
+ git.commits.map do |commit|
216
+ commit.message.gsub(regexp) do |match|
217
+ jira_issues << match
218
+ end
219
+ end
220
+
221
+ jira_issues
222
+ end
223
+
224
+ def search_branch(regexp)
225
+ jira_issues = []
226
+
227
+ vcs_host.branch_for_head.gsub(regexp) do |match|
228
+ jira_issues << match
229
+ end
230
+
231
+ jira_issues
232
+ end
233
+
234
+ def search_pr_body(regexp)
235
+ jira_issues = []
236
+
237
+ vcs_host.pr_body.gsub(regexp) do |match|
238
+ jira_issues << match
239
+ end
240
+
241
+ jira_issues
242
+ end
243
+ end
244
+ end