tefoji 1.0.9 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8279bb8ebda1af1e674da5c4d16932b790e69210a5c163563746a80d44aca92b
4
- data.tar.gz: f17bbc221ed1ec7c4fde90d2e64caddb45078ee0ee2d5cb59e7226d90df14317
3
+ metadata.gz: b048f1a4325a22d2fa736626f148827792639424d448ccc0d5a09250e250bdfa
4
+ data.tar.gz: 8350c3e8d0fdf3ad2ecfdf922ef654b37ddd86f9e44b4358cc713bf54cb462b8
5
5
  SHA512:
6
- metadata.gz: d790551fe749db5eea41ce6889479c3f23e8278ad94744b8be4561d0cceadb33b609949c612224e49bbc21587b8b3cf50501a6eeef1e2d3059c860420e583c15
7
- data.tar.gz: a0b4bd8875027f141a87f2bc960c43143813500db372e0764df428896fc04f34fd3f5b91d3a4d4ca305e9949b88dd3329f4363a1981905f6b4b7d7fe6c7b9fe9
6
+ metadata.gz: fd6187add4fe25d2ea7ebcc3aadc8346cfbfeae133e1fc4c366dad99395cfcbddd0aa6c024caa9f86676651037dcacdf0f1092a98ab0a2d1cc74a9033ab7dd7c
7
+ data.tar.gz: 2d5fe6518a6a174aeb033e006fa92b168f5115bb4526cb7dbbce2b177ac185bb79b795fcf7e43e86e63f063150bb5e5e6ad2fee28f17251010bbe2d319efd4c7
@@ -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',
@@ -50,14 +49,13 @@ module UserFunctions
50
49
  OPERATIONS: 'OPS',
51
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',
data/lib/tefoji/cli.rb CHANGED
@@ -6,8 +6,11 @@ require 'yaml'
6
6
  module Tefoji
7
7
  class CLI
8
8
  DEFAULT_JIRA_URL = 'https://tickets.puppetlabs.com'
9
- JIRA_TEST_URL = 'https://jira-app-dev-1.ops.puppetlabs.net'
9
+ JIRA_TEST_URL = 'https://jira-app-test-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.
@@ -47,19 +50,24 @@ module Tefoji
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
- if File.file?(jira_auth_file)
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
- @user_options['--jira'] = JIRA_TEST_URL if @user_options['--jira-test']
62
- @user_options['--jira'] = DEFAULT_JIRA_URL if @user_options['--jira'].nil?
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
@@ -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'
@@ -21,7 +23,6 @@ module Tefoji
21
23
 
22
24
  ## Issue type constants
23
25
  ISSUE_EPIC = 'Epic'
24
- ISSUE_FEATURE = 'Feature'
25
26
  ISSUE_NEW_FEATURE = 'New Feature'
26
27
  ISSUE_SUB_TASK = 'Sub-task'
27
28
  ISSUE_TASK = 'Task'
@@ -74,20 +75,26 @@ module Tefoji
74
75
 
75
76
  # Do this so we can inform the user quickly that their credentials didn't work
76
77
  # There may be a better test, but this is the one the original Winston uses
77
- def test_authentication
78
- get_username(@jira_username)
78
+ def test_authentication(jira_cloud)
79
+ get_username(@jira_username, jira_cloud)
79
80
  rescue RestClient::Forbidden
80
81
  fatal 'Forbidden: either the authentication is incorrect or ' \
81
82
  'Jira might be requiring a CAPTCHA response from the web interface.'
82
83
  end
83
84
 
84
85
  # Get information about user in Jira
85
- def get_username(username, fail_if_not_found = true)
86
- jira_get("user?username=#{username}", fail_if_not_found)
86
+ def get_username(username, jira_cloud, fail_if_not_found = true)
87
+ search_parameters = if jira_cloud
88
+ "user/search?query=#{username}"
89
+ else
90
+ "user?username=#{username}"
91
+ end
92
+
93
+ jira_get(search_parameters, fail_if_not_found)
87
94
  end
88
95
 
89
96
  # Save authentication YAML to the a file for reuse.
90
- def save_authentication(save_file_name)
97
+ def save_authentication(save_file_name, jira_cloud)
91
98
  backup_file_name = "#{save_file_name}.bak"
92
99
 
93
100
  if File.readable?(save_file_name)
@@ -95,18 +102,22 @@ module Tefoji
95
102
  @logger.info "Saved #{save_file_name} to #{backup_file_name}"
96
103
  end
97
104
 
98
- File.open(save_file_name, 'w') do |f|
99
- f.puts "jira-auth: #{@jira_auth_string}"
100
- end
105
+ jira_auth_string = if jira_cloud
106
+ "jira-auth: #{@jira_auth_string}\n"
107
+ else
108
+ "jira-cloud-auth: #{@jira_auth_string}\n"
109
+ end
110
+ File.write(save_file_name, jira_auth_string)
111
+
101
112
  File.chmod(0o600, save_file_name)
102
113
 
103
114
  @logger.info "Saved Jira authentication to #{save_file_name}"
104
115
  end
105
116
 
106
117
  # Create a Jira issue
107
- def create_issue(issue_data)
118
+ def create_issue(issue_data, jira_cloud)
108
119
  request_path = 'issue'
109
- jira_fields = issue_data_to_jira_fields(issue_data)
120
+ jira_fields = issue_data_to_jira_fields(issue_data, jira_cloud)
110
121
  jira_post(request_path, jira_fields)
111
122
  rescue RestClient::Forbidden
112
123
  fatal "Forbidden: could not send #{request_path} request to Jira. "\
@@ -160,12 +171,25 @@ module Tefoji
160
171
  end
161
172
 
162
173
  # https://www.youtube.com/watch?v=JsntlJZ9h1U
163
- def add_watcher(issue_key, watcher)
174
+ def add_watcher(issue_key, watcher, jira_cloud)
164
175
  request_path = "issue/#{issue_key}/watchers"
176
+ watcher = useremail_to_id(watcher) if jira_cloud
165
177
  request_data = watcher
166
178
  jira_post(request_path, request_data)
167
179
  end
168
180
 
181
+ # jira cloud api calls now require an accountId rather than a username. This method will convert
182
+ # emails or display names to accountIds.
183
+ def useremail_to_id(email)
184
+ response = get_username(email, true, true)[0]
185
+ id = response['accountId']
186
+ if id.nil? || id.empty?
187
+ @logger.error "accountId not found for #{email}."
188
+ exit 1
189
+ end
190
+ return id
191
+ end
192
+
169
193
  private
170
194
 
171
195
  def jira_get(jira_request_path, fail_if_not_found = true)
@@ -177,7 +201,6 @@ module Tefoji
177
201
  url = "#{@jira_base_rest_url}/#{jira_request_path}"
178
202
  end
179
203
  headers = @authentication_header
180
-
181
204
  begin
182
205
  response = RestClient.get(url, headers)
183
206
  rescue RestClient::MovedPermanently,
@@ -244,7 +267,7 @@ module Tefoji
244
267
  # Provide the needed translation of user-created data to required format
245
268
  # of Jira requests. This is mostly a list of fussing with the Jira field
246
269
  # names.
247
- def issue_data_to_jira_fields(issue_data)
270
+ def issue_data_to_jira_fields(issue_data, jira_cloud)
248
271
  # Check to ensure we have what we need to create a issue
249
272
 
250
273
  unless issue_data['summary']
@@ -257,64 +280,130 @@ module Tefoji
257
280
  end
258
281
 
259
282
  # build the jira_fields hash describing the issue
260
-
261
283
  # These are required for all issues
262
284
  jira_fields = {
263
285
  'summary' => issue_data['summary'],
264
286
  'project' => { 'key' => issue_data['project'] }
265
287
  }
266
288
 
289
+ set_common_jira_fields(issue_data, jira_fields)
290
+
291
+ if jira_cloud
292
+ set_cloud_jira_fields(issue_data, jira_fields)
293
+ else
294
+ set_onprem_jira_fields(issue_data, jira_fields)
295
+ end
296
+
297
+ return { 'fields' => jira_fields }
298
+ end
299
+
300
+ def set_common_jira_fields(issue_data, jira_fields)
267
301
  # The following are optional
268
302
  if issue_data['description']
269
303
  jira_fields['description'] = issue_data['description']
270
304
  end
305
+
306
+ if issue_data['labels']
307
+ labels = issue_data['labels'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
308
+ jira_fields['labels'] = labels unless labels.empty?
309
+ end
310
+ if issue_data['duedate']
311
+ jira_fields['duedate'] = issue_data['duedate']
312
+ end
313
+ if issue_data['fix_version']
314
+ jira_fields['fixVersions'] = [issue_data['fix_version']]
315
+ end
316
+
317
+ if issue_data['components']
318
+ components = issue_data[:components].to_a.flatten.reject { |t| t =~ /^\s*$/ }
319
+ unless components.empty?
320
+ jira_fields['components'] = components.map { |component| { FIELD_NAME => component } }
321
+ end
322
+ end
323
+
324
+ # Default issue type to ISSUE_TASK if it isn't already set
325
+ jira_fields['issuetype'] = { FIELD_NAME => ISSUE_TASK }
326
+
327
+ security = ENV['SECURITY'] || issue_data['security']
328
+ return unless security
329
+
330
+ case security.downcase
331
+ when 'confidential'
332
+ jira_fields['security'] = { FIELD_ID => '10002' }
333
+ when 'internal'
334
+ jira_fields['security'] = { FIELD_ID => '10001' }
335
+ when 'public'
336
+ # Nothing to do here - public is default
337
+ else
338
+ @logger.fatal "Unknown security type: #{security}"
339
+ exit 1
340
+ end
341
+ end
342
+
343
+ def set_cloud_jira_fields(issue_data, jira_fields)
271
344
  if issue_data['assignee']
272
- jira_fields['assignee'] = { FIELD_NAME => issue_data['assignee'] }
345
+ assignee = issue_data['assignee']
346
+ assignee = useremail_to_id(assignee)
347
+ jira_fields['assignee'] = { FIELD_ACCOUNT_ID => assignee }
273
348
  end
349
+
350
+ if issue_data['type']
351
+ jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
352
+ # If this is an epic, we need to add an epic name
353
+ if issue_data['type'].casecmp?(ISSUE_EPIC)
354
+ jira_fields['customfield_10011'] = issue_data['epic_name'] || issue_data['summary']
355
+ end
356
+ end
357
+
274
358
  if issue_data['story_points']
275
- jira_fields['customfield_10002'] = issue_data['story_points'].to_i
359
+ jira_fields['customfield_10038'] = issue_data['story_points'].to_i
276
360
  end
277
361
  if issue_data['team']
278
- jira_fields['customfield_14200'] = { FIELD_VALUE => issue_data['team'] }
362
+ jira_fields['customfield_10052'] = { FIELD_VALUE => issue_data['team'] }
279
363
  end
280
364
  if issue_data['teams']
281
365
  teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
282
366
  unless teams.empty?
283
- jira_fields['customfield_14201'] = teams.map { |team| { FIELD_VALUE => team } }
367
+ jira_fields['customfield_10066'] = teams.map { |team| { FIELD_VALUE => team } }
284
368
  end
285
369
  end
286
370
  if issue_data['subteam']
287
- jira_fields['customfield_11700'] = [issue_data['subteam']]
371
+ jira_fields['customfield_10045'] = [issue_data['subteam']]
288
372
  end
289
373
  if issue_data['sprint']
290
- jira_fields['customfield_10005'] = issue_data['sprint'].to_i
374
+ jira_fields['customfield_10020'] = issue_data['sprint'].to_i
291
375
  end
292
376
  if issue_data['acceptance']
293
- jira_fields['customfield_11501'] = issue_data['acceptance']
294
- end
295
- if issue_data['labels']
296
- labels = issue_data['labels'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
297
- jira_fields['labels'] = labels unless labels.empty?
298
- end
299
- if issue_data['duedate']
300
- jira_fields['duedate'] = issue_data['duedate']
301
- end
302
- if issue_data['fix_version']
303
- jira_fields['fixVersions'] = [issue_data['fix_version']]
377
+ jira_fields['customfield_10062'] = issue_data['acceptance']
304
378
  end
305
379
  if issue_data['release_notes']
306
- jira_fields['customfield_11100'] = { FIELD_VALUE => issue_data['release_notes'] }
380
+ jira_fields['customfield_10043'] = { FIELD_VALUE => issue_data['release_notes'] }
307
381
  end
308
382
 
309
- if issue_data['components']
310
- components = issue_data[:components].to_a.flatten.reject { |t| t =~ /^\s*$/ }
311
- unless components.empty?
312
- jira_fields['components'] = components.map { |component| { FIELD_NAME => component } }
383
+ # If a issue has a specified parent issue, prefer that. The parent issue *should* already
384
+ # be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
385
+ # either be an epic linked to the main epic or the main epic itself.
386
+
387
+ if issue_data['parent']
388
+ unless issue_data['type'].casecmp?(ISSUE_SUB_TASK) || !issue_data['type']
389
+ @logger.fatal "A issue with a parent must be classified as a Sub-issue\n\n#{issue_data}"
390
+ exit 1
391
+ end
392
+ jira_fields['issuetype'] = { FIELD_NAME => ISSUE_SUB_TASK }
393
+ jira_fields['parent'] = { FIELD_KEY => issue_data['parent'] }
394
+ elsif issue_data['epic_parent']
395
+ if issue_data['type'].casecmp?(ISSUE_SUB_TASK)
396
+ @logger.fatal "This issue cannot be a subtask of an epic\n\n#{issue_data}"
397
+ exit 1
313
398
  end
399
+ jira_fields['customfield_10018'] = issue_data['epic_parent']
314
400
  end
401
+ end
315
402
 
316
- # Default issue type to ISSUE_TASK if it isn't already set
317
- jira_fields['issuetype'] = { FIELD_NAME => ISSUE_TASK }
403
+ def set_onprem_jira_fields(issue_data, jira_fields)
404
+ if issue_data['assignee']
405
+ jira_fields['assignee'] = { FIELD_NAME => issue_data['assignee'] }
406
+ end
318
407
 
319
408
  if issue_data['type']
320
409
  jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
@@ -324,6 +413,31 @@ module Tefoji
324
413
  end
325
414
  end
326
415
 
416
+ if issue_data['story_points']
417
+ jira_fields['customfield_10002'] = issue_data['story_points'].to_i
418
+ end
419
+ if issue_data['team']
420
+ jira_fields['customfield_14200'] = { FIELD_VALUE => issue_data['team'] }
421
+ end
422
+ if issue_data['teams']
423
+ teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
424
+ unless teams.empty?
425
+ jira_fields['customfield_14201'] = teams.map { |team| { FIELD_VALUE => team } }
426
+ end
427
+ end
428
+ if issue_data['subteam']
429
+ jira_fields['customfield_11700'] = [issue_data['subteam']]
430
+ end
431
+ if issue_data['sprint']
432
+ jira_fields['customfield_10005'] = issue_data['sprint'].to_i
433
+ end
434
+ if issue_data['acceptance']
435
+ jira_fields['customfield_11501'] = issue_data['acceptance']
436
+ end
437
+ if issue_data['release_notes']
438
+ jira_fields['customfield_11100'] = { FIELD_VALUE => issue_data['release_notes'] }
439
+ end
440
+
327
441
  # If a issue has a specified parent issue, prefer that. The parent issue *should* already
328
442
  # be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
329
443
  # either be an epic linked to the main epic or the main epic itself.
@@ -342,23 +456,6 @@ module Tefoji
342
456
  end
343
457
  jira_fields['customfield_10006'] = issue_data['epic_parent']
344
458
  end
345
-
346
- security = ENV['SECURITY'] || issue_data['security']
347
- if security
348
- case security.downcase
349
- when 'confidential'
350
- jira_fields['security'] = { FIELD_ID => '10002' }
351
- when 'internal'
352
- jira_fields['security'] = { FIELD_ID => '10001' }
353
- when 'public'
354
- # Nothing to do here - public is default
355
- else
356
- @logger.fatal "Unknown security type: #{security}"
357
- exit 1
358
- end
359
- end
360
-
361
- return { 'fields' => jira_fields }
362
459
  end
363
460
  end
364
461
 
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 = {}
@@ -272,13 +273,13 @@ module Tefoji
272
273
  @jira_api.logger = @logger
273
274
 
274
275
  @jira_api.authenticate(@jira_url, @jira_user, @jira_auth_string)
275
- @jira_api.test_authentication unless @jira_mock
276
+ @jira_api.test_authentication(@jira_cloud) unless @jira_mock
276
277
  end
277
278
 
278
279
  # Save Jira auth data
279
280
  def save_authentication
280
281
  authenticate unless @jira_api
281
- @jira_api.save_authentication(@jira_auth_file)
282
+ @jira_api.save_authentication(@jira_auth_file, @jira_cloud)
282
283
  end
283
284
 
284
285
  private
@@ -297,8 +298,9 @@ module Tefoji
297
298
  feature_to_do = @template_data['feature']
298
299
 
299
300
  feature = variable_substitute(feature_to_do)
300
- feature['type'] = JiraApi::ISSUE_FEATURE
301
- @feature_issue = @jira_api.create_issue(feature)
301
+ feature['type'] = JiraApi::ISSUE_NEW_FEATURE
302
+
303
+ @feature_issue = @jira_api.create_issue(feature, @jira_cloud)
302
304
  @logger.info "Feature issue: #{@feature_issue['key']}"
303
305
  @feature_issue
304
306
  end
@@ -360,7 +362,7 @@ module Tefoji
360
362
  epic['security'] = 'internal'
361
363
  end
362
364
 
363
- epic_issue = @jira_api.create_issue(epic)
365
+ epic_issue = @jira_api.create_issue(epic, @jira_cloud)
364
366
  epic_issue['short_name'] = short_name
365
367
  @logger.info 'Epic: %16s [%s]' % [epic_issue['key'], short_name]
366
368
  @jira_api.link_issues(@feature_issue['key'], epic_issue['key']) if @feature_issue
@@ -400,7 +402,7 @@ module Tefoji
400
402
  jira_ready_data, raw_issue_data = prepare_jira_ready_data(issue, issue_defaults)
401
403
  next if jira_ready_data.nil? || raw_issue_data.nil?
402
404
 
403
- response_data = @jira_api.create_issue(jira_ready_data)
405
+ response_data = @jira_api.create_issue(jira_ready_data, @jira_cloud)
404
406
  jira_issue = @jira_api.retrieve_issue(response_data['self'])
405
407
  jira_issue['short_name'] = raw_issue_data['short_name']
406
408
 
@@ -610,7 +612,7 @@ module Tefoji
610
612
  issue_key = jira_issue_data['key']
611
613
  watchers = raw_issue_data[deferred_tag].value
612
614
  watchers.each do |watcher|
613
- @jira_api.add_watcher(issue_key, watcher)
615
+ @jira_api.add_watcher(issue_key, watcher, @jira_cloud)
614
616
  @logger.info '%14s: watched by %s' % [issue_key, watcher]
615
617
  end
616
618
  end
@@ -932,7 +934,7 @@ module Tefoji
932
934
  def update_user_validity(username, issue_name, valid_users, invalid_users)
933
935
  if invalid_users.key?(username)
934
936
  invalid_users[username] += [issue_name]
935
- elsif @jira_api.get_username(username, false)
937
+ elsif @jira_api.get_username(username, @jira_cloud, false)
936
938
  valid_users << username
937
939
  else
938
940
  invalid_users[username] = invalid_users.fetch(username, []) + [issue_name]
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.9
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet Labs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-01 00:00:00.000000000 Z
11
+ date: 2023-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry-byebug