tefoji 2.1.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 +71 -126
- 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
|
|
@@ -345,21 +355,6 @@ module Tefoji
|
|
|
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')
|
|
@@ -383,7 +378,7 @@ module Tefoji
|
|
|
383
378
|
end
|
|
384
379
|
|
|
385
380
|
def scrum_teams_rules(issue_data, scrum_teams)
|
|
386
|
-
processed_teams = scrum_teams.to_a.flatten.reject { |t| t =~
|
|
381
|
+
processed_teams = scrum_teams.to_a.flatten.reject { |t| t =~ %r{^\s*$} }
|
|
387
382
|
if epic?(issue_data)
|
|
388
383
|
return ['components', processed_teams.map { |team| { FIELD_NAME => team } }]
|
|
389
384
|
end
|
|
@@ -395,7 +390,7 @@ module Tefoji
|
|
|
395
390
|
def set_cloud_jira_fields(issue_data, jira_fields)
|
|
396
391
|
if issue_data['assignee']
|
|
397
392
|
assignee = issue_data['assignee']
|
|
398
|
-
assignee =
|
|
393
|
+
assignee = user_email_to_account_id(assignee.value)
|
|
399
394
|
jira_fields['assignee'] = { FIELD_ACCOUNT_ID => assignee }
|
|
400
395
|
end
|
|
401
396
|
|
|
@@ -414,7 +409,7 @@ module Tefoji
|
|
|
414
409
|
jira_fields['customfield_10052'] = { FIELD_VALUE => issue_data['team'] }
|
|
415
410
|
end
|
|
416
411
|
if issue_data['teams']
|
|
417
|
-
teams = issue_data['teams'].to_a.flatten.reject { |t| t =~
|
|
412
|
+
teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ %r{^\s*$} }
|
|
418
413
|
unless teams.empty?
|
|
419
414
|
jira_fields['customfield_10066'] = teams.map { |team| { FIELD_VALUE => team } }
|
|
420
415
|
end
|
|
@@ -451,56 +446,6 @@ module Tefoji
|
|
|
451
446
|
jira_fields['customfield_10018'] = issue_data['epic_parent']
|
|
452
447
|
end
|
|
453
448
|
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
449
|
end
|
|
505
450
|
|
|
506
451
|
class JiraMockApi < JiraApi
|
|
@@ -510,20 +455,20 @@ module Tefoji
|
|
|
510
455
|
@summary_count = 0
|
|
511
456
|
end
|
|
512
457
|
|
|
513
|
-
def authenticate(jira_base_url, username = nil,
|
|
458
|
+
def authenticate(jira_base_url, username = nil, jira_auth = nil)
|
|
514
459
|
@jira_base_url = jira_base_url
|
|
515
460
|
@jira_base_rest_url = "#{jira_base_url}/rest/api/2"
|
|
516
461
|
|
|
517
462
|
@logger.info "Using mock of Jira instance: \"#{jira_base_url}\""
|
|
518
463
|
|
|
519
|
-
if
|
|
464
|
+
if jira_auth.nil?
|
|
520
465
|
if username.nil?
|
|
521
466
|
print 'Enter jira user name: '
|
|
522
467
|
username = $stdin.gets.chomp
|
|
523
468
|
end
|
|
524
469
|
else
|
|
525
|
-
|
|
526
|
-
username =
|
|
470
|
+
plain_auth = Base64.decode64(jira_auth)
|
|
471
|
+
username = plain_auth.split(':')[0]
|
|
527
472
|
end
|
|
528
473
|
|
|
529
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: []
|