tefoji 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
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