fastlane-plugin-jira_issues_release_notes 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,173 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/jira_issues_release_notes_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+
7
+ module SharedValues
8
+ FL_JIRA_LAST_TAG ||= :S3_APK_OUTPUT_PATH
9
+ FL_JIRA_LAST_TAG_HASH ||= :FL_JIRA_LAST_TAG_HASH
10
+ FL_JIRA_COMMITS_FROM_HASH ||= :FL_JIRA_COMMITS_FROM_HASH
11
+ FL_JIRA_LAST_KEYS_FROM_COMMITS ||= :FL_JIRA_LAST_KEYS_FROM_COMMITS
12
+ FL_JIRA_LAST_ISSUES_FROM_COMMITS ||= :FL_JIRA_LAST_ISSUES_FROM_COMMITS
13
+ end
14
+
15
+ class JiraReleaseValidationAction < Action
16
+ def self.run(params)
17
+ Helper::JiraIssuesReleaseNotesHelper.initialize_jira(
18
+ host: params[:host],
19
+ username: params[:username],
20
+ password: params[:password],
21
+ context_path: params[:context_path],
22
+ api_version: params[:api_version],
23
+ disable_ssl_verification: params[:disable_ssl_verification]
24
+ )
25
+
26
+ @format = params[:format]
27
+ @format_line_break = @format === 'html' ? '<br />' : "\n"
28
+
29
+ issue_key_regex = Regexp.new("(#{params[:ticket_prefix]}-\\d+)")
30
+
31
+ issues = Helper::JiraIssuesReleaseNotesHelper.get_issues_from_commit_after_latest_tag(
32
+ tag_regex: params[:tag_prefix],
33
+ tag_version_match: params[:tag_version_match],
34
+ issue_key_regex: issue_key_regex,
35
+ debug: params[:debug]
36
+ )
37
+
38
+ return '' unless issues
39
+
40
+ grouped_issues = {
41
+ "Tasks to validate" => issues.select { |issue| params[:to_validate_status].include?(issue.status.name) },
42
+ "Validated tasks" => issues.select { |issue| params[:validated_status].include?(issue.status.name) }
43
+ }
44
+
45
+ Helper::JiraIssuesReleaseNotesHelper.generate_changelog(
46
+ groups: grouped_issues,
47
+ line_break_format: @format_line_break
48
+ )
49
+ end
50
+
51
+ def self.description
52
+ "It generates a release note based on the issues keys and descriptions found in the commits"
53
+ end
54
+
55
+ def self.authors
56
+ ["Erick Martins"]
57
+ end
58
+
59
+ def self.return_value
60
+ # If your method provides a return value, you can describe here what it does
61
+ end
62
+
63
+ def self.details
64
+ # Optional:
65
+ "It generates a release note based on the issues keys and descriptions found in the commits"
66
+ end
67
+
68
+ def self.available_options
69
+ [
70
+ FastlaneCore::ConfigItem.new(
71
+ key: :tag_prefix,
72
+ description: "Match parameter of git describe. See man page of git describe for more info",
73
+ verify_block: proc do |value|
74
+ UI.user_error!("No match for analyze_commits action given, pass using `match: 'expr'`") unless value && !value.empty?
75
+ end
76
+ ),
77
+ FastlaneCore::ConfigItem.new(
78
+ key: :ticket_prefix,
79
+ env_name: 'FL_FIND_TICKETS_MATCHING',
80
+ description: 'regex to extract ticket numbers',
81
+ default_value: '[A-Z]+',
82
+ optional: true
83
+ ),
84
+ FastlaneCore::ConfigItem.new(
85
+ key: :tag_version_match,
86
+ description: "To parse version number from tag name",
87
+ default_value: '\d+\.\d+\.\d+'
88
+ ),
89
+ FastlaneCore::ConfigItem.new(
90
+ key: :validated_status,
91
+ env_name: 'FL_JIRA_VALIDATED_STATUS',
92
+ description: 'List of jira issues status already validated',
93
+ optional: false,
94
+ type: Array
95
+ ),
96
+ FastlaneCore::ConfigItem.new(
97
+ key: :to_validate_status,
98
+ env_name: 'FL_JIRA_TO_VALIDATE_STATUS',
99
+ description: 'List of jira issues status to be validated',
100
+ optional: false,
101
+ type: Array
102
+ ),
103
+ FastlaneCore::ConfigItem.new(
104
+ key: :format,
105
+ description: "You can use either markdown, slack, html or plain",
106
+ default_value: "markdown",
107
+ optional: true,
108
+ verify_block: proc do |value|
109
+ UI.user_error!("Invalid format! You can use either markdown, slack, html or plain") unless ['markdown', 'html', 'slack', 'plain'].include?(value)
110
+ end
111
+ ),
112
+
113
+ # Jira Client options
114
+ FastlaneCore::ConfigItem.new(
115
+ key: :username,
116
+ env_name: 'FL_JIRA_USERNAME',
117
+ description: 'Jira user',
118
+ optional: false
119
+ ),
120
+ FastlaneCore::ConfigItem.new(
121
+ key: :password,
122
+ env_name: 'FL_JIRA_PASSWORD',
123
+ description: 'Jira user password',
124
+ optional: false
125
+ ),
126
+ FastlaneCore::ConfigItem.new(
127
+ key: :host,
128
+ env_name: 'FL_JIRA_HOST',
129
+ description: 'Jira location',
130
+ optional: false
131
+ ),
132
+ FastlaneCore::ConfigItem.new(
133
+ key: :api_version,
134
+ env_name: 'FL_JIRA_API_VERSION',
135
+ description: 'Jira api version',
136
+ default_value: '2',
137
+ optional: true,
138
+ ),
139
+ FastlaneCore::ConfigItem.new(
140
+ key: :context_path,
141
+ env_name: 'FL_JIRA_CONTEXT_PATH',
142
+ description: 'Jira context path',
143
+ optional: true,
144
+ default_value: ''
145
+ ),
146
+ FastlaneCore::ConfigItem.new(
147
+ key: :disable_ssl_verification,
148
+ env_name: 'FL_JIRA_DISABLE_SSL_VERIFICATION',
149
+ description: 'Jira SSL Verification mode',
150
+ optional: true,
151
+ default_value: false,
152
+ type: Boolean
153
+ ),
154
+ FastlaneCore::ConfigItem.new(
155
+ key: :debug,
156
+ description: "True if you want to log out a debug info",
157
+ default_value: false,
158
+ type: Boolean,
159
+ optional: true
160
+ )
161
+ ]
162
+ end
163
+
164
+ def self.is_supported?(platform)
165
+ # Adjust this if your plugin only works for a particular platform (iOS vs. Android, for example)
166
+ # See: https://docs.fastlane.tools/advanced/#control-configuration-by-lane-and-by-platform
167
+ #
168
+ # [:ios, :mac, :android].include?(platform)
169
+ true
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,162 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/jira_issues_release_notes_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ module SharedValues
7
+ FL_JIRA_LAST_TAG ||= :S3_APK_OUTPUT_PATH
8
+ FL_JIRA_LAST_TAG_HASH ||= :FL_JIRA_LAST_TAG_HASH
9
+ FL_JIRA_COMMITS_FROM_HASH ||= :FL_JIRA_COMMITS_FROM_HASH
10
+ FL_JIRA_LAST_KEYS_FROM_COMMITS ||= :FL_JIRA_LAST_KEYS_FROM_COMMITS
11
+ FL_JIRA_LAST_ISSUES_FROM_COMMITS ||= :FL_JIRA_LAST_ISSUES_FROM_COMMITS
12
+ end
13
+
14
+ class JiraVersionsAction < Action
15
+ def self.run(params)
16
+ @jira_helper = Helper::JiraIssuesReleaseNotesHelper.initialize_jira(
17
+ host: params[:host],
18
+ api_version: params[:api_version],
19
+ username: params[:username],
20
+ password: params[:password],
21
+ context_path: params[:context_path],
22
+ disable_ssl_verification: params[:disable_ssl_verification]
23
+ )
24
+
25
+ versions = @jira_helper.list_versions(
26
+ project_id_or_key: params[:project_id],
27
+ query: params[:query],
28
+ order_by: params[:order_by],
29
+ status: params[:status]
30
+ )
31
+
32
+ versions.map { |version|
33
+ {
34
+ :id => version.self,
35
+ :name => version.name,
36
+ :archived => version.archived,
37
+ :released => version.released,
38
+ :user_release_date => defined? version.userReleaseDate ? version.userReleaseDate : nil,
39
+ :overdue => defined? version.overdue ? version.overdue : false,
40
+ :projectId => version.projectId,
41
+ }
42
+ }
43
+ end
44
+
45
+ def self.description
46
+ "It generates a release note based on the issues keys found in branch name and descriptions found in the commits"
47
+ end
48
+
49
+ def self.authors
50
+ ["Erick Martins"]
51
+ end
52
+
53
+ def self.return_value
54
+ "boolean value"
55
+ end
56
+
57
+ def self.details
58
+ # Optional:
59
+ "It generates a release note based on the issues keys found in branch name and descriptions found in the commits"
60
+ end
61
+
62
+ def self.available_options
63
+ conflict_extraction_method = Proc.new do |other|
64
+ UI.user_error! "Unexpected conflict with option #{other}" unless [:extract_from_branch, :tag_prefix].include?(other)
65
+ end
66
+
67
+ conflict_comment = Proc.new do |other|
68
+ UI.user_error! "Unexpected conflict with option #{other}" unless [:comment_block, :comment].include?(other)
69
+ end
70
+
71
+ [
72
+ FastlaneCore::ConfigItem.new(
73
+ key: :project_id,
74
+ env_name: 'FL_JIRA_PROJECT_ID',
75
+ description: 'The project ID or project key',
76
+ optional: false,
77
+ ),
78
+ FastlaneCore::ConfigItem.new(
79
+ key: :query,
80
+ description: 'Filter the results using a literal string. Versions with matching name or description are returned (case insensitive).',
81
+ optional: true,
82
+ ),
83
+ FastlaneCore::ConfigItem.new(
84
+ key: :order_by,
85
+ description: 'Order the results by a field. Valid values: description, -description, +description, name, -name, +name, releaseDate, -releaseDate, +releaseDate, sequence, -sequence, +sequence, startDate, -startDate, +startDate',
86
+ optional: true,
87
+ verify_block: proc do |value|
88
+ valid_order = ["description", "-description", "+description", "name", "-name", "+name", "releaseDate", "-releaseDate", "+releaseDate", "sequence", "-sequence", "+sequence", "startDate", "-startDate", "+startDate"]
89
+ UI.user_error!("Invalid :order_by value! Valid values: #{valid_order.join(", ")}") unless valid_order.include?(value)
90
+ end
91
+ ),
92
+ FastlaneCore::ConfigItem.new(
93
+ key: :status,
94
+ description: 'A list of status values used to filter the results by version status. This parameter accepts a comma-separated list. The status values are released, unreleased, and archived',
95
+ optional: true,
96
+ verify_block: proc do |value|
97
+ valid_values = ["released", "unreleased", "archived"]
98
+ UI.user_error!("Invalid :status value! Valid values: #{valid_values.join(", ")}") unless valid_values.include?(value)
99
+ end
100
+ ),
101
+
102
+ # Jira Client options
103
+ FastlaneCore::ConfigItem.new(
104
+ key: :username,
105
+ env_name: 'FL_JIRA_USERNAME',
106
+ description: 'Jira user',
107
+ optional: false
108
+ ),
109
+ FastlaneCore::ConfigItem.new(
110
+ key: :password,
111
+ env_name: 'FL_JIRA_PASSWORD',
112
+ description: 'Jira user',
113
+ optional: false
114
+ ),
115
+ FastlaneCore::ConfigItem.new(
116
+ key: :host,
117
+ env_name: 'FL_JIRA_HOST',
118
+ description: 'Jira location',
119
+ optional: false
120
+ ),
121
+ FastlaneCore::ConfigItem.new(
122
+ key: :api_version,
123
+ env_name: 'FL_JIRA_API_VERSION',
124
+ description: 'Jira api version',
125
+ default_value: '3',
126
+ optional: true,
127
+ ),
128
+ FastlaneCore::ConfigItem.new(
129
+ key: :context_path,
130
+ env_name: 'FL_JIRA_CONTEXT_PATH',
131
+ description: 'Jira context path',
132
+ optional: true,
133
+ default_value: ''
134
+ ),
135
+ FastlaneCore::ConfigItem.new(
136
+ key: :disable_ssl_verification,
137
+ env_name: 'FL_JIRA_DISABLE_SSL_VERIFICATION',
138
+ description: 'Jira SSL Verification mode',
139
+ optional: true,
140
+ default_value: false,
141
+ type: Boolean
142
+ ),
143
+ FastlaneCore::ConfigItem.new(
144
+ key: :debug,
145
+ description: "True if you want to log out a debug info",
146
+ default_value: false,
147
+ type: Boolean,
148
+ optional: true
149
+ )
150
+ ]
151
+ end
152
+
153
+ def self.is_supported?(platform)
154
+ # Adjust this if your plugin only works for a particular platform (iOS vs. Android, for example)
155
+ # See: https://docs.fastlane.tools/advanced/#control-configuration-by-lane-and-by-platform
156
+ #
157
+ # [:ios, :mac, :android].include?(platform)
158
+ true
159
+ end
160
+ end
161
+ end
162
+ end
@@ -1,29 +1,213 @@
1
1
  require 'fastlane_core/ui/ui'
2
- require 'jira-ruby'
2
+ require 'zk-jira-ruby'
3
3
 
4
4
  module Fastlane
5
5
  UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
6
6
 
7
7
  module Helper
8
8
  class JiraIssuesReleaseNotesHelper
9
- # class methods that you define here become available in your action
10
- # as `Helper::JiraIssuesReleaseNotesHelper.your_method`
11
- #
12
- def self.show_message
13
- UI.message("Hello from the jira_issues_release_notes plugin helper!")
9
+
10
+ def self.format_issue_link(issue:)
11
+ # formats the link according to the output format we need
12
+ link = @jira_helper.url(issue: issue)
13
+ case @format
14
+ when "slack"
15
+ "*<#{link}|#{issue.key}>*: #{issue.summary}"
16
+ when "markdown"
17
+ "- **[#{issue.key}](#{link})**: #{issue.summary}"
18
+ when "html"
19
+ "&nbsp;&nbsp;- <strong><a href=\"#{link}\" target=\"_blank\">#{issue.key}</a><strong>: #{issue.summary}"
20
+ else
21
+ "- #{issue.key}: #{issue.summary} (#{link})"
22
+ end
23
+ end
24
+
25
+ def self.style_text(text:, style:)
26
+ # formats the text according to the style we're looking to use
27
+
28
+ # Skips all styling
29
+ case style
30
+ when "title"
31
+ case @format
32
+ when "markdown"
33
+ "# #{text}"
34
+ when "slack"
35
+ "*#{text}*"
36
+ when "html"
37
+ "<h1>#{text}</h1>"
38
+ else
39
+ text
40
+ end
41
+ when "heading"
42
+ case @format
43
+ when "markdown"
44
+ "### #{text}"
45
+ when "slack"
46
+ "*#{text}*"
47
+ when "html"
48
+ "<h3>#{text}</h3>"
49
+ else
50
+ "#{text}:"
51
+ end
52
+ when "bold"
53
+ case @format
54
+ when "markdown"
55
+ "**#{text}**"
56
+ when "slack"
57
+ "*#{text}*"
58
+ when "html"
59
+ "<strong>#{text}</strong>"
60
+ else
61
+ text
62
+ end
63
+ else
64
+ text # catchall, shouldn't be needed
65
+ end
66
+ end
67
+
68
+ def self.get_commit_after_latest_tag(tag_regex:, tag_version_match:, debug:)
69
+ unless Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_COMMITS_FROM_HASH] then
70
+
71
+ # Try to find the tag
72
+ command = "git describe --tags --match=#{tag_regex}"
73
+ tag = Actions.sh(command, log: debug)
74
+
75
+ if tag.empty?
76
+ UI.message("First commit of the branch is taken as a begining of next release")
77
+ # If there is no tag found we taking the first commit of current branch
78
+ hash = Actions.sh('git rev-list --max-parents=0 HEAD', log: debug).chomp
79
+ else
80
+ # Tag's format is v2.3.4-5-g7685948
81
+ # Get a hash of last version tag
82
+ tag_name = tag.split('-')[0...-2].join('-').strip
83
+ parsed_version = tag_name.match(tag_version_match)
84
+
85
+ if parsed_version.nil?
86
+ UI.user_error!("Error while parsing version from tag #{tag_name} by using tag_version_match - #{tag_version_match}. Please check if the tag contains version as you expect and if you are using single brackets for tag_version_match parameter.")
87
+ end
88
+
89
+ version = parsed_version[0]
90
+
91
+ # Get a hash of last version tag
92
+ command = "git rev-list -n 1 refs/tags/#{tag_name}"
93
+ hash = Actions.sh(command, log: debug).chomp
94
+
95
+ UI.message("Found a tag #{tag_name} associated with version #{version}")
96
+ end
97
+
98
+ # Get commits log between last version and head
99
+ commits = git_log(
100
+ pretty: '%s|%b|>',
101
+ start: hash,
102
+ debug: debug
103
+ )
104
+
105
+ Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_LAST_TAG] = tag
106
+ Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_LAST_TAG_HASH] = hash
107
+ Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_COMMITS_FROM_HASH] = commits
108
+ end
109
+
110
+ Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_COMMITS_FROM_HASH]
111
+ rescue StandardError => error
112
+ UI.error error.message
113
+ UI.message("Tag was not found for match pattern - #{tag_regex}")
114
+ nil
14
115
  end
15
116
 
16
- # class methods that you define here become available in your action
17
- # as `Helper::SemanticConventionReleaseHelper.your_method`
18
- #
117
+ def self.get_key_from_commit_after_latest_tag(tag_regex:, tag_version_match:, issue_key_regex:, debug:)
118
+ unless Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_LAST_KEYS_FROM_COMMITS] then
119
+ commits = get_commit_after_latest_tag(
120
+ tag_regex: tag_regex,
121
+ tag_version_match: tag_version_match,
122
+ debug: debug,
123
+ )
124
+
125
+ return nil unless commits
126
+
127
+ last_keys_from_commits = commits
128
+ .to_enum(:scan, issue_key_regex)
129
+ .map { $& }
130
+ .reject(&:empty?)
131
+ .uniq
132
+
133
+ Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_LAST_KEYS_FROM_COMMITS] = last_keys_from_commits
134
+ end
135
+
136
+ Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_LAST_KEYS_FROM_COMMITS]
137
+ end
138
+
139
+ def self.get_issues_from_commit_after_latest_tag(tag_regex:, tag_version_match:, issue_key_regex:, debug:)
140
+ unless Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_LAST_ISSUES_FROM_COMMITS] then
141
+ unless @jira_helper then
142
+ UI.user_error! "Uninitialized jira client"
143
+ end
144
+
145
+ keys = get_key_from_commit_after_latest_tag(
146
+ tag_regex: tag_regex,
147
+ tag_version_match: tag_version_match,
148
+ issue_key_regex: issue_key_regex,
149
+ debug: debug
150
+ )
151
+
152
+ issues = @jira_helper.get(keys: keys)
153
+ Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_LAST_ISSUES_FROM_COMMITS] = issues
154
+ end
155
+
156
+ Actions.lane_context[Fastlane::Actions::SharedValues::FL_JIRA_LAST_ISSUES_FROM_COMMITS]
157
+ end
158
+
19
159
  def self.git_log(params)
20
160
  command = "git log --pretty='#{params[:pretty]}' --reverse #{params[:start]}..HEAD"
21
161
  Actions.sh(command, log: params[:debug]).chomp
22
162
  end
23
163
 
24
- def self.jira_helper(host:, username:, password:, context_path:, disable_ssl_verification:)
164
+ def self.generate_changelog(groups:, line_break_format:)
165
+ changelog = []
166
+
167
+ groups.each do |label, issues|
168
+ changelog.concat(['']) unless changelog.to_s.empty?
169
+ changelog.concat(format_issues(label: label, issues: issues)) unless issues.empty?
170
+ end
171
+ # changes = format_issues(issues: issues)
172
+
173
+ changelog.join(line_break_format)
174
+ end
175
+
176
+ def self.format_issues(label:, issues:)
177
+ [
178
+ style_text(text: "► #{label}", style: 'heading'),
179
+ issues.map { |issue| format_issue_link(issue: issue) }
180
+ ].flatten!
181
+ end
182
+
183
+ def self.extract_key_from_branch(branch:, ticket_prefix:)
184
+ regex = Regexp.new("(#{ticket_prefix}-\\d+)")
185
+
186
+ ticket_code = regex.match branch
187
+
188
+ ticket_code
189
+ end
190
+
191
+
192
+ def self.initialize_jira(host:, api_version:, username:, password:, context_path:, disable_ssl_verification:)
193
+ unless @jira_helper then
194
+ @jira_helper = jira_helper(
195
+ host: host,
196
+ api_version: api_version,
197
+ username: username,
198
+ password: password,
199
+ context_path: context_path,
200
+ disable_ssl_verification: disable_ssl_verification
201
+ )
202
+ end
203
+
204
+ @jira_helper
205
+ end
206
+
207
+ def self.jira_helper(host:, api_version:, username:, password:, context_path:, disable_ssl_verification:)
25
208
  JiraHelper.new(
26
209
  host: host,
210
+ api_version: api_version,
27
211
  username: username,
28
212
  password: password,
29
213
  context_path: context_path,
@@ -32,13 +216,14 @@ module Fastlane
32
216
  end
33
217
 
34
218
  class JiraClientHelper
35
- def self.client(host:, username:, password:, context_path:, disable_ssl_verification:)
219
+ def self.client(host:, api_version:, username:, password:, context_path:, disable_ssl_verification:)
36
220
  options = {
37
221
  site: host,
38
222
  context_path: context_path,
39
223
  auth_type: :basic,
40
224
  username: username,
41
225
  password: password,
226
+ rest_base_path: "/rest/api/#{api_version}",
42
227
  ssl_verify_mode: disable_ssl_verification ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
43
228
  }
44
229
 
@@ -47,28 +232,28 @@ module Fastlane
47
232
  end
48
233
 
49
234
  class JiraHelper
50
- def initialize(host:, username:, password:, context_path:, disable_ssl_verification:, jira_client_helper: nil)
235
+ def initialize(host:, api_version:, username:, password:, context_path:, disable_ssl_verification:, jira_client_helper: nil)
51
236
  @host = host
52
237
  @context_path = context_path
53
238
 
54
- jira_client_helper ||= JiraClientHelper.client(
239
+ @client ||= JiraClientHelper.client(
55
240
  host: host,
241
+ api_version: api_version,
56
242
  username: username,
57
243
  password: password,
58
244
  context_path: context_path,
59
245
  disable_ssl_verification: disable_ssl_verification
60
246
  )
61
- @client = jira_client_helper
62
247
  end
63
248
 
64
- def get(issues:, extra_fields: [])
65
- return [] if issues.to_a.empty?
249
+ def get(keys:, extra_fields: [])
250
+ return [] if keys.to_a.empty?
66
251
 
67
252
  fields = [:key, :summary, :status, :issuetype, :description]
68
253
  fields.concat extra_fields
69
254
 
70
255
  begin
71
- @client.Issue.jql("KEY IN (#{issues.join(',')})", fields: fields, validate_query: false)
256
+ @client.Issue.jql("KEY IN (#{keys.join(',')})", fields: fields, validate_query: false)
72
257
  rescue StandardError => e
73
258
  UI.important('Jira Client: Failed to get issue.')
74
259
  UI.important("Jira Client: Reason - #{e.message}")
@@ -76,6 +261,16 @@ module Fastlane
76
261
  end
77
262
  end
78
263
 
264
+ def list_versions(project_id_or_key:, query: nil, order_by: nil, status: nil)
265
+ @client.Version.find(
266
+ project_id_or_key: project_id_or_key,
267
+ query: query,
268
+ orderBy: order_by,
269
+ status: status
270
+ )
271
+ end
272
+
273
+
79
274
  def add_comment(comment:, issues:)
80
275
  return if issues.to_a.empty?
81
276