tefoji 1.0.8 → 1.0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/mixins/logging.rb +1 -1
- data/lib/mixins/user_functions.rb +4 -6
- data/lib/tefoji/cli.rb +17 -9
- data/lib/tefoji/jira_api.rb +158 -57
- data/lib/tefoji.rb +136 -10
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8859212ce735cdc0292cbbf58899931446e7a0b90e330899c15f277d071e723
|
4
|
+
data.tar.gz: 41baa8c0faa84d4c058adf946eb0a9944a8a997a464a9c83c172b7eb191e19d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8e4ca95fd24a97cbe1d24c25885abf7cad21a1ec135c2c346bd5e349b583403419c10c98d07ada2ffd5a44bcbc64aedfbce7ff97c754e9ce77c93b2a96133443
|
7
|
+
data.tar.gz: bd8c63324127e9d2a291af0d489fcb739d99d587c95360aacb1b5164003ec267c832bd30ad98dd57da399c071f17929dc6aa0605375f3b0554bfe9f6e165d44b
|
data/lib/mixins/logging.rb
CHANGED
@@ -34,7 +34,6 @@ module UserFunctions
|
|
34
34
|
# Maps of Winston-style Jira names (projects, sprints, to the actual assets
|
35
35
|
def jira_projects
|
36
36
|
{
|
37
|
-
BOLT: 'BOLT',
|
38
37
|
BUILD_TOOLS: 'BUILD',
|
39
38
|
CLIENT_TOOLS: 'CT',
|
40
39
|
CODE_MANAGEMENT: 'CODEMGMT',
|
@@ -48,16 +47,15 @@ module UserFunctions
|
|
48
47
|
MODULES: 'MODULES',
|
49
48
|
MODULES_INTERNAL: 'FM',
|
50
49
|
OPERATIONS: 'OPS',
|
51
|
-
PDK: '
|
50
|
+
PDK: 'CONT',
|
52
51
|
PE_INTERNAL: 'PE',
|
52
|
+
POOLER: 'POOLER',
|
53
53
|
PROJECT_CENTRAL: 'PC',
|
54
54
|
PUPPETDB: 'PDB',
|
55
55
|
PUPPETSERVER: 'SERVER',
|
56
56
|
PUPPET_AGENT: 'PA',
|
57
57
|
PUPPET: 'PUP',
|
58
58
|
QUALITY_ENGINEERING: 'QENG',
|
59
|
-
DIO: 'DIO',
|
60
|
-
RAZOR: 'RAZOR',
|
61
59
|
RELEASE_ENGINEERING: 'RE',
|
62
60
|
SLV: 'SLV',
|
63
61
|
SUPPORT: 'SUP',
|
@@ -89,12 +87,12 @@ module UserFunctions
|
|
89
87
|
CD4PE: 'CD4PE',
|
90
88
|
CODE_MANAGEMENT: 'Dumpling',
|
91
89
|
DUMPLING: 'Dumpling',
|
92
|
-
FACTER:
|
90
|
+
FACTER: 'Phoenix',
|
93
91
|
INSTALLER: 'Installer and Management',
|
94
92
|
NETWORKING: 'Network Automation',
|
95
93
|
OPERATIONS: 'Operations',
|
96
94
|
PE: 'Dumpling',
|
97
|
-
PLATFORM_OS:
|
95
|
+
PLATFORM_OS: 'Phoenix',
|
98
96
|
PUPPETDB: 'Dumpling',
|
99
97
|
PUPPETSERVER: 'Dumpling',
|
100
98
|
QE: 'Quality Engineering',
|
data/lib/tefoji/cli.rb
CHANGED
@@ -8,6 +8,9 @@ module Tefoji
|
|
8
8
|
DEFAULT_JIRA_URL = 'https://tickets.puppetlabs.com'
|
9
9
|
JIRA_TEST_URL = 'https://jira-app-dev-1.ops.puppetlabs.net'
|
10
10
|
DEFAULT_JIRA_AUTH_FILE = "#{ENV['HOME']}/.tefoji-auth.yaml"
|
11
|
+
JIRA_CLOUD_URLS = [
|
12
|
+
'https://puppet.atlassian.net'
|
13
|
+
]
|
11
14
|
|
12
15
|
DOCUMENTATION = <<~DOCOPT
|
13
16
|
Generate Jira issues from YAML files.
|
@@ -43,23 +46,28 @@ module Tefoji
|
|
43
46
|
@user_options = parse_options(argv)
|
44
47
|
|
45
48
|
if @user_options['--version']
|
46
|
-
warn "tefoji version #{Gem.loaded_specs['tefoji'].version
|
49
|
+
warn "tefoji version #{Gem.loaded_specs['tefoji'].version}"
|
47
50
|
exit 0
|
48
51
|
end
|
49
52
|
|
53
|
+
@user_options['--jira'] = JIRA_TEST_URL if @user_options['--jira-test']
|
54
|
+
@user_options['--jira'] = DEFAULT_JIRA_URL if @user_options['--jira'].nil?
|
55
|
+
|
50
56
|
@user_options['jira-auth-string'] = nil
|
51
57
|
@user_options['jira-auth-file'] = @user_options['--jira-auth'] || DEFAULT_JIRA_AUTH_FILE
|
52
58
|
|
59
|
+
@user_options['jira-cloud'] = false
|
60
|
+
@user_options['jira-cloud'] = true if JIRA_CLOUD_URLS.include?(@user_options['--jira'])
|
61
|
+
|
53
62
|
jira_auth_file = @user_options['jira-auth-file']
|
54
|
-
|
55
|
-
authentication = YAML.load_file(jira_auth_file)
|
56
|
-
if authentication.key?('jira-auth')
|
57
|
-
@user_options['jira-auth-string'] = authentication['jira-auth']
|
58
|
-
end
|
59
|
-
end
|
63
|
+
return unless File.file?(jira_auth_file)
|
60
64
|
|
61
|
-
|
62
|
-
@user_options['
|
65
|
+
authentication = YAML.load_file(jira_auth_file)
|
66
|
+
if @user_options['jira-cloud'] && authentication.key?('jira-cloud-auth')
|
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
|
63
71
|
end
|
64
72
|
|
65
73
|
# Iterate through issue templates, validating each. If that goes well, generate
|
data/lib/tefoji/jira_api.rb
CHANGED
@@ -2,6 +2,7 @@ require 'base64'
|
|
2
2
|
require 'io/console'
|
3
3
|
require 'json'
|
4
4
|
require 'rest-client'
|
5
|
+
RestClient.log = 'stdout'
|
5
6
|
|
6
7
|
module Tefoji
|
7
8
|
## An interface to send API request to Jira using the rest-client gem.
|
@@ -14,6 +15,7 @@ module Tefoji
|
|
14
15
|
include Logging
|
15
16
|
|
16
17
|
## Jira field keys
|
18
|
+
FIELD_ACCOUNT_ID = 'accountId'
|
17
19
|
FIELD_ID = 'id'
|
18
20
|
FIELD_KEY = 'key'
|
19
21
|
FIELD_NAME = 'name'
|
@@ -74,19 +76,26 @@ module Tefoji
|
|
74
76
|
|
75
77
|
# Do this so we can inform the user quickly that their credentials didn't work
|
76
78
|
# There may be a better test, but this is the one the original Winston uses
|
77
|
-
def test_authentication
|
78
|
-
get_username
|
79
|
+
def test_authentication(jira_cloud)
|
80
|
+
get_username(@jira_username, jira_cloud)
|
79
81
|
rescue RestClient::Forbidden
|
80
82
|
fatal 'Forbidden: either the authentication is incorrect or ' \
|
81
83
|
'Jira might be requiring a CAPTCHA response from the web interface.'
|
82
84
|
end
|
83
85
|
|
84
|
-
|
85
|
-
|
86
|
+
# Get information about user in Jira
|
87
|
+
def get_username(username, jira_cloud, fail_if_not_found = true)
|
88
|
+
search_parameters = if jira_cloud
|
89
|
+
"user/search?query=#{username}"
|
90
|
+
else
|
91
|
+
"user?username=#{username}"
|
92
|
+
end
|
93
|
+
|
94
|
+
jira_get(search_parameters, fail_if_not_found)
|
86
95
|
end
|
87
96
|
|
88
97
|
# Save authentication YAML to the a file for reuse.
|
89
|
-
def save_authentication(save_file_name)
|
98
|
+
def save_authentication(save_file_name, jira_cloud)
|
90
99
|
backup_file_name = "#{save_file_name}.bak"
|
91
100
|
|
92
101
|
if File.readable?(save_file_name)
|
@@ -94,18 +103,22 @@ module Tefoji
|
|
94
103
|
@logger.info "Saved #{save_file_name} to #{backup_file_name}"
|
95
104
|
end
|
96
105
|
|
97
|
-
|
98
|
-
|
99
|
-
|
106
|
+
jira_auth_string = if jira_cloud
|
107
|
+
"jira-auth: #{@jira_auth_string}\n"
|
108
|
+
else
|
109
|
+
"jira-cloud-auth: #{@jira_auth_string}\n"
|
110
|
+
end
|
111
|
+
File.write(save_file_name, jira_auth_string)
|
112
|
+
|
100
113
|
File.chmod(0o600, save_file_name)
|
101
114
|
|
102
115
|
@logger.info "Saved Jira authentication to #{save_file_name}"
|
103
116
|
end
|
104
117
|
|
105
118
|
# Create a Jira issue
|
106
|
-
def create_issue(issue_data)
|
119
|
+
def create_issue(issue_data, jira_cloud)
|
107
120
|
request_path = 'issue'
|
108
|
-
jira_fields = issue_data_to_jira_fields(issue_data)
|
121
|
+
jira_fields = issue_data_to_jira_fields(issue_data, jira_cloud)
|
109
122
|
jira_post(request_path, jira_fields)
|
110
123
|
rescue RestClient::Forbidden
|
111
124
|
fatal "Forbidden: could not send #{request_path} request to Jira. "\
|
@@ -159,15 +172,28 @@ module Tefoji
|
|
159
172
|
end
|
160
173
|
|
161
174
|
# https://www.youtube.com/watch?v=JsntlJZ9h1U
|
162
|
-
def add_watcher(issue_key, watcher)
|
175
|
+
def add_watcher(issue_key, watcher, jira_cloud)
|
163
176
|
request_path = "issue/#{issue_key}/watchers"
|
177
|
+
watcher = useremail_to_id(watcher) if jira_cloud
|
164
178
|
request_data = watcher
|
165
179
|
jira_post(request_path, request_data)
|
166
180
|
end
|
167
181
|
|
182
|
+
# jira cloud api calls now require an accountId rather than a username. This method will convert
|
183
|
+
# emails or display names to accountIds.
|
184
|
+
def useremail_to_id(email)
|
185
|
+
response = get_username(email, true, true)[0]
|
186
|
+
id = response['accountId']
|
187
|
+
if id.nil? || id.empty?
|
188
|
+
@logger.error "accountId not found for #{email}."
|
189
|
+
exit 1
|
190
|
+
end
|
191
|
+
return id
|
192
|
+
end
|
193
|
+
|
168
194
|
private
|
169
195
|
|
170
|
-
def jira_get(jira_request_path)
|
196
|
+
def jira_get(jira_request_path, fail_if_not_found = true)
|
171
197
|
# Jira likes to send complete URLs for responses. Handle
|
172
198
|
# the case where we've received a 'self' query from Jira with
|
173
199
|
# fully formed url
|
@@ -176,7 +202,6 @@ module Tefoji
|
|
176
202
|
url = "#{@jira_base_rest_url}/#{jira_request_path}"
|
177
203
|
end
|
178
204
|
headers = @authentication_header
|
179
|
-
|
180
205
|
begin
|
181
206
|
response = RestClient.get(url, headers)
|
182
207
|
rescue RestClient::MovedPermanently,
|
@@ -191,7 +216,9 @@ module Tefoji
|
|
191
216
|
rescue RestClient::NotFound,
|
192
217
|
SocketError,
|
193
218
|
Errno::ECONNREFUSED => e
|
194
|
-
|
219
|
+
# Return False if not found rather than fail to allow for checking the existence of users.
|
220
|
+
fatal "Cannot connect to #{@jira_base_rest_url}: #{e.message}" if fail_if_not_found
|
221
|
+
return false
|
195
222
|
end
|
196
223
|
|
197
224
|
return JSON.parse(response.body)
|
@@ -241,7 +268,7 @@ module Tefoji
|
|
241
268
|
# Provide the needed translation of user-created data to required format
|
242
269
|
# of Jira requests. This is mostly a list of fussing with the Jira field
|
243
270
|
# names.
|
244
|
-
def issue_data_to_jira_fields(issue_data)
|
271
|
+
def issue_data_to_jira_fields(issue_data, jira_cloud)
|
245
272
|
# Check to ensure we have what we need to create a issue
|
246
273
|
|
247
274
|
unless issue_data['summary']
|
@@ -254,64 +281,130 @@ module Tefoji
|
|
254
281
|
end
|
255
282
|
|
256
283
|
# build the jira_fields hash describing the issue
|
257
|
-
|
258
284
|
# These are required for all issues
|
259
285
|
jira_fields = {
|
260
286
|
'summary' => issue_data['summary'],
|
261
287
|
'project' => { 'key' => issue_data['project'] }
|
262
288
|
}
|
263
289
|
|
290
|
+
set_common_jira_fields(issue_data, jira_fields)
|
291
|
+
|
292
|
+
if jira_cloud
|
293
|
+
set_cloud_jira_fields(issue_data, jira_fields)
|
294
|
+
else
|
295
|
+
set_onprem_jira_fields(issue_data, jira_fields)
|
296
|
+
end
|
297
|
+
|
298
|
+
return { 'fields' => jira_fields }
|
299
|
+
end
|
300
|
+
|
301
|
+
def set_common_jira_fields(issue_data, jira_fields)
|
264
302
|
# The following are optional
|
265
303
|
if issue_data['description']
|
266
304
|
jira_fields['description'] = issue_data['description']
|
267
305
|
end
|
306
|
+
|
307
|
+
if issue_data['labels']
|
308
|
+
labels = issue_data['labels'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
|
309
|
+
jira_fields['labels'] = labels unless labels.empty?
|
310
|
+
end
|
311
|
+
if issue_data['duedate']
|
312
|
+
jira_fields['duedate'] = issue_data['duedate']
|
313
|
+
end
|
314
|
+
if issue_data['fix_version']
|
315
|
+
jira_fields['fixVersions'] = [issue_data['fix_version']]
|
316
|
+
end
|
317
|
+
|
318
|
+
if issue_data['components']
|
319
|
+
components = issue_data[:components].to_a.flatten.reject { |t| t =~ /^\s*$/ }
|
320
|
+
unless components.empty?
|
321
|
+
jira_fields['components'] = components.map { |component| { FIELD_NAME => component } }
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# Default issue type to ISSUE_TASK if it isn't already set
|
326
|
+
jira_fields['issuetype'] = { FIELD_NAME => ISSUE_TASK }
|
327
|
+
|
328
|
+
security = ENV['SECURITY'] || issue_data['security']
|
329
|
+
return unless security
|
330
|
+
|
331
|
+
case security.downcase
|
332
|
+
when 'confidential'
|
333
|
+
jira_fields['security'] = { FIELD_ID => '10002' }
|
334
|
+
when 'internal'
|
335
|
+
jira_fields['security'] = { FIELD_ID => '10001' }
|
336
|
+
when 'public'
|
337
|
+
# Nothing to do here - public is default
|
338
|
+
else
|
339
|
+
@logger.fatal "Unknown security type: #{security}"
|
340
|
+
exit 1
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def set_cloud_jira_fields(issue_data, jira_fields)
|
268
345
|
if issue_data['assignee']
|
269
|
-
|
346
|
+
assignee = issue_data['assignee']
|
347
|
+
assignee = useremail_to_id(assignee)
|
348
|
+
jira_fields['assignee'] = { FIELD_ACCOUNT_ID => assignee }
|
349
|
+
end
|
350
|
+
|
351
|
+
if issue_data['type']
|
352
|
+
jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
|
353
|
+
# If this is an epic, we need to add an epic name
|
354
|
+
if issue_data['type'].casecmp?(ISSUE_EPIC)
|
355
|
+
jira_fields['customfield_10011'] = issue_data['epic_name'] || issue_data['summary']
|
356
|
+
end
|
270
357
|
end
|
358
|
+
|
271
359
|
if issue_data['story_points']
|
272
|
-
jira_fields['
|
360
|
+
jira_fields['customfield_10038'] = issue_data['story_points'].to_i
|
273
361
|
end
|
274
362
|
if issue_data['team']
|
275
|
-
jira_fields['
|
363
|
+
jira_fields['customfield_10052'] = { FIELD_VALUE => issue_data['team'] }
|
276
364
|
end
|
277
365
|
if issue_data['teams']
|
278
366
|
teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
|
279
367
|
unless teams.empty?
|
280
|
-
jira_fields['
|
368
|
+
jira_fields['customfield_10066'] = teams.map { |team| { FIELD_VALUE => team } }
|
281
369
|
end
|
282
370
|
end
|
283
371
|
if issue_data['subteam']
|
284
|
-
jira_fields['
|
372
|
+
jira_fields['customfield_10045'] = [issue_data['subteam']]
|
285
373
|
end
|
286
374
|
if issue_data['sprint']
|
287
|
-
jira_fields['
|
375
|
+
jira_fields['customfield_10020'] = issue_data['sprint'].to_i
|
288
376
|
end
|
289
377
|
if issue_data['acceptance']
|
290
|
-
jira_fields['
|
291
|
-
end
|
292
|
-
if issue_data['labels']
|
293
|
-
labels = issue_data['labels'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
|
294
|
-
jira_fields['labels'] = labels unless labels.empty?
|
295
|
-
end
|
296
|
-
if issue_data['duedate']
|
297
|
-
jira_fields['duedate'] = issue_data['duedate']
|
298
|
-
end
|
299
|
-
if issue_data['fix_version']
|
300
|
-
jira_fields['fixVersions'] = [issue_data['fix_version']]
|
378
|
+
jira_fields['customfield_10062'] = issue_data['acceptance']
|
301
379
|
end
|
302
380
|
if issue_data['release_notes']
|
303
|
-
jira_fields['
|
381
|
+
jira_fields['customfield_10043'] = { FIELD_VALUE => issue_data['release_notes'] }
|
304
382
|
end
|
305
383
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
384
|
+
# If a issue has a specified parent issue, prefer that. The parent issue *should* already
|
385
|
+
# be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
|
386
|
+
# either be an epic linked to the main epic or the main epic itself.
|
387
|
+
|
388
|
+
if issue_data['parent']
|
389
|
+
unless issue_data['type'].casecmp?(ISSUE_SUB_TASK) || !issue_data['type']
|
390
|
+
@logger.fatal "A issue with a parent must be classified as a Sub-issue\n\n#{issue_data}"
|
391
|
+
exit 1
|
392
|
+
end
|
393
|
+
jira_fields['issuetype'] = { FIELD_NAME => ISSUE_SUB_TASK }
|
394
|
+
jira_fields['parent'] = { FIELD_KEY => issue_data['parent'] }
|
395
|
+
elsif issue_data['epic_parent']
|
396
|
+
if issue_data['type'].casecmp?(ISSUE_SUB_TASK)
|
397
|
+
@logger.fatal "This issue cannot be a subtask of an epic\n\n#{issue_data}"
|
398
|
+
exit 1
|
310
399
|
end
|
400
|
+
jira_fields['customfield_10018'] = issue_data['epic_parent']
|
311
401
|
end
|
402
|
+
end
|
312
403
|
|
313
|
-
|
314
|
-
|
404
|
+
def set_onprem_jira_fields(issue_data, jira_fields)
|
405
|
+
if issue_data['assignee']
|
406
|
+
jira_fields['assignee'] = { FIELD_NAME => issue_data['assignee'] }
|
407
|
+
end
|
315
408
|
|
316
409
|
if issue_data['type']
|
317
410
|
jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
|
@@ -321,6 +414,31 @@ module Tefoji
|
|
321
414
|
end
|
322
415
|
end
|
323
416
|
|
417
|
+
if issue_data['story_points']
|
418
|
+
jira_fields['customfield_10002'] = issue_data['story_points'].to_i
|
419
|
+
end
|
420
|
+
if issue_data['team']
|
421
|
+
jira_fields['customfield_14200'] = { FIELD_VALUE => issue_data['team'] }
|
422
|
+
end
|
423
|
+
if issue_data['teams']
|
424
|
+
teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
|
425
|
+
unless teams.empty?
|
426
|
+
jira_fields['customfield_14201'] = teams.map { |team| { FIELD_VALUE => team } }
|
427
|
+
end
|
428
|
+
end
|
429
|
+
if issue_data['subteam']
|
430
|
+
jira_fields['customfield_11700'] = [issue_data['subteam']]
|
431
|
+
end
|
432
|
+
if issue_data['sprint']
|
433
|
+
jira_fields['customfield_10005'] = issue_data['sprint'].to_i
|
434
|
+
end
|
435
|
+
if issue_data['acceptance']
|
436
|
+
jira_fields['customfield_11501'] = issue_data['acceptance']
|
437
|
+
end
|
438
|
+
if issue_data['release_notes']
|
439
|
+
jira_fields['customfield_11100'] = { FIELD_VALUE => issue_data['release_notes'] }
|
440
|
+
end
|
441
|
+
|
324
442
|
# If a issue has a specified parent issue, prefer that. The parent issue *should* already
|
325
443
|
# be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
|
326
444
|
# either be an epic linked to the main epic or the main epic itself.
|
@@ -339,23 +457,6 @@ module Tefoji
|
|
339
457
|
end
|
340
458
|
jira_fields['customfield_10006'] = issue_data['epic_parent']
|
341
459
|
end
|
342
|
-
|
343
|
-
security = ENV['SECURITY'] || issue_data['security']
|
344
|
-
if security
|
345
|
-
case security.downcase
|
346
|
-
when 'confidential'
|
347
|
-
jira_fields['security'] = { FIELD_ID => '10002' }
|
348
|
-
when 'internal'
|
349
|
-
jira_fields['security'] = { FIELD_ID => '10001' }
|
350
|
-
when 'public'
|
351
|
-
# Nothing to do here - public is default
|
352
|
-
else
|
353
|
-
@logger.fatal "Unknown security type: #{security}"
|
354
|
-
exit 1
|
355
|
-
end
|
356
|
-
end
|
357
|
-
|
358
|
-
return { 'fields' => jira_fields }
|
359
460
|
end
|
360
461
|
end
|
361
462
|
|
data/lib/tefoji.rb
CHANGED
@@ -28,6 +28,7 @@ module Tefoji
|
|
28
28
|
@jira_auth_string = user_options['jira-auth-string']
|
29
29
|
@jira_auth_file = user_options['jira-auth-file']
|
30
30
|
@jira_mock = user_options['--jira-mock']
|
31
|
+
@jira_cloud = user_options['jira-cloud']
|
31
32
|
|
32
33
|
@no_notes = user_options['--no-notes']
|
33
34
|
@template_data = {}
|
@@ -74,6 +75,8 @@ module Tefoji
|
|
74
75
|
resolve_variables(main_template_data['declare'])
|
75
76
|
@logger.debug "Declarations: #{@declarations}"
|
76
77
|
|
78
|
+
check_assignees(main_template_data)
|
79
|
+
|
77
80
|
# Process 'before' templates
|
78
81
|
main_template.dig(:includes, :before)&.each do |before|
|
79
82
|
default_epic_saved = @default_target_epic
|
@@ -270,13 +273,13 @@ module Tefoji
|
|
270
273
|
@jira_api.logger = @logger
|
271
274
|
|
272
275
|
@jira_api.authenticate(@jira_url, @jira_user, @jira_auth_string)
|
273
|
-
@jira_api.test_authentication unless @jira_mock
|
276
|
+
@jira_api.test_authentication(@jira_cloud) unless @jira_mock
|
274
277
|
end
|
275
278
|
|
276
279
|
# Save Jira auth data
|
277
280
|
def save_authentication
|
278
281
|
authenticate unless @jira_api
|
279
|
-
@jira_api.save_authentication(@jira_auth_file)
|
282
|
+
@jira_api.save_authentication(@jira_auth_file, @jira_cloud)
|
280
283
|
end
|
281
284
|
|
282
285
|
private
|
@@ -296,7 +299,7 @@ module Tefoji
|
|
296
299
|
|
297
300
|
feature = variable_substitute(feature_to_do)
|
298
301
|
feature['type'] = JiraApi::ISSUE_FEATURE
|
299
|
-
@feature_issue = @jira_api.create_issue(feature)
|
302
|
+
@feature_issue = @jira_api.create_issue(feature, @jira_cloud)
|
300
303
|
@logger.info "Feature issue: #{@feature_issue['key']}"
|
301
304
|
@feature_issue
|
302
305
|
end
|
@@ -358,7 +361,7 @@ module Tefoji
|
|
358
361
|
epic['security'] = 'internal'
|
359
362
|
end
|
360
363
|
|
361
|
-
epic_issue = @jira_api.create_issue(epic)
|
364
|
+
epic_issue = @jira_api.create_issue(epic, @jira_cloud)
|
362
365
|
epic_issue['short_name'] = short_name
|
363
366
|
@logger.info 'Epic: %16s [%s]' % [epic_issue['key'], short_name]
|
364
367
|
@jira_api.link_issues(@feature_issue['key'], epic_issue['key']) if @feature_issue
|
@@ -398,7 +401,7 @@ module Tefoji
|
|
398
401
|
jira_ready_data, raw_issue_data = prepare_jira_ready_data(issue, issue_defaults)
|
399
402
|
next if jira_ready_data.nil? || raw_issue_data.nil?
|
400
403
|
|
401
|
-
response_data = @jira_api.create_issue(jira_ready_data)
|
404
|
+
response_data = @jira_api.create_issue(jira_ready_data, @jira_cloud)
|
402
405
|
jira_issue = @jira_api.retrieve_issue(response_data['self'])
|
403
406
|
jira_issue['short_name'] = raw_issue_data['short_name']
|
404
407
|
|
@@ -608,7 +611,7 @@ module Tefoji
|
|
608
611
|
issue_key = jira_issue_data['key']
|
609
612
|
watchers = raw_issue_data[deferred_tag].value
|
610
613
|
watchers.each do |watcher|
|
611
|
-
@jira_api.add_watcher(issue_key, watcher)
|
614
|
+
@jira_api.add_watcher(issue_key, watcher, @jira_cloud)
|
612
615
|
@logger.info '%14s: watched by %s' % [issue_key, watcher]
|
613
616
|
end
|
614
617
|
end
|
@@ -641,12 +644,17 @@ module Tefoji
|
|
641
644
|
def user_function_call(function_definition)
|
642
645
|
@logger.debug "function_definition is: #{function_definition}"
|
643
646
|
function_name = function_definition['name']
|
644
|
-
|
645
|
-
|
647
|
+
argument = function_definition['argument']
|
648
|
+
arguments = function_definition['arguments']
|
649
|
+
|
650
|
+
if argument.is_a?(Array) || arguments.is_a?(Array)
|
651
|
+
arguments = argument || function_definition['arguments']
|
652
|
+
function_arguments = arguments.map do |a|
|
646
653
|
expand_right_value(a).value
|
647
654
|
end
|
648
|
-
elsif
|
649
|
-
|
655
|
+
elsif argument.is_a?(String) || arguments.is_a?(String)
|
656
|
+
argument = function_definition['argument'] || function_definition['arguments']
|
657
|
+
function_arguments = [expand_right_value(argument).value]
|
650
658
|
else
|
651
659
|
fatal("No arguments supplied to function \"#{function_name}\"")
|
652
660
|
end
|
@@ -814,6 +822,124 @@ module Tefoji
|
|
814
822
|
return true
|
815
823
|
end
|
816
824
|
|
825
|
+
# Iterates through the assignees in a template and checks if they exist on jira. If any assignee in
|
826
|
+
# the template does not exist, it will fail, outputting a message saying which users failed and
|
827
|
+
# what epics/issues they are associated with.
|
828
|
+
# NOTE: This check does not guarantee a user is assignable, so there may be cases when a user passes
|
829
|
+
# the check but still fails when being assigned a ticket/epic. If we find a reasonable way to
|
830
|
+
# check assignability prior to the epic/issue being created, we should implement it in place of
|
831
|
+
# using the get_username method.
|
832
|
+
def check_assignees(main_template_data)
|
833
|
+
valid_users = []
|
834
|
+
|
835
|
+
invalid_users_to_features = {}
|
836
|
+
main_template_data['features']&.each do |feature|
|
837
|
+
next unless feature['assignee']
|
838
|
+
|
839
|
+
assignee_username = get_username_from_assignee(feature['assignee'])
|
840
|
+
next if valid_users.include?(assignee_username)
|
841
|
+
|
842
|
+
update_user_validity(assignee_username, feature['summary'], valid_users, invalid_users_to_features)
|
843
|
+
end
|
844
|
+
|
845
|
+
if main_template_data.dig('feature', 'assignee')
|
846
|
+
assignee_username = get_username_from_assignee(main_template_data['feature']['assignee'])
|
847
|
+
unless valid_users.include?(assignee_username)
|
848
|
+
update_user_validity(assignee_username, main_template_data['feature']['summary'], valid_users,
|
849
|
+
invalid_users_to_features)
|
850
|
+
end
|
851
|
+
end
|
852
|
+
|
853
|
+
invalid_users_to_epics = {}
|
854
|
+
main_template_data['epics']&.each do |epic|
|
855
|
+
next unless epic['assignee']
|
856
|
+
|
857
|
+
assignee_username = get_username_from_assignee(epic['assignee'])
|
858
|
+
next if valid_users.include?(assignee_username)
|
859
|
+
|
860
|
+
update_user_validity(assignee_username, epic['summary'], valid_users, invalid_users_to_epics)
|
861
|
+
end
|
862
|
+
|
863
|
+
if main_template_data.dig('epic', 'assignee')
|
864
|
+
assignee_username = get_username_from_assignee(main_template_data['epic']['assignee'])
|
865
|
+
unless valid_users.include?(assignee_username)
|
866
|
+
update_user_validity(assignee_username, main_template_data['epic']['summary'], valid_users,
|
867
|
+
invalid_users_to_epics)
|
868
|
+
end
|
869
|
+
end
|
870
|
+
|
871
|
+
invalid_users_to_issue_defaults = {}
|
872
|
+
if main_template_data.dig('issue_defaults', 'assignee')
|
873
|
+
assignee_username = get_username_from_assignee(main_template_data['issue_defaults']['assignee'])
|
874
|
+
unless valid_users.include?(assignee_username)
|
875
|
+
update_user_validity(assignee_username, 'issue_defaults', valid_users, invalid_users_to_issue_defaults)
|
876
|
+
end
|
877
|
+
end
|
878
|
+
|
879
|
+
invalid_users_to_issues = {}
|
880
|
+
main_template_data['issues'].each do |issue|
|
881
|
+
next unless issue['assignee']
|
882
|
+
|
883
|
+
assignee_username = get_username_from_assignee(issue['assignee'])
|
884
|
+
next if valid_users.include?(assignee_username)
|
885
|
+
|
886
|
+
update_user_validity(assignee_username, issue['summary'], valid_users, invalid_users_to_issues)
|
887
|
+
end
|
888
|
+
unless invalid_users_to_features.empty? &&
|
889
|
+
invalid_users_to_epics.empty? &&
|
890
|
+
invalid_users_to_issue_defaults.empty? &&
|
891
|
+
invalid_users_to_issues.empty?
|
892
|
+
invalid_user_message = "Invalid assignees:\n"
|
893
|
+
end
|
894
|
+
unless invalid_users_to_features.empty?
|
895
|
+
invalid_users_to_features.each do |assignee, feature|
|
896
|
+
invalid_user_message += "Assignee #{assignee} associated with features #{feature}\n"
|
897
|
+
end
|
898
|
+
end
|
899
|
+
unless invalid_users_to_epics.empty?
|
900
|
+
invalid_users_to_epics.each do |assignee, epic|
|
901
|
+
invalid_user_message += "Assignee #{assignee} associated with epics #{epic}\n"
|
902
|
+
end
|
903
|
+
end
|
904
|
+
unless invalid_users_to_issue_defaults.empty?
|
905
|
+
invalid_users_to_issue_defaults.each do |assignee, _|
|
906
|
+
invalid_user_message += "Assignee #{assignee} associated with issue_defaults\n"
|
907
|
+
end
|
908
|
+
end
|
909
|
+
unless invalid_users_to_issues.empty?
|
910
|
+
invalid_users_to_issues.each do |assignee, issues|
|
911
|
+
invalid_user_message += "Assignee #{assignee} associated with issues #{issues}\n"
|
912
|
+
end
|
913
|
+
end
|
914
|
+
fatal invalid_user_message if invalid_user_message
|
915
|
+
end
|
916
|
+
|
917
|
+
def get_username_from_assignee(assignee)
|
918
|
+
# Assignee is a variable or direct input
|
919
|
+
username = expand_string(assignee) if assignee.is_a?(String)
|
920
|
+
# Assignee is a function
|
921
|
+
if assignee.is_a?(Hash) && assignee.key?('function')
|
922
|
+
username = user_function_call(assignee['function'])
|
923
|
+
end
|
924
|
+
|
925
|
+
unless username
|
926
|
+
fatal "invalid assignee format for issue: #{issue['short_name']} failed: must be a function or a string"
|
927
|
+
end
|
928
|
+
|
929
|
+
username = username.value unless username.is_a?(String)
|
930
|
+
username
|
931
|
+
end
|
932
|
+
|
933
|
+
def update_user_validity(username, issue_name, valid_users, invalid_users)
|
934
|
+
if invalid_users.key?(username)
|
935
|
+
invalid_users[username] += [issue_name]
|
936
|
+
elsif @jira_api.get_username(username, @jira_cloud, false)
|
937
|
+
valid_users << username
|
938
|
+
else
|
939
|
+
invalid_users[username] = invalid_users.fetch(username, []) + [issue_name]
|
940
|
+
end
|
941
|
+
end
|
942
|
+
|
817
943
|
def logger_initialize(log_location)
|
818
944
|
logger = Logger.new(log_location, progname: 'tefoji', level: @log_level)
|
819
945
|
logger.formatter = proc do |severity, _, script_name, message|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tefoji
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Puppet Labs
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry-byebug
|