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