tefoji 1.0.7

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 26059a47709ee9c7a4c33054c4da05d66b43e77cd3042497ffa7f7f48ccc2831
4
+ data.tar.gz: fe60fc5a310f2147e07c050483bb4a86665ef697396afbf8027ada79a7f25e48
5
+ SHA512:
6
+ metadata.gz: 29eb974c65570687064562c4ca3a6acf1b5fdefe3a75a913003fe3ee6bbf930f0bac749821705817e7d6fd8e4e00b51fdc81d937d4ddd62fe5038b291aeef22d
7
+ data.tar.gz: f115c57f3ba7c00ee7ae24f581d544da201964cb5a896080f4b2d92f132d0522a350cf340ab43a33eebf4933f3f6f087129f4d88bd4983f6cfc5ca1e84a80935
data/exe/tefoji ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tefoji'
4
+
5
+ Tefoji::CLI.new(ARGV).run
@@ -0,0 +1,9 @@
1
+ # Handles all logging output for Tefoji
2
+ module Logging
3
+ # Another cheap hack to wrap 'logger' above. Called as 'fatal' rather than 'logger.fatal'
4
+ # directly, does the exit for us so we can be lazy.
5
+ def fatal(message)
6
+ logger.fatal(message)
7
+ exit 1
8
+ end
9
+ end
@@ -0,0 +1,319 @@
1
+ module UserFunctions
2
+ ##
3
+ ## These are functions defined to be called from within the yaml files, thus the funny
4
+ ## calling signatures. The functions are called with a simple array of arguments.
5
+ ## It is up to the function to unwind those arguments.
6
+ ##
7
+ ## It is essential that the user functions remain as spare as possible. Pure functions
8
+ ## are best, but 'prompt' violates that already.
9
+ ##
10
+
11
+ # A bit of paranoia that lists the allowable function names so that arbitrary
12
+ # function calls aren't permitted from the YAML files.
13
+ def user_function_allowlist
14
+ %w[
15
+ chooser
16
+ conditional
17
+ date_after
18
+ date_before
19
+ default
20
+ jira_project
21
+ jira_sprint
22
+ jira_status
23
+ jira_team
24
+ jira_user
25
+ n_digit_version
26
+ prompt
27
+ prompt_only
28
+ random
29
+ release_type
30
+ split
31
+ ]
32
+ end
33
+
34
+ # Maps of Winston-style Jira names (projects, sprints, to the actual assets
35
+ def jira_projects
36
+ {
37
+ BOLT: 'BOLT',
38
+ BUILD_TOOLS: 'BUILD',
39
+ CLIENT_TOOLS: 'CT',
40
+ CODE_MANAGEMENT: 'CODEMGMT',
41
+ COMMUNITY_PACKAGE_REPO: 'CPR',
42
+ DOC: 'DOC',
43
+ DOCS: 'DOC',
44
+ EZBAKE: 'EZ',
45
+ FACTER: 'FACT',
46
+ FORGE_INTERNAL: 'PF',
47
+ IMAGES: 'IMAGES',
48
+ MODULES: 'MODULES',
49
+ MODULES_INTERNAL: 'FM',
50
+ OPERATIONS: 'OPS',
51
+ PDK: 'PDK',
52
+ PE_INTERNAL: 'PE',
53
+ PROJECT_CENTRAL: 'PC',
54
+ PUPPETDB: 'PDB',
55
+ PUPPETSERVER: 'SERVER',
56
+ PUPPET_AGENT: 'PA',
57
+ PUPPET: 'PUP',
58
+ QUALITY_ENGINEERING: 'QENG',
59
+ DIO: 'DIO',
60
+ RAZOR: 'RAZOR',
61
+ RELEASE_ENGINEERING: 'RE',
62
+ SLV: 'SLV',
63
+ SUPPORT: 'SUP',
64
+ VANAGON: 'VANAGON',
65
+ WEB: 'WWM',
66
+ WHO: 'WHO'
67
+ }
68
+ end
69
+
70
+ def jira_sprints
71
+ {
72
+ RE_KANBAN: 3150,
73
+ DUMPLING_READY_TO_WORK: 5515
74
+ }
75
+ end
76
+
77
+ def jira_statuses
78
+ {
79
+ READY_FOR_ENGINEERING: 111,
80
+ ACCEPTED: 271,
81
+ DEVELOPING_FEATURE: 61,
82
+ DEVELOPING_EPIC: 41
83
+ }
84
+ end
85
+
86
+ def jira_teams
87
+ {
88
+ BOLT: 'Bolt',
89
+ CD4PE: 'CD4PE',
90
+ CODE_MANAGEMENT: 'Dumpling',
91
+ DUMPLING: 'Dumpling',
92
+ FACTER: "Phoenix",
93
+ INSTALLER: 'Installer and Management',
94
+ NETWORKING: 'Network Automation',
95
+ OPERATIONS: 'Operations',
96
+ PE: 'Dumpling',
97
+ PLATFORM_OS: "Phoenix",
98
+ PUPPETDB: 'Dumpling',
99
+ PUPPETSERVER: 'Dumpling',
100
+ QE: 'Quality Engineering',
101
+ RELEASE_ENGINEERING: 'Release Engineering',
102
+ SKELETOR: 'Skeletor',
103
+ SLV: 'System Level Validation'
104
+ }
105
+ end
106
+
107
+ def jira_predeclared_variables
108
+ variables = {}
109
+
110
+ projects = jira_projects.transform_keys { |k| "#{k.downcase}_project".to_sym }
111
+ sprints = jira_sprints.transform_keys { |k| "#{k.downcase}_sprint".to_sym }
112
+ statuses = jira_statuses.transform_keys { |k| "#{k.downcase}_status".to_sym }
113
+ teams = jira_teams.transform_keys { |k| "#{k.downcase}_team".to_sym }
114
+
115
+ variables.merge(**projects, **sprints, **statuses, **teams)
116
+ end
117
+
118
+ # Takes at least two arguments.
119
+ # The first is a string to match against.
120
+ # The remainders are single key -> value hashes. If the string matches the key, the value
121
+ # is returned.
122
+ #
123
+ # Returns '' if nothing matches.
124
+ def chooser(args)
125
+ _fail_if_unset(__method__.to_s, args)
126
+ match_string = args[0]
127
+ args[1..].each do |match_case|
128
+ return match_case[match_string] if match_case.key? match_string
129
+ end
130
+ return ''
131
+ end
132
+
133
+ # Takes three arguments. If the first is truthy, return the 2nd. Else return the 3rd
134
+ def conditional(args)
135
+ case args[0]
136
+ when nil, '', false, 'false'
137
+ args[2]
138
+ else
139
+ args[1]
140
+ end
141
+ end
142
+
143
+ # Date `number_of_days` after `date_string`
144
+ def date_after(args)
145
+ _fail_if_unset(__method__.to_s, args)
146
+ date_string = args[0]
147
+ number_of_days = args[1].to_i
148
+
149
+ date = _date_string_to_date(date_string)
150
+ _date_to_date_string(date + number_of_days)
151
+ end
152
+
153
+ # Date `number_of_days` before `date_string`
154
+ def date_before(args)
155
+ _fail_if_unset(__method__.to_s, args)
156
+ date_string = args[0]
157
+ number_of_days = args[1].to_i
158
+
159
+ date = _date_string_to_date(date_string)
160
+ _date_to_date_string(date - number_of_days)
161
+ end
162
+
163
+ # From the argument list, returns the first that isn't empty or unset
164
+ # Return an empty string if none are.
165
+ def default(args)
166
+ args.each do |a|
167
+ next if a.nil?
168
+
169
+ return a
170
+ end
171
+ return ''
172
+ end
173
+
174
+ def jira_project(args)
175
+ _jira_key('project', jira_projects, args[0])
176
+ end
177
+
178
+ def jira_sprint(args)
179
+ _jira_key('sprint', jira_sprints, args[0])
180
+ end
181
+
182
+ def jira_status(args)
183
+ _jira_key('status', jira_statuses, args[0])
184
+ end
185
+
186
+ def jira_team(args)
187
+ _jira_key('team', jira_teams, args[0])
188
+ end
189
+
190
+ # Return the first n fields of '.' - delimited version string
191
+ def n_digit_version(args)
192
+ _fail_if_unset(__method__.to_s, args)
193
+
194
+ n = args[0].to_i - 1
195
+ version = args[1]
196
+ version.split('.')[0..n].join('.')
197
+ end
198
+
199
+ # Prompt if the value isn't defined or in the environment
200
+ # Arguments:
201
+ # 0: The name of the user variable to prompt for
202
+ # Returns:
203
+ # The first found
204
+ # value of declared variable
205
+ # value from the shell environment
206
+ # the string the user types at the prompt
207
+ def prompt(args)
208
+ if args.nil?
209
+ fatal 'function "prompt_only" requires variable name.'
210
+ end
211
+ _fail_if_unset(__method__.to_s, args)
212
+
213
+ variable_name = args[0]
214
+ variable_symbol = variable_name.to_sym
215
+
216
+ return @declarations[variable_symbol] if @declarations.key?(variable_symbol)
217
+ return ENV[variable_name] if ENV.key?(variable_name)
218
+
219
+ print "Enter value for \"#{variable_name}\": "
220
+ return $stdin.gets.chomp
221
+ end
222
+
223
+ # Explicitly prompt for a value
224
+ # Arguments:
225
+ # 0: The name of the user variable to prompt for
226
+ # Returns:
227
+ # The string user types from STDIN
228
+ def prompt_only(args)
229
+ if args.nil?
230
+ fatal 'function "prompt_only" requires variable name.'
231
+ end
232
+ _fail_if_unset(__method__.to_s, args)
233
+ variable_name = args[0]
234
+ print "Enter value for \"#{variable_name}\": "
235
+ $stdin.gets.chomp
236
+ end
237
+
238
+ # Randomly pick from a list of strings
239
+ # Arguments:
240
+ # *: a list of strings from which to pick
241
+ #
242
+ def random(args)
243
+ _fail_if_unset(__method__.to_s, args)
244
+ args[Random.new.rand(args.count)]
245
+ end
246
+
247
+ # Return a conditional value based upon a version string
248
+ # Arguments:
249
+ # 0: a semver version string.
250
+ # 1: a hash with keys of 'X', 'Y', or 'Z' representing the semver fields
251
+ # and a value to set for that corresponding release type.
252
+ # For example:
253
+ # 0: "2.5.9"
254
+ # 1: {'X' => 'foo', 'Y' => 'bar', 'Z' => 'baz'}
255
+ # will return 'baz'
256
+ def release_type(args)
257
+ _fail_if_unset(__method__.to_s, args)
258
+ this_type = _string_to_xyz(args[0])
259
+ condition_hash = args[1]
260
+
261
+ unless condition_hash.keys.sort == %w[X Y Z].sort
262
+ fatal 'function "release_type" requires a hash with keys ("X", "Y", "Z") as 2nd argument. '\
263
+ "Got: #{condition_hash}"
264
+ end
265
+
266
+ return condition_hash[this_type] if condition_hash.key?(this_type)
267
+
268
+ fatal "function \"release_type\" could not find a matching case for #{args[0]}\""
269
+ end
270
+
271
+ # Split a string into a packed array given a regular expression to split with
272
+ # Arguments:
273
+ # 0: a regular expression for splitting, typically '\s*,\s*'
274
+ # 1: string to split
275
+ def split(args)
276
+ _fail_if_unset(__method__.to_s, args)
277
+ fatal('function "split" requires exactly two arguments') unless args.size == 2
278
+ args[1].split(/#{args[0]}/)
279
+ end
280
+
281
+ ## Jira fields helper functions
282
+ def _jira_key(item_name, hash, key)
283
+ key_symbol = key.upcase.to_sym
284
+ unless hash.key? key_symbol
285
+ fatal "Unknown #{item_name} \"#{key}\""
286
+ end
287
+ hash[key_symbol]
288
+ end
289
+
290
+ ## Date calculation helper functions
291
+
292
+ # Expects date_string in the format YYYY-MM-DD
293
+ def _date_string_to_date(date_string)
294
+ year, month, day = date_string.split('-').map(&:to_i)
295
+ Date.new(year, month, day)
296
+ end
297
+
298
+ def _date_to_date_string(date)
299
+ "#{date.year}-#{date.month}-#{date.day}"
300
+ end
301
+
302
+ ## Release type helper functions
303
+ def _string_to_xyz(version)
304
+ y_value = version.split('.')[1]
305
+ z_value = version.split('.')[2]
306
+ return 'Z' unless z_value == '0'
307
+
308
+ return 'Y' unless y_value == '0'
309
+
310
+ return 'X'
311
+ end
312
+
313
+ ## Fail on unset arguments
314
+ def _fail_if_unset(function_name, arguments)
315
+ arguments.each do |a|
316
+ fatal "\"#{function_name}\" called with unset variable." if a.nil?
317
+ end
318
+ end
319
+ end
data/lib/tefoji/cli.rb ADDED
@@ -0,0 +1,113 @@
1
+ require 'docopt'
2
+ require 'rubygems'
3
+ require 'uri'
4
+ require 'yaml'
5
+
6
+ module Tefoji
7
+ class CLI
8
+ DEFAULT_JIRA_URL = 'https://tickets.puppetlabs.com'
9
+ JIRA_TEST_URL = 'https://jira-app-dev-1.ops.puppetlabs.net'
10
+ DEFAULT_JIRA_AUTH_FILE = "#{ENV['HOME']}/.tefoji-auth.yaml"
11
+
12
+ DOCUMENTATION = <<~DOCOPT
13
+ Generate Jira issues from YAML files.
14
+
15
+ Usage:
16
+ tefoji [--jira=URL] [--jira-test] [--jira-auth=FILE] [--jira-save-auth] [--jira-user=USER] [--jira-mock] [--no-notes] [--version] [--quiet|--debug] <issue_template> ...
17
+ tefoji [--debug] [--quiet] --print-notes <issue_template> ...
18
+ tefoji [--debug] [--quiet] --verify-only <issue_template> ...
19
+ tefoji --version
20
+
21
+ Options:
22
+ -j --jira URL URL of the Jira instance (defaults to #{DEFAULT_JIRA_URL})
23
+ -J --jira-test Use the jira test environment (#{JIRA_TEST_URL})
24
+ -a --jira-auth=FILE Alternate YAML file with Base64-encoded 'username:password' string
25
+ for Jira authentication
26
+ -S --jira-save-auth Save/overwrite Base64-encoded yaml authentication to the jira-auth
27
+ file (defaults to #{DEFAULT_JIRA_AUTH_FILE}) after succesfully
28
+ authenticating to the Jira server.
29
+ -u USER --jira-user USER User name for Jira
30
+ -M --jira-mock Mock the Jira API. Useful only for development testing.
31
+ -n --no-notes Do not print the template 'notes' section. This is mainly for tefoji
32
+ wrappers that prefer to handle notes their own way.
33
+ -p --print-notes Just print the notes for templates but do no other processing
34
+ -v --version Show version
35
+ -q --quiet Turn off (INFO) logging
36
+
37
+ -D --debug Turn on debug (DEBUG) logging
38
+ -V --verify-only Perform template verification and exit.
39
+ -h --help Show this help
40
+ DOCOPT
41
+
42
+ def initialize(argv)
43
+ @user_options = parse_options(argv)
44
+
45
+ if @user_options['--version']
46
+ warn "tefoji version #{Gem.loaded_specs['tefoji'].version.to_s}"
47
+ exit 0
48
+ end
49
+
50
+ @user_options['jira-auth-string'] = nil
51
+ @user_options['jira-auth-file'] = @user_options['--jira-auth'] || DEFAULT_JIRA_AUTH_FILE
52
+
53
+ 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
60
+
61
+ @user_options['--jira'] = JIRA_TEST_URL if @user_options['--jira-test']
62
+ @user_options['--jira'] = DEFAULT_JIRA_URL if @user_options['--jira'].nil?
63
+ end
64
+
65
+ # Iterate through issue templates, validating each. If that goes well, generate
66
+ # Jira issues from them.
67
+ def run
68
+ tefoji = Tefoji.new(@user_options)
69
+
70
+ template_uris = @user_options['<issue_template>']
71
+
72
+ # 1st pass, try to find each of the templates, verify each for syntax,
73
+ # keep going even if we find errors in some templates
74
+ error_count, templates = tefoji.yaml_syntax_check(template_uris)
75
+
76
+ # 2nd pass, scan any templates that passed syntax-check for semantic problems
77
+ exit 1 unless error_count.zero?
78
+
79
+ if @user_options['--verify-only']
80
+ warn 'tefoji: no errors found.'
81
+ exit 0
82
+ end
83
+
84
+ if @user_options['--print-notes']
85
+ templates.each do |template|
86
+ warn template[1]['notes'] if template[1].key?('notes')
87
+ end
88
+ exit 0
89
+ end
90
+
91
+ # Retrieve any included templates
92
+ templates.each do |template|
93
+ template[:includes] = tefoji.includes(template)
94
+ end
95
+
96
+ # Looks like we're good to go, authenticate to jira and do the work
97
+ tefoji.authenticate
98
+ tefoji.save_authentication if @user_options['--jira-save-auth']
99
+ templates.each { |template| tefoji.run(template) }
100
+
101
+ warn 'tefoji: completed sucessfully.'
102
+ end
103
+
104
+ private
105
+
106
+ def parse_options(argv = nil)
107
+ Docopt.docopt(DOCUMENTATION, { argv: argv })
108
+ rescue Docopt::Exit => e
109
+ warn e.message
110
+ exit 1
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,51 @@
1
+ module Tefoji
2
+ class DeclaredValue
3
+ attr_accessor :value, :properties
4
+
5
+ def initialize(value = nil)
6
+ @value = value
7
+ @value = value.value if value.is_a? DeclaredValue
8
+ @properties = {}
9
+ end
10
+
11
+ def prepend(*prepended_things)
12
+ @value.prepend(*prepended_things)
13
+ end
14
+
15
+ def to_i
16
+ @value.to_i
17
+ end
18
+
19
+ def to_s
20
+ @value.to_s
21
+ end
22
+
23
+ def to_a
24
+ if @value.is_a? Array
25
+ @value
26
+ else
27
+ [@value]
28
+ end
29
+ end
30
+
31
+ def %(other)
32
+ @value % other
33
+ end
34
+
35
+ def unset?
36
+ @value.nil?
37
+ end
38
+
39
+ def falsey?
40
+ if @value.nil? || @value.empty? || @value.downcase == 'false' || @value == false
41
+ true
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ def truthy?
48
+ !falsey?
49
+ end
50
+ end
51
+ end