tefoji 1.0.8 → 1.0.10
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/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
|