tefoji 2.1.0 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|