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 +7 -0
- data/exe/tefoji +5 -0
- data/lib/mixins/logging.rb +9 -0
- data/lib/mixins/user_functions.rb +319 -0
- data/lib/tefoji/cli.rb +113 -0
- data/lib/tefoji/declared_value.rb +51 -0
- data/lib/tefoji/jira_api.rb +430 -0
- data/lib/tefoji.rb +830 -0
- metadata +210 -0
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,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
|