tefoji 2.0.0 → 3.0.0
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 +104 -135
- data/lib/tefoji.rb +37 -38
- 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: 438ee8e0448c8a7805362ad3821f68f9c1ce6c50f06f4ad93aa1928d2a60516b
|
4
|
+
data.tar.gz: a6dd2a6d4f52ba6145d68889671ba6b0a95d36d34d3cf2c0a2fa9f11b426972f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 885c76aa6b283c24412fe5114397dae1375c68acce44186d421aa3be5d9e87e59e934d6d6cdb863d0bc17434bdabd3eee50c281de0bee50297ea4e1e936d0f2b
|
7
|
+
data.tar.gz: 434262e7a761ab55076bd4e53d679bd7a3f45154803d21409600456998bad4e49dd68742bf753ccb92e0b2556a7740e26c6cfea724393b3396fed4647e3aede1
|
@@ -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
|
@@ -329,49 +339,58 @@ module Tefoji
|
|
329
339
|
|
330
340
|
# Backward compatiblity: translate 'team' to 'scrum_team'
|
331
341
|
scrum_team = issue_data['scrum_team'] || issue_data['team']
|
342
|
+
|
332
343
|
if scrum_team
|
333
|
-
|
334
|
-
|
335
|
-
else
|
336
|
-
jira_fields['customfield_11500'] = { FIELD_VALUE => scrum_team }
|
337
|
-
end
|
344
|
+
field_name, field_value = scrum_team_rules(issue_data, scrum_team)
|
345
|
+
jira_fields[field_name] = field_value
|
338
346
|
end
|
339
347
|
|
340
|
-
#
|
341
|
-
# issues take only the first of the list
|
348
|
+
# Backward compatiblity: translate 'teams' to 'scrum_teams'
|
342
349
|
scrum_teams = issue_data['scrum_teams'] || issue_data['teams']
|
350
|
+
|
343
351
|
if scrum_teams
|
344
|
-
|
345
|
-
|
346
|
-
jira_fields['components'] = processed_teams.map { |team| { FIELD_NAME => team } }
|
347
|
-
else
|
348
|
-
jira_fields['customfield_11500'] = { FIELD_VALUE => processed_teams.first }
|
349
|
-
end
|
352
|
+
field_name, field_value = scrum_teams_rules(issue_data, scrum_team)
|
353
|
+
jira_fields[field_name] = field_value
|
350
354
|
end
|
351
355
|
|
352
356
|
# Default issue type to ISSUE_TASK if it isn't already set
|
353
357
|
jira_fields['issuetype'] = { FIELD_NAME => ISSUE_TASK }
|
358
|
+
end
|
354
359
|
|
355
|
-
|
356
|
-
|
360
|
+
# There are now some funky rules for setting the 'scrum_team' (previously 'team')
|
361
|
+
# and 'scrum_teams' field in different issues.
|
362
|
+
#
|
363
|
+
# For epics, put the scrum team in the 'components' field.
|
364
|
+
# Except for a couple of projects, put the scrum team in customfield_11500
|
365
|
+
# For Puppet Agent (PA) and Puppet Server (SERVER) projects, put the scrum team in
|
366
|
+
# customfield_14200
|
367
|
+
def scrum_team_rules(issue_data, scrum_team)
|
368
|
+
if epic?(issue_data)
|
369
|
+
return ['components', [{ FIELD_NAME => scrum_team }]]
|
370
|
+
end
|
357
371
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
when 'internal'
|
362
|
-
jira_fields['security'] = { FIELD_ID => '10001' }
|
363
|
-
when 'public'
|
364
|
-
# Nothing to do here - public is default
|
365
|
-
else
|
366
|
-
@logger.fatal "Unknown security type: #{security}"
|
367
|
-
exit 1
|
372
|
+
customfield = 'customfield_11500'
|
373
|
+
if %w[PA SERVER].include?(issue_data['project'].value)
|
374
|
+
customfield = 'customfield_14200'
|
368
375
|
end
|
376
|
+
|
377
|
+
return [customfield, { FIELD_VALUE => scrum_team }]
|
378
|
+
end
|
379
|
+
|
380
|
+
def scrum_teams_rules(issue_data, scrum_teams)
|
381
|
+
processed_teams = scrum_teams.to_a.flatten.reject { |t| t =~ %r{^\s*$} }
|
382
|
+
if epic?(issue_data)
|
383
|
+
return ['components', processed_teams.map { |team| { FIELD_NAME => team } }]
|
384
|
+
end
|
385
|
+
|
386
|
+
# For ordinary issues we can only set one team.
|
387
|
+
scrum_team_rules(issue_data, processed_teams.first)
|
369
388
|
end
|
370
389
|
|
371
390
|
def set_cloud_jira_fields(issue_data, jira_fields)
|
372
391
|
if issue_data['assignee']
|
373
392
|
assignee = issue_data['assignee']
|
374
|
-
assignee =
|
393
|
+
assignee = user_email_to_account_id(assignee.value)
|
375
394
|
jira_fields['assignee'] = { FIELD_ACCOUNT_ID => assignee }
|
376
395
|
end
|
377
396
|
|
@@ -390,7 +409,7 @@ module Tefoji
|
|
390
409
|
jira_fields['customfield_10052'] = { FIELD_VALUE => issue_data['team'] }
|
391
410
|
end
|
392
411
|
if issue_data['teams']
|
393
|
-
teams = issue_data['teams'].to_a.flatten.reject { |t| t =~
|
412
|
+
teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ %r{^\s*$} }
|
394
413
|
unless teams.empty?
|
395
414
|
jira_fields['customfield_10066'] = teams.map { |team| { FIELD_VALUE => team } }
|
396
415
|
end
|
@@ -427,56 +446,6 @@ module Tefoji
|
|
427
446
|
jira_fields['customfield_10018'] = issue_data['epic_parent']
|
428
447
|
end
|
429
448
|
end
|
430
|
-
|
431
|
-
def set_onprem_jira_fields(issue_data, jira_fields)
|
432
|
-
if issue_data['assignee']
|
433
|
-
jira_fields['assignee'] = { FIELD_NAME => issue_data['assignee'] }
|
434
|
-
end
|
435
|
-
|
436
|
-
if issue_data['type']
|
437
|
-
jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
|
438
|
-
# If this is an epic, we need to add an epic name
|
439
|
-
if epic?(issue_data)
|
440
|
-
jira_fields['customfield_10007'] = issue_data['epic_name'] || issue_data['summary']
|
441
|
-
end
|
442
|
-
end
|
443
|
-
|
444
|
-
if issue_data['story_points']
|
445
|
-
jira_fields['customfield_10002'] = issue_data['story_points'].to_i
|
446
|
-
end
|
447
|
-
|
448
|
-
if issue_data['subteam']
|
449
|
-
jira_fields['customfield_11700'] = [issue_data['subteam']]
|
450
|
-
end
|
451
|
-
if issue_data['sprint']
|
452
|
-
jira_fields['customfield_10005'] = issue_data['sprint'].to_i
|
453
|
-
end
|
454
|
-
if issue_data['acceptance']
|
455
|
-
jira_fields['customfield_11501'] = issue_data['acceptance']
|
456
|
-
end
|
457
|
-
if issue_data['release_notes']
|
458
|
-
jira_fields['customfield_11100'] = { FIELD_VALUE => issue_data['release_notes'] }
|
459
|
-
end
|
460
|
-
|
461
|
-
# If a issue has a specified parent issue, prefer that. The parent issue *should* already
|
462
|
-
# be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
|
463
|
-
# either be an epic linked to the main epic or the main epic itself.
|
464
|
-
|
465
|
-
if issue_data['parent']
|
466
|
-
unless issue_data['type'].casecmp?(ISSUE_SUB_TASK) || !issue_data['type']
|
467
|
-
@logger.fatal "A issue with a parent must be classified as a Sub-issue\n\n#{issue_data}"
|
468
|
-
exit 1
|
469
|
-
end
|
470
|
-
jira_fields['issuetype'] = { FIELD_NAME => ISSUE_SUB_TASK }
|
471
|
-
jira_fields['parent'] = { FIELD_KEY => issue_data['parent'] }
|
472
|
-
elsif issue_data['epic_parent']
|
473
|
-
if issue_data['type'].casecmp?(ISSUE_SUB_TASK)
|
474
|
-
@logger.fatal "This issue cannot be a subtask of an epic\n\n#{issue_data}"
|
475
|
-
exit 1
|
476
|
-
end
|
477
|
-
jira_fields['customfield_10006'] = issue_data['epic_parent']
|
478
|
-
end
|
479
|
-
end
|
480
449
|
end
|
481
450
|
|
482
451
|
class JiraMockApi < JiraApi
|
@@ -486,20 +455,20 @@ module Tefoji
|
|
486
455
|
@summary_count = 0
|
487
456
|
end
|
488
457
|
|
489
|
-
def authenticate(jira_base_url, username = nil,
|
458
|
+
def authenticate(jira_base_url, username = nil, jira_auth = nil)
|
490
459
|
@jira_base_url = jira_base_url
|
491
460
|
@jira_base_rest_url = "#{jira_base_url}/rest/api/2"
|
492
461
|
|
493
462
|
@logger.info "Using mock of Jira instance: \"#{jira_base_url}\""
|
494
463
|
|
495
|
-
if
|
464
|
+
if jira_auth.nil?
|
496
465
|
if username.nil?
|
497
466
|
print 'Enter jira user name: '
|
498
467
|
username = $stdin.gets.chomp
|
499
468
|
end
|
500
469
|
else
|
501
|
-
|
502
|
-
username =
|
470
|
+
plain_auth = Base64.decode64(jira_auth)
|
471
|
+
username = plain_auth.split(':')[0]
|
503
472
|
end
|
504
473
|
|
505
474
|
@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
|
@@ -906,7 +905,7 @@ module Tefoji
|
|
906
905
|
def update_user_validity(username, issue_name, valid_users, invalid_users)
|
907
906
|
if invalid_users.key?(username)
|
908
907
|
invalid_users[username] += [issue_name]
|
909
|
-
elsif @jira_api.get_username(username,
|
908
|
+
elsif @jira_api.get_username(username, false)
|
910
909
|
valid_users << username
|
911
910
|
else
|
912
911
|
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.0
|
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-17 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: []
|