tefoji 2.1.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/mixins/user_functions.rb +2 -2
- data/lib/tefoji/cli.rb +6 -16
- data/lib/tefoji/declared_value.rb +1 -5
- data/lib/tefoji/jira_api.rb +76 -148
- data/lib/tefoji.rb +37 -39
- metadata +7 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 751842bf3544f557c135661e9d4b57531fadf07df24b7e3d4ff69784a415d948
|
|
4
|
+
data.tar.gz: c7c957bf732fe96d8719acdcd483ef8778b464aacd33de2ae9db589246cd7c89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bf8f8a192b87c00ffa4856c6c72f5e7d52a490f75eb2e54b6b95707a573cbca3d323540f841cad33b5a61f59c761d31d3170a40bba4f24b0e22b4354ef286f6
|
|
7
|
+
data.tar.gz: bbe6db199d6bafab455ce075f6c34cd63eeb91273a1d6469f3cbbd246ef918848f3d53fc070d8eda1cbf4c745c3cf77a2905880df6dc2ebaabd24d63a2e4c239
|
|
@@ -265,7 +265,7 @@ module UserFunctions
|
|
|
265
265
|
condition_hash = args[1]
|
|
266
266
|
|
|
267
267
|
unless condition_hash.keys.sort == %w[X Y Z].sort
|
|
268
|
-
fatal 'function "release_type" requires a hash with keys ("X", "Y", "Z") as 2nd argument. '\
|
|
268
|
+
fatal 'function "release_type" requires a hash with keys ("X", "Y", "Z") as 2nd argument. ' \
|
|
269
269
|
"Got: #{condition_hash}"
|
|
270
270
|
end
|
|
271
271
|
|
|
@@ -281,7 +281,7 @@ module UserFunctions
|
|
|
281
281
|
def split(args)
|
|
282
282
|
_fail_if_unset(__method__.to_s, args)
|
|
283
283
|
fatal('function "split" requires exactly two arguments') unless args.size == 2
|
|
284
|
-
args[1].split(
|
|
284
|
+
args[1].split(%r{#{args[0]}})
|
|
285
285
|
end
|
|
286
286
|
|
|
287
287
|
## Jira fields helper functions
|
data/lib/tefoji/cli.rb
CHANGED
|
@@ -5,12 +5,9 @@ require 'yaml'
|
|
|
5
5
|
|
|
6
6
|
module Tefoji
|
|
7
7
|
class CLI
|
|
8
|
-
DEFAULT_JIRA_URL = 'https://
|
|
9
|
-
JIRA_TEST_URL = 'https://
|
|
10
|
-
DEFAULT_JIRA_AUTH_FILE = "#{
|
|
11
|
-
JIRA_CLOUD_URLS = [
|
|
12
|
-
'https://puppet.atlassian.net'
|
|
13
|
-
]
|
|
8
|
+
DEFAULT_JIRA_URL = 'https://perforce.atlassian.net'
|
|
9
|
+
JIRA_TEST_URL = 'https://perforce-sandbox-903.atlassian.net'
|
|
10
|
+
DEFAULT_JIRA_AUTH_FILE = "#{Dir.home}/.tefoji-auth.yaml"
|
|
14
11
|
|
|
15
12
|
DOCUMENTATION = <<~DOCOPT
|
|
16
13
|
Generate Jira issues from YAML files.
|
|
@@ -24,8 +21,8 @@ module Tefoji
|
|
|
24
21
|
Options:
|
|
25
22
|
-j --jira URL URL of the Jira instance (defaults to #{DEFAULT_JIRA_URL})
|
|
26
23
|
-J --jira-test Use the jira test environment (#{JIRA_TEST_URL})
|
|
27
|
-
-a --jira-auth=FILE Alternate YAML file with Base64-encoded 'username:
|
|
28
|
-
|
|
24
|
+
-a --jira-auth=FILE Alternate YAML file with Base64-encoded 'username:application-token'
|
|
25
|
+
string for Jira authentication
|
|
29
26
|
-S --jira-save-auth Save/overwrite Base64-encoded yaml authentication to the jira-auth
|
|
30
27
|
file (defaults to #{DEFAULT_JIRA_AUTH_FILE}) after succesfully
|
|
31
28
|
authenticating to the Jira server.
|
|
@@ -56,18 +53,11 @@ module Tefoji
|
|
|
56
53
|
@user_options['jira-auth-string'] = nil
|
|
57
54
|
@user_options['jira-auth-file'] = @user_options['--jira-auth'] || DEFAULT_JIRA_AUTH_FILE
|
|
58
55
|
|
|
59
|
-
@user_options['jira-cloud'] = false
|
|
60
|
-
@user_options['jira-cloud'] = true if JIRA_CLOUD_URLS.include?(@user_options['--jira'])
|
|
61
|
-
|
|
62
56
|
jira_auth_file = @user_options['jira-auth-file']
|
|
63
57
|
return unless File.file?(jira_auth_file)
|
|
64
58
|
|
|
65
59
|
authentication = YAML.load_file(jira_auth_file)
|
|
66
|
-
|
|
67
|
-
@user_options['jira-auth-string'] = authentication['jira-cloud-auth']
|
|
68
|
-
elsif authentication.key?('jira-auth')
|
|
69
|
-
@user_options['jira-auth-string'] = authentication['jira-auth']
|
|
70
|
-
end
|
|
60
|
+
@user_options['jira-auth-string'] = authentication['jira-auth']
|
|
71
61
|
end
|
|
72
62
|
|
|
73
63
|
# Iterate through issue templates, validating each. If that goes well, generate
|
|
@@ -37,11 +37,7 @@ module Tefoji
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def falsey?
|
|
40
|
-
|
|
41
|
-
true
|
|
42
|
-
else
|
|
43
|
-
false
|
|
44
|
-
end
|
|
40
|
+
@value.nil? || @value.empty? || @value.downcase == 'false' || @value == false
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
def truthy?
|
data/lib/tefoji/jira_api.rb
CHANGED
|
@@ -2,6 +2,8 @@ require 'base64'
|
|
|
2
2
|
require 'io/console'
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'rest-client'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
5
7
|
RestClient.log = 'stdout'
|
|
6
8
|
|
|
7
9
|
module Tefoji
|
|
@@ -35,8 +37,8 @@ module Tefoji
|
|
|
35
37
|
|
|
36
38
|
# Depending on how user specified their authorization we want to
|
|
37
39
|
# derive and store the username and the Base64-encoded
|
|
38
|
-
# 'username:
|
|
39
|
-
def authenticate(jira_base_url, requested_user = nil,
|
|
40
|
+
# 'username:application-token' string.
|
|
41
|
+
def authenticate(jira_base_url, requested_user = nil, jira_auth = nil)
|
|
40
42
|
@jira_base_url = jira_base_url
|
|
41
43
|
@jira_base_rest_url = "#{jira_base_url}/rest/api/2"
|
|
42
44
|
|
|
@@ -45,12 +47,12 @@ module Tefoji
|
|
|
45
47
|
# If we found an auth string, try to use it. Allow the requested_user
|
|
46
48
|
# to override and send us to prompt
|
|
47
49
|
decoded_auth = nil
|
|
48
|
-
unless
|
|
49
|
-
|
|
50
|
+
unless jira_auth.nil?
|
|
51
|
+
auth_user, auth_application_token = Base64.decode64(jira_auth).split(':')
|
|
50
52
|
# Only set decoded auth if the user name in the auth string matches the
|
|
51
53
|
# requested user name
|
|
52
|
-
unless requested_user && requested_user !=
|
|
53
|
-
decoded_auth = [
|
|
54
|
+
unless requested_user && requested_user != auth_user
|
|
55
|
+
decoded_auth = [auth_user, auth_application_token]
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
|
|
@@ -63,38 +65,35 @@ module Tefoji
|
|
|
63
65
|
print 'Enter jira user name: '
|
|
64
66
|
decoded_auth[0] = $stdin.gets.chomp
|
|
65
67
|
end
|
|
66
|
-
decoded_auth[1] = IO.console.getpass(
|
|
68
|
+
decoded_auth[1] = IO.console.getpass(
|
|
69
|
+
"Enter jira application_token for #{decoded_auth[0]}: "
|
|
70
|
+
)
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
@jira_username = decoded_auth[0]
|
|
70
74
|
|
|
71
75
|
# HTTP doesn't like linefeeds in the auth string, hence #strict_encode64
|
|
72
|
-
@
|
|
73
|
-
@authentication_header = { 'Authorization' => "Basic #{@
|
|
76
|
+
@jira_auth = Base64.strict_encode64(decoded_auth.join(':'))
|
|
77
|
+
@authentication_header = { 'Authorization' => "Basic #{@jira_auth}" }
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
# Do this so we can inform the user quickly that their credentials didn't work
|
|
77
81
|
# There may be a better test, but this is the one the original Winston uses
|
|
78
|
-
def test_authentication
|
|
79
|
-
get_username(@jira_username
|
|
82
|
+
def test_authentication
|
|
83
|
+
get_username(@jira_username)
|
|
80
84
|
rescue RestClient::Forbidden
|
|
81
85
|
fatal 'Forbidden: either the authentication is incorrect or ' \
|
|
82
86
|
'Jira might be requiring a CAPTCHA response from the web interface.'
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
# Get information about user in Jira
|
|
86
|
-
def get_username(username,
|
|
87
|
-
search_parameters =
|
|
88
|
-
"user/search?query=#{username}"
|
|
89
|
-
else
|
|
90
|
-
"user?username=#{username}"
|
|
91
|
-
end
|
|
92
|
-
|
|
90
|
+
def get_username(username, fail_if_not_found = true)
|
|
91
|
+
search_parameters = "user/search?query=#{username}"
|
|
93
92
|
jira_get(search_parameters, fail_if_not_found)
|
|
94
93
|
end
|
|
95
94
|
|
|
96
95
|
# Save authentication YAML to the a file for reuse.
|
|
97
|
-
def save_authentication(save_file_name
|
|
96
|
+
def save_authentication(save_file_name)
|
|
98
97
|
backup_file_name = "#{save_file_name}.bak"
|
|
99
98
|
|
|
100
99
|
if File.readable?(save_file_name)
|
|
@@ -102,12 +101,8 @@ module Tefoji
|
|
|
102
101
|
@logger.info "Saved #{save_file_name} to #{backup_file_name}"
|
|
103
102
|
end
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
else
|
|
108
|
-
"jira-cloud-auth: #{@jira_auth_string}\n"
|
|
109
|
-
end
|
|
110
|
-
File.write(save_file_name, jira_auth_string)
|
|
104
|
+
jira_auth = "jira-auth: #{@jira_auth}\n"
|
|
105
|
+
File.write(save_file_name, jira_auth)
|
|
111
106
|
|
|
112
107
|
File.chmod(0o600, save_file_name)
|
|
113
108
|
|
|
@@ -115,21 +110,21 @@ module Tefoji
|
|
|
115
110
|
end
|
|
116
111
|
|
|
117
112
|
# Create a Jira issue
|
|
118
|
-
def create_issue(issue_data
|
|
113
|
+
def create_issue(issue_data)
|
|
119
114
|
request_path = 'issue'
|
|
120
|
-
jira_fields = issue_data_to_jira_fields(issue_data
|
|
115
|
+
jira_fields = issue_data_to_jira_fields(issue_data)
|
|
121
116
|
jira_post(request_path, jira_fields)
|
|
122
117
|
rescue RestClient::Forbidden
|
|
123
|
-
fatal "Forbidden: could not send #{request_path} request to Jira. "\
|
|
118
|
+
fatal "Forbidden: could not send #{request_path} request to Jira. " \
|
|
124
119
|
'Jira may not be accepting requests.'
|
|
125
120
|
rescue RestClient::BadRequest => e
|
|
126
121
|
error_json = JSON.parse(e.response.body)
|
|
127
122
|
errors = error_json['errors'].values.join("\n")
|
|
128
|
-
fatal "Bad Request: something went wrong with this #{request_path} request: "\
|
|
123
|
+
fatal "Bad Request: something went wrong with this #{request_path} request: " \
|
|
129
124
|
"#{request_path}, #{jira_fields}\n" \
|
|
130
125
|
"Errors were: #{errors}"
|
|
131
126
|
rescue RestClient::Unauthorized
|
|
132
|
-
fatal "Unauthorized: could not send #{request_path} request to Jira. "\
|
|
127
|
+
fatal "Unauthorized: could not send #{request_path} request to Jira. " \
|
|
133
128
|
'Did you use the correct credentials?'
|
|
134
129
|
end
|
|
135
130
|
|
|
@@ -171,23 +166,42 @@ module Tefoji
|
|
|
171
166
|
end
|
|
172
167
|
|
|
173
168
|
# https://www.youtube.com/watch?v=JsntlJZ9h1U
|
|
174
|
-
def add_watcher(issue_key,
|
|
169
|
+
def add_watcher(issue_key, watcher_name)
|
|
175
170
|
request_path = "issue/#{issue_key}/watchers"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
jira_post(request_path, request_data)
|
|
171
|
+
watcher_id = user_email_to_account_id(watcher_name)
|
|
172
|
+
jira_post(request_path, watcher_id)
|
|
179
173
|
end
|
|
180
174
|
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
175
|
+
# Jira on-prem used user names ('eric.griswold', 'tefoji', etc.) to identify users.
|
|
176
|
+
# Jira cloud requires an '@domain.extension' suffix. Handle translating from the onprem
|
|
177
|
+
# form to the cloud form.
|
|
178
|
+
# Nothe the special case of the tefoji service user.
|
|
179
|
+
def user_name_to_account_name(user_name)
|
|
180
|
+
case user_name
|
|
181
|
+
when 'tefoji'
|
|
182
|
+
'svc-tefoji-puppet-jira@perforce.com'
|
|
183
|
+
when %r{URI::MailTo::EMAIL_REGEXP}
|
|
184
|
+
user_name
|
|
185
|
+
else
|
|
186
|
+
"#{user_name.gsub(%r{@.*}, '')}@perforce.com"
|
|
189
187
|
end
|
|
190
|
-
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# jira cloud api calls require an accountId rather than a username. This method will convert
|
|
191
|
+
# emails or display names to accountIds.
|
|
192
|
+
def user_email_to_account_id(user_email)
|
|
193
|
+
account_name = user_name_to_account_name(user_email)
|
|
194
|
+
response = get_username(account_name)
|
|
195
|
+
|
|
196
|
+
fatal "Jira account ID not found for \"#{account_name}\"" if response.empty?
|
|
197
|
+
account_id = response[0]['accountId']
|
|
198
|
+
|
|
199
|
+
@logger.debug("user account name: \"#{account_name} \" " \
|
|
200
|
+
"converted to account ID: \"#{account_id}\"")
|
|
201
|
+
|
|
202
|
+
return account_id if account_id
|
|
203
|
+
|
|
204
|
+
fatal "Jira account ID not found for #{account_name}"
|
|
191
205
|
end
|
|
192
206
|
|
|
193
207
|
private
|
|
@@ -221,11 +235,10 @@ module Tefoji
|
|
|
221
235
|
rescue RestClient::NotFound,
|
|
222
236
|
SocketError,
|
|
223
237
|
Errno::ECONNREFUSED => e
|
|
224
|
-
# Return
|
|
238
|
+
# Return false if not found rather than fail to allow for checking the existence of users.
|
|
225
239
|
fatal "Cannot connect to #{@jira_base_rest_url}: #{e.message}" if fail_if_not_found
|
|
226
240
|
return false
|
|
227
241
|
end
|
|
228
|
-
|
|
229
242
|
return JSON.parse(response.body)
|
|
230
243
|
end
|
|
231
244
|
|
|
@@ -243,6 +256,8 @@ module Tefoji
|
|
|
243
256
|
end
|
|
244
257
|
|
|
245
258
|
return JSON.parse(response.body) unless response.body.empty?
|
|
259
|
+
|
|
260
|
+
true
|
|
246
261
|
end
|
|
247
262
|
|
|
248
263
|
# Using JIRA's REST API to update the Epic Link field returns "204 No Content",
|
|
@@ -273,7 +288,7 @@ module Tefoji
|
|
|
273
288
|
# Provide the needed translation of user-created data to required format
|
|
274
289
|
# of Jira requests. This is mostly a list of fussing with the Jira field
|
|
275
290
|
# names.
|
|
276
|
-
def issue_data_to_jira_fields(issue_data
|
|
291
|
+
def issue_data_to_jira_fields(issue_data)
|
|
277
292
|
# Check to ensure we have what we need to create a issue
|
|
278
293
|
|
|
279
294
|
unless issue_data['summary']
|
|
@@ -293,12 +308,7 @@ module Tefoji
|
|
|
293
308
|
}
|
|
294
309
|
|
|
295
310
|
set_common_jira_fields(issue_data, jira_fields)
|
|
296
|
-
|
|
297
|
-
if jira_cloud
|
|
298
|
-
set_cloud_jira_fields(issue_data, jira_fields)
|
|
299
|
-
else
|
|
300
|
-
set_onprem_jira_fields(issue_data, jira_fields)
|
|
301
|
-
end
|
|
311
|
+
set_cloud_jira_fields(issue_data, jira_fields)
|
|
302
312
|
|
|
303
313
|
return { 'fields' => jira_fields }
|
|
304
314
|
end
|
|
@@ -310,7 +320,7 @@ module Tefoji
|
|
|
310
320
|
end
|
|
311
321
|
|
|
312
322
|
if issue_data['labels']
|
|
313
|
-
labels = issue_data['labels'].to_a.flatten.reject { |t| t =~
|
|
323
|
+
labels = issue_data['labels'].to_a.flatten.reject { |t| t =~ %r{^\s*$} }
|
|
314
324
|
jira_fields['labels'] = labels unless labels.empty?
|
|
315
325
|
end
|
|
316
326
|
if issue_data['duedate']
|
|
@@ -321,7 +331,7 @@ module Tefoji
|
|
|
321
331
|
end
|
|
322
332
|
|
|
323
333
|
if issue_data['components']
|
|
324
|
-
components = issue_data[:components].to_a.flatten.reject { |t| t =~
|
|
334
|
+
components = issue_data[:components].to_a.flatten.reject { |t| t =~ %r{^\s*$} }
|
|
325
335
|
unless components.empty?
|
|
326
336
|
jira_fields['components'] = components.map { |component| { FIELD_NAME => component } }
|
|
327
337
|
end
|
|
@@ -339,51 +349,30 @@ module Tefoji
|
|
|
339
349
|
scrum_teams = issue_data['scrum_teams'] || issue_data['teams']
|
|
340
350
|
|
|
341
351
|
if scrum_teams
|
|
342
|
-
field_name, field_value = scrum_teams_rules(issue_data,
|
|
352
|
+
field_name, field_value = scrum_teams_rules(issue_data, scrum_teams)
|
|
343
353
|
jira_fields[field_name] = field_value
|
|
344
354
|
end
|
|
345
355
|
|
|
346
356
|
# Default issue type to ISSUE_TASK if it isn't already set
|
|
347
357
|
jira_fields['issuetype'] = { FIELD_NAME => ISSUE_TASK }
|
|
348
|
-
|
|
349
|
-
security = ENV['SECURITY'] || issue_data['security']
|
|
350
|
-
return unless security
|
|
351
|
-
|
|
352
|
-
case security.downcase
|
|
353
|
-
when 'confidential'
|
|
354
|
-
jira_fields['security'] = { FIELD_ID => '10002' }
|
|
355
|
-
when 'internal'
|
|
356
|
-
jira_fields['security'] = { FIELD_ID => '10001' }
|
|
357
|
-
when 'public'
|
|
358
|
-
# Nothing to do here - public is default
|
|
359
|
-
else
|
|
360
|
-
@logger.fatal "Unknown security type: #{security}"
|
|
361
|
-
exit 1
|
|
362
|
-
end
|
|
363
358
|
end
|
|
364
359
|
|
|
365
360
|
# There are now some funky rules for setting the 'scrum_team' (previously 'team')
|
|
366
361
|
# and 'scrum_teams' field in different issues.
|
|
367
362
|
#
|
|
368
363
|
# For epics, put the scrum team in the 'components' field.
|
|
369
|
-
#
|
|
370
|
-
# For Puppet Agent (PA) and Puppet Server (SERVER) projects, put the scrum team in
|
|
371
|
-
# customfield_14200
|
|
364
|
+
# For ordinary issues, put the scrum team in customfield_10067
|
|
372
365
|
def scrum_team_rules(issue_data, scrum_team)
|
|
373
366
|
if epic?(issue_data)
|
|
374
367
|
return ['components', [{ FIELD_NAME => scrum_team }]]
|
|
375
368
|
end
|
|
376
369
|
|
|
377
|
-
|
|
378
|
-
if %w[PA SERVER].include?(issue_data['project'].value)
|
|
379
|
-
customfield = 'customfield_14200'
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
return [customfield, { FIELD_VALUE => scrum_team }]
|
|
370
|
+
return ['customfield_10067', { FIELD_VALUE => scrum_team }]
|
|
383
371
|
end
|
|
384
372
|
|
|
385
373
|
def scrum_teams_rules(issue_data, scrum_teams)
|
|
386
|
-
|
|
374
|
+
# For epics, we can have a list of teams.
|
|
375
|
+
processed_teams = scrum_teams.to_a.flatten.reject { |t| t =~ %r{^\s*$} }
|
|
387
376
|
if epic?(issue_data)
|
|
388
377
|
return ['components', processed_teams.map { |team| { FIELD_NAME => team } }]
|
|
389
378
|
end
|
|
@@ -395,7 +384,7 @@ module Tefoji
|
|
|
395
384
|
def set_cloud_jira_fields(issue_data, jira_fields)
|
|
396
385
|
if issue_data['assignee']
|
|
397
386
|
assignee = issue_data['assignee']
|
|
398
|
-
assignee =
|
|
387
|
+
assignee = user_email_to_account_id(assignee.value)
|
|
399
388
|
jira_fields['assignee'] = { FIELD_ACCOUNT_ID => assignee }
|
|
400
389
|
end
|
|
401
390
|
|
|
@@ -408,20 +397,9 @@ module Tefoji
|
|
|
408
397
|
end
|
|
409
398
|
|
|
410
399
|
if issue_data['story_points']
|
|
411
|
-
jira_fields['
|
|
412
|
-
end
|
|
413
|
-
if issue_data['team']
|
|
414
|
-
jira_fields['customfield_10052'] = { FIELD_VALUE => issue_data['team'] }
|
|
415
|
-
end
|
|
416
|
-
if issue_data['teams']
|
|
417
|
-
teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
|
|
418
|
-
unless teams.empty?
|
|
419
|
-
jira_fields['customfield_10066'] = teams.map { |team| { FIELD_VALUE => team } }
|
|
420
|
-
end
|
|
421
|
-
end
|
|
422
|
-
if issue_data['subteam']
|
|
423
|
-
jira_fields['customfield_10045'] = [issue_data['subteam']]
|
|
400
|
+
jira_fields['customfield_10058'] = issue_data['story_points'].to_i
|
|
424
401
|
end
|
|
402
|
+
|
|
425
403
|
if issue_data['sprint']
|
|
426
404
|
jira_fields['customfield_10020'] = issue_data['sprint'].to_i
|
|
427
405
|
end
|
|
@@ -451,56 +429,6 @@ module Tefoji
|
|
|
451
429
|
jira_fields['customfield_10018'] = issue_data['epic_parent']
|
|
452
430
|
end
|
|
453
431
|
end
|
|
454
|
-
|
|
455
|
-
def set_onprem_jira_fields(issue_data, jira_fields)
|
|
456
|
-
if issue_data['assignee']
|
|
457
|
-
jira_fields['assignee'] = { FIELD_NAME => issue_data['assignee'] }
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
if issue_data['type']
|
|
461
|
-
jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
|
|
462
|
-
# If this is an epic, we need to add an epic name
|
|
463
|
-
if epic?(issue_data)
|
|
464
|
-
jira_fields['customfield_10007'] = issue_data['epic_name'] || issue_data['summary']
|
|
465
|
-
end
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
if issue_data['story_points']
|
|
469
|
-
jira_fields['customfield_10002'] = issue_data['story_points'].to_i
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
if issue_data['subteam']
|
|
473
|
-
jira_fields['customfield_11700'] = [issue_data['subteam']]
|
|
474
|
-
end
|
|
475
|
-
if issue_data['sprint']
|
|
476
|
-
jira_fields['customfield_10005'] = issue_data['sprint'].to_i
|
|
477
|
-
end
|
|
478
|
-
if issue_data['acceptance']
|
|
479
|
-
jira_fields['customfield_11501'] = issue_data['acceptance']
|
|
480
|
-
end
|
|
481
|
-
if issue_data['release_notes']
|
|
482
|
-
jira_fields['customfield_11100'] = { FIELD_VALUE => issue_data['release_notes'] }
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
# If a issue has a specified parent issue, prefer that. The parent issue *should* already
|
|
486
|
-
# be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
|
|
487
|
-
# either be an epic linked to the main epic or the main epic itself.
|
|
488
|
-
|
|
489
|
-
if issue_data['parent']
|
|
490
|
-
unless issue_data['type'].casecmp?(ISSUE_SUB_TASK) || !issue_data['type']
|
|
491
|
-
@logger.fatal "A issue with a parent must be classified as a Sub-issue\n\n#{issue_data}"
|
|
492
|
-
exit 1
|
|
493
|
-
end
|
|
494
|
-
jira_fields['issuetype'] = { FIELD_NAME => ISSUE_SUB_TASK }
|
|
495
|
-
jira_fields['parent'] = { FIELD_KEY => issue_data['parent'] }
|
|
496
|
-
elsif issue_data['epic_parent']
|
|
497
|
-
if issue_data['type'].casecmp?(ISSUE_SUB_TASK)
|
|
498
|
-
@logger.fatal "This issue cannot be a subtask of an epic\n\n#{issue_data}"
|
|
499
|
-
exit 1
|
|
500
|
-
end
|
|
501
|
-
jira_fields['customfield_10006'] = issue_data['epic_parent']
|
|
502
|
-
end
|
|
503
|
-
end
|
|
504
432
|
end
|
|
505
433
|
|
|
506
434
|
class JiraMockApi < JiraApi
|
|
@@ -510,20 +438,20 @@ module Tefoji
|
|
|
510
438
|
@summary_count = 0
|
|
511
439
|
end
|
|
512
440
|
|
|
513
|
-
def authenticate(jira_base_url, username = nil,
|
|
441
|
+
def authenticate(jira_base_url, username = nil, jira_auth = nil)
|
|
514
442
|
@jira_base_url = jira_base_url
|
|
515
443
|
@jira_base_rest_url = "#{jira_base_url}/rest/api/2"
|
|
516
444
|
|
|
517
445
|
@logger.info "Using mock of Jira instance: \"#{jira_base_url}\""
|
|
518
446
|
|
|
519
|
-
if
|
|
447
|
+
if jira_auth.nil?
|
|
520
448
|
if username.nil?
|
|
521
449
|
print 'Enter jira user name: '
|
|
522
450
|
username = $stdin.gets.chomp
|
|
523
451
|
end
|
|
524
452
|
else
|
|
525
|
-
|
|
526
|
-
username =
|
|
453
|
+
plain_auth = Base64.decode64(jira_auth)
|
|
454
|
+
username = plain_auth.split(':')[0]
|
|
527
455
|
end
|
|
528
456
|
|
|
529
457
|
@jira_username = username
|
data/lib/tefoji.rb
CHANGED
|
@@ -30,7 +30,6 @@ module Tefoji
|
|
|
30
30
|
@jira_auth_string = user_options['jira-auth-string']
|
|
31
31
|
@jira_auth_file = user_options['jira-auth-file']
|
|
32
32
|
@jira_mock = user_options['--jira-mock']
|
|
33
|
-
@jira_cloud = user_options['jira-cloud']
|
|
34
33
|
|
|
35
34
|
@no_notes = user_options['--no-notes']
|
|
36
35
|
@template_data = {}
|
|
@@ -270,13 +269,13 @@ module Tefoji
|
|
|
270
269
|
@jira_api.logger = @logger
|
|
271
270
|
|
|
272
271
|
@jira_api.authenticate(@jira_url, @jira_user, @jira_auth_string)
|
|
273
|
-
@jira_api.test_authentication
|
|
272
|
+
@jira_api.test_authentication unless @jira_mock
|
|
274
273
|
end
|
|
275
274
|
|
|
276
275
|
# Save Jira auth data
|
|
277
276
|
def save_authentication
|
|
278
277
|
authenticate unless @jira_api
|
|
279
|
-
@jira_api.save_authentication(@jira_auth_file
|
|
278
|
+
@jira_api.save_authentication(@jira_auth_file)
|
|
280
279
|
end
|
|
281
280
|
|
|
282
281
|
def generate_epics_with_issues(epic_key)
|
|
@@ -312,31 +311,9 @@ module Tefoji
|
|
|
312
311
|
end
|
|
313
312
|
|
|
314
313
|
epic['type'] = JiraApi::ISSUE_EPIC
|
|
315
|
-
if private_release?(epic)
|
|
316
|
-
@epic_security = 'internal'
|
|
317
|
-
|
|
318
|
-
private_description = ''
|
|
319
|
-
private_repos = epic['private_release'].value['repos']
|
|
320
|
-
|
|
321
|
-
if private_repos
|
|
322
|
-
private_description << private_release_note
|
|
323
|
-
private_description << private_repos.value.map do |repo|
|
|
324
|
-
repo.map { |k, v| "* #{k.upcase}: #{v.value}" }
|
|
325
|
-
end.join("\n")
|
|
326
|
-
private_description << "\n\n"
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
private_leads = epic['private_release'].value['leads']
|
|
330
|
-
if private_leads
|
|
331
|
-
leads = private_leads.value.map { |lead| "[~#{lead}]" }.join(' ')
|
|
332
|
-
private_description << "/cc #{leads} \n\n"
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
epic['description'].prepend(private_description)
|
|
336
|
-
epic['security'] = 'internal'
|
|
337
|
-
end
|
|
314
|
+
add_private_release(epic) if private_release?(epic)
|
|
338
315
|
|
|
339
|
-
epic_issue = @jira_api.create_issue(epic
|
|
316
|
+
epic_issue = @jira_api.create_issue(epic)
|
|
340
317
|
epic_issue['short_name'] = short_name
|
|
341
318
|
@logger.info 'Epic: %16s [%s]' % [epic_issue['key'], short_name]
|
|
342
319
|
@deferral_data[short_name.to_s] = {
|
|
@@ -359,6 +336,30 @@ module Tefoji
|
|
|
359
336
|
"NOTE: THIS IS A PRIVATE RELEASE. WORK WAS DONE IN THESE PRIVATE REPOS:\n"
|
|
360
337
|
end
|
|
361
338
|
|
|
339
|
+
def add_private_release(epic)
|
|
340
|
+
private_description = ''
|
|
341
|
+
private_repos = epic['private_release'].value['repos']
|
|
342
|
+
|
|
343
|
+
if private_repos
|
|
344
|
+
private_description << private_release_note
|
|
345
|
+
private_description << private_repos.value.map do |repo|
|
|
346
|
+
repo.map { |k, v| "* #{k.upcase}: #{v.value}" }
|
|
347
|
+
end.join("\n")
|
|
348
|
+
private_description << "\n\n"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
private_leads = epic['private_release'].value['leads']
|
|
352
|
+
if private_leads
|
|
353
|
+
leads = private_leads.value.map do |lead|
|
|
354
|
+
"[~accountid:#{@jira_api.user_email_to_account_id(lead)}]"
|
|
355
|
+
end.join(' ')
|
|
356
|
+
|
|
357
|
+
private_description << "/cc #{leads} \n\n"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
epic['description'].prepend(private_description)
|
|
361
|
+
end
|
|
362
|
+
|
|
362
363
|
# Iterate through all issues in the template, creating each one in Jira.
|
|
363
364
|
# Link the issues back to their epic, if required
|
|
364
365
|
def generate_ordinary_issues
|
|
@@ -375,7 +376,7 @@ module Tefoji
|
|
|
375
376
|
jira_ready_data, raw_issue_data = prepare_jira_ready_data(issue, issue_defaults)
|
|
376
377
|
next if jira_ready_data.nil? || raw_issue_data.nil?
|
|
377
378
|
|
|
378
|
-
response_data = @jira_api.create_issue(jira_ready_data
|
|
379
|
+
response_data = @jira_api.create_issue(jira_ready_data)
|
|
379
380
|
jira_issue = @jira_api.retrieve_issue(response_data['self'])
|
|
380
381
|
jira_issue['short_name'] = raw_issue_data['short_name']
|
|
381
382
|
|
|
@@ -584,7 +585,7 @@ module Tefoji
|
|
|
584
585
|
issue_key = jira_issue_data['key']
|
|
585
586
|
watchers = raw_issue_data[deferred_tag].value
|
|
586
587
|
watchers.each do |watcher|
|
|
587
|
-
@jira_api.add_watcher(issue_key, watcher
|
|
588
|
+
@jira_api.add_watcher(issue_key, watcher)
|
|
588
589
|
@logger.info '%14s: watched by %s' % [issue_key, watcher]
|
|
589
590
|
end
|
|
590
591
|
end
|
|
@@ -652,10 +653,10 @@ module Tefoji
|
|
|
652
653
|
# cannot.
|
|
653
654
|
if right_value.unset?
|
|
654
655
|
# Convert %{FOO} to FOO
|
|
655
|
-
environment_variable = value.gsub(
|
|
656
|
+
environment_variable = value.gsub(%r{^%{(.+?)}}, '\1')
|
|
656
657
|
if environment_keys.include?(environment_variable)
|
|
657
|
-
@logger.info "Setting \"#{key}\" to \"#{ENV[environment_variable]}\" "\
|
|
658
|
-
|
|
658
|
+
@logger.info "Setting \"#{key}\" to \"#{ENV[environment_variable]}\" " \
|
|
659
|
+
'from environment variable.'
|
|
659
660
|
@declarations[key.to_sym] = DeclaredValue.new(ENV[environment_variable])
|
|
660
661
|
next
|
|
661
662
|
end
|
|
@@ -717,7 +718,7 @@ module Tefoji
|
|
|
717
718
|
def expand_string(value)
|
|
718
719
|
# Special-case where 'foo: "%{bar}"'; this allows for passing around hashes and
|
|
719
720
|
# arrays without interpolation into strings
|
|
720
|
-
single_value =
|
|
721
|
+
single_value = %r{\A%{(.+?)}\Z}.match(value)
|
|
721
722
|
return @declarations[single_value[1].to_sym] if single_value
|
|
722
723
|
|
|
723
724
|
return value % @declarations
|
|
@@ -741,10 +742,8 @@ module Tefoji
|
|
|
741
742
|
def conflicting_conditionals?(template)
|
|
742
743
|
if template.key?('unless') && (template.key?('conditional') || template.key?('if'))
|
|
743
744
|
true
|
|
744
|
-
elsif template.key?('if') && template.key?('conditional')
|
|
745
|
-
true
|
|
746
745
|
else
|
|
747
|
-
|
|
746
|
+
template.key?('if') && template.key?('conditional')
|
|
748
747
|
end
|
|
749
748
|
end
|
|
750
749
|
|
|
@@ -789,7 +788,7 @@ module Tefoji
|
|
|
789
788
|
|
|
790
789
|
def missing_expected_keys?(template_uri, template)
|
|
791
790
|
expected_keys = %w[epic epics issues]
|
|
792
|
-
return false if
|
|
791
|
+
return false if template.keys.intersect?(expected_keys)
|
|
793
792
|
|
|
794
793
|
@logger.error "Cannot find any known template keywords in \"#{template_uri}\""
|
|
795
794
|
return true
|
|
@@ -809,7 +808,6 @@ module Tefoji
|
|
|
809
808
|
|
|
810
809
|
def check_assignees(main_template_data)
|
|
811
810
|
valid_users = []
|
|
812
|
-
|
|
813
811
|
invalid_users_to_epics = {}
|
|
814
812
|
main_template_data['epics']&.each do |epic|
|
|
815
813
|
next unless epic['assignee']
|
|
@@ -906,7 +904,7 @@ module Tefoji
|
|
|
906
904
|
def update_user_validity(username, issue_name, valid_users, invalid_users)
|
|
907
905
|
if invalid_users.key?(username)
|
|
908
906
|
invalid_users[username] += [issue_name]
|
|
909
|
-
elsif @jira_api.get_username(username,
|
|
907
|
+
elsif @jira_api.get_username(username, false)
|
|
910
908
|
valid_users << username
|
|
911
909
|
else
|
|
912
910
|
invalid_users[username] = invalid_users.fetch(username, []) + [issue_name]
|
metadata
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tefoji
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- Puppet
|
|
7
|
+
- Puppet By Perforce
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-08-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: debug
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: 1.0.0
|
|
20
20
|
type: :development
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: pry-doc
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
34
|
-
type: :development
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
26
|
+
version: 1.0.0
|
|
41
27
|
- !ruby/object:Gem::Dependency
|
|
42
28
|
name: rake
|
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -168,7 +154,7 @@ description: 'From a YAML specification, create a batch of Jira issues allowing
|
|
|
168
154
|
variable substitutions.
|
|
169
155
|
|
|
170
156
|
'
|
|
171
|
-
email:
|
|
157
|
+
email: release@puppet.com
|
|
172
158
|
executables:
|
|
173
159
|
- tefoji
|
|
174
160
|
extensions: []
|