firespring_dev_commands 1.3.0
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/LICENSE +21 -0
- data/README.md +83 -0
- data/lib/firespring_dev_commands/audit/report/item.rb +33 -0
- data/lib/firespring_dev_commands/audit/report/levels.rb +36 -0
- data/lib/firespring_dev_commands/audit/report.rb +49 -0
- data/lib/firespring_dev_commands/aws/account/info.rb +15 -0
- data/lib/firespring_dev_commands/aws/account.rb +164 -0
- data/lib/firespring_dev_commands/aws/cloudformation/parameters.rb +26 -0
- data/lib/firespring_dev_commands/aws/cloudformation.rb +188 -0
- data/lib/firespring_dev_commands/aws/codepipeline.rb +96 -0
- data/lib/firespring_dev_commands/aws/credentials.rb +136 -0
- data/lib/firespring_dev_commands/aws/login.rb +131 -0
- data/lib/firespring_dev_commands/aws/parameter.rb +32 -0
- data/lib/firespring_dev_commands/aws/profile.rb +55 -0
- data/lib/firespring_dev_commands/aws/s3.rb +42 -0
- data/lib/firespring_dev_commands/aws.rb +10 -0
- data/lib/firespring_dev_commands/boolean.rb +7 -0
- data/lib/firespring_dev_commands/common.rb +112 -0
- data/lib/firespring_dev_commands/daterange.rb +171 -0
- data/lib/firespring_dev_commands/docker/compose.rb +271 -0
- data/lib/firespring_dev_commands/docker/status.rb +38 -0
- data/lib/firespring_dev_commands/docker.rb +276 -0
- data/lib/firespring_dev_commands/dotenv.rb +6 -0
- data/lib/firespring_dev_commands/env.rb +38 -0
- data/lib/firespring_dev_commands/eol/product_version.rb +86 -0
- data/lib/firespring_dev_commands/eol.rb +58 -0
- data/lib/firespring_dev_commands/git/info.rb +13 -0
- data/lib/firespring_dev_commands/git.rb +420 -0
- data/lib/firespring_dev_commands/jira/issue.rb +33 -0
- data/lib/firespring_dev_commands/jira/project.rb +13 -0
- data/lib/firespring_dev_commands/jira/user/type.rb +20 -0
- data/lib/firespring_dev_commands/jira/user.rb +31 -0
- data/lib/firespring_dev_commands/jira.rb +78 -0
- data/lib/firespring_dev_commands/logger.rb +8 -0
- data/lib/firespring_dev_commands/node/audit.rb +39 -0
- data/lib/firespring_dev_commands/node.rb +107 -0
- data/lib/firespring_dev_commands/php/audit.rb +71 -0
- data/lib/firespring_dev_commands/php.rb +109 -0
- data/lib/firespring_dev_commands/rake.rb +24 -0
- data/lib/firespring_dev_commands/ruby/audit.rb +30 -0
- data/lib/firespring_dev_commands/ruby.rb +113 -0
- data/lib/firespring_dev_commands/second.rb +22 -0
- data/lib/firespring_dev_commands/tar/pax_header.rb +49 -0
- data/lib/firespring_dev_commands/tar/type_flag.rb +49 -0
- data/lib/firespring_dev_commands/tar.rb +149 -0
- data/lib/firespring_dev_commands/templates/aws.rb +84 -0
- data/lib/firespring_dev_commands/templates/base_interface.rb +54 -0
- data/lib/firespring_dev_commands/templates/ci.rb +138 -0
- data/lib/firespring_dev_commands/templates/docker/application.rb +177 -0
- data/lib/firespring_dev_commands/templates/docker/default.rb +200 -0
- data/lib/firespring_dev_commands/templates/docker/node/application.rb +145 -0
- data/lib/firespring_dev_commands/templates/docker/php/application.rb +190 -0
- data/lib/firespring_dev_commands/templates/docker/ruby/application.rb +146 -0
- data/lib/firespring_dev_commands/templates/eol.rb +23 -0
- data/lib/firespring_dev_commands/templates/git.rb +147 -0
- data/lib/firespring_dev_commands/version.rb +11 -0
- data/lib/firespring_dev_commands.rb +21 -0
- metadata +436 -0
@@ -0,0 +1,420 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'git'
|
3
|
+
|
4
|
+
module Dev
|
5
|
+
# Class for performing git functions
|
6
|
+
class Git
|
7
|
+
# The default base branch to use
|
8
|
+
DEFAULT_MAIN_BRANCH = 'master'.freeze
|
9
|
+
|
10
|
+
# Config object for setting top level git config options
|
11
|
+
Config = Struct.new(:main_branch, :staging_branch, :info, :min_version, :max_version) do
|
12
|
+
def initialize
|
13
|
+
self.main_branch = DEFAULT_MAIN_BRANCH
|
14
|
+
self.staging_branch = nil
|
15
|
+
self.info = [Dev::Git::Info.new(DEV_COMMANDS_PROJECT_NAME, DEV_COMMANDS_ROOT_DIR)]
|
16
|
+
self.min_version = nil
|
17
|
+
self.max_version = nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
# Instantiates a new top level config object if one hasn't already been created
|
23
|
+
# Yields that config object to any given block
|
24
|
+
# Returns the resulting config object
|
25
|
+
def config
|
26
|
+
@config ||= Config.new
|
27
|
+
yield(@config) if block_given?
|
28
|
+
@config
|
29
|
+
end
|
30
|
+
|
31
|
+
# Alias the config method to configure for a slightly clearer access syntax
|
32
|
+
alias_method :configure, :config
|
33
|
+
|
34
|
+
# Returns the version of the git executable running on the system
|
35
|
+
def version
|
36
|
+
@version ||= ::Git::Lib.new(nil, nil).current_command_version.join('.')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_accessor :main_branch, :staging_branch, :release_branches, :info, :original_branches
|
41
|
+
|
42
|
+
def initialize(
|
43
|
+
main_branch: self.class.config.main_branch,
|
44
|
+
staging_branch: self.class.config.staging_branch,
|
45
|
+
info: self.class.config.info
|
46
|
+
)
|
47
|
+
@main_branch = main_branch
|
48
|
+
raise 'main branch must be configured' if main_branch.to_s.empty?
|
49
|
+
|
50
|
+
@staging_branch = staging_branch || main_branch
|
51
|
+
@info = Array(info)
|
52
|
+
raise 'git repositories must be configured' if @info.empty? || !@info.all?(Dev::Git::Info)
|
53
|
+
|
54
|
+
check_version
|
55
|
+
end
|
56
|
+
|
57
|
+
# Checks the min and max version against the current git version if they have been configured
|
58
|
+
def check_version
|
59
|
+
min_version = self.class.config.min_version
|
60
|
+
raise "requires git version >= #{min_version} (found #{self.class.version})" if min_version && !Dev::Common.new.version_greater_than(min_version, self.class.version)
|
61
|
+
|
62
|
+
max_version = self.class.config.max_version
|
63
|
+
raise "requires git version < #{max_version} (found #{self.class.version})" if max_version && Dev::Common.new.version_greater_than(max_version, self.class.version)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns all git paths configured in our info
|
67
|
+
def project_dirs
|
68
|
+
@project_dirs ||= @info.map(&:path).sort
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the first configured project dire
|
72
|
+
def default_project_dir
|
73
|
+
project_dirs.first
|
74
|
+
end
|
75
|
+
|
76
|
+
# Populates and returns a hash containing the original version of branches
|
77
|
+
def original_branches
|
78
|
+
@original_branches ||= current_branches
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a hash of each project repo and the branch that is currently checked out
|
82
|
+
def current_branches
|
83
|
+
{}.tap do |hsh|
|
84
|
+
project_dirs.each do |project_dir|
|
85
|
+
next unless File.exist?(project_dir)
|
86
|
+
|
87
|
+
Dir.chdir(project_dir) do
|
88
|
+
hsh[project_dir] = branch_name(dir: project_dir)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the branch name associated with the given repository
|
95
|
+
# Defaults to the current directory
|
96
|
+
def branch_name(dir: default_project_dir)
|
97
|
+
return unless File.exist?(dir)
|
98
|
+
|
99
|
+
::Git.open(dir).current_branch
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns true if the remote branch exists, false otherwise
|
103
|
+
def branch_exists?(project_dir, branch_name)
|
104
|
+
::Git.ls_remote(project_dir)['remotes']["origin/#{branch_name}"]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Prints the status of multiple repository directories and displays the results in a nice format
|
108
|
+
def status_all
|
109
|
+
@success = true
|
110
|
+
puts
|
111
|
+
puts 'Getting status in each repo'.light_yellow if project_dirs.length > 1
|
112
|
+
project_dirs.each do |project_dir|
|
113
|
+
next unless File.exist?(project_dir)
|
114
|
+
|
115
|
+
repo_basename = File.basename(File.realpath(project_dir))
|
116
|
+
header = " #{repo_basename} (#{original_branches[project_dir]}) "
|
117
|
+
puts center_pad(header).light_green
|
118
|
+
@success &= status(dir: project_dir)
|
119
|
+
puts center_pad.light_green
|
120
|
+
end
|
121
|
+
puts
|
122
|
+
|
123
|
+
raise 'Failed getting status on one or more repositories' unless @success
|
124
|
+
end
|
125
|
+
|
126
|
+
# Prints the results of the status command
|
127
|
+
# Currently running "git status" instead of using the library because it doesn't do well formatting the output
|
128
|
+
def status(dir: default_project_dir)
|
129
|
+
return unless File.exist?(dir)
|
130
|
+
|
131
|
+
# NOTE: git library doesn't have a good "status" analog. So just run the standard "git" one
|
132
|
+
# splitting and puts'ing to prefix each line with spaces...
|
133
|
+
Dir.chdir(dir) { indent `git status` }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns the name of any repositories which have changes
|
137
|
+
def repos_with_changes
|
138
|
+
info.filter_map { |it| it.name unless changes(dir: it.path).empty? }
|
139
|
+
end
|
140
|
+
|
141
|
+
# Print the changes on the given repo
|
142
|
+
# Defaults to the current directory
|
143
|
+
def changes(dir: default_project_dir)
|
144
|
+
return unless File.exist?(dir)
|
145
|
+
|
146
|
+
Dir.chdir(dir) { `git status --porcelain | grep -v '^?'` }.split("\n").map(&:strip)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Print the changes on the given repo using the ruby built-in method... which seems _REALLY_ slow compared to the porcelain version
|
150
|
+
# Defaults to the current directory
|
151
|
+
def changes_slow(dir: default_project_dir)
|
152
|
+
return unless File.exist?(dir)
|
153
|
+
|
154
|
+
s = ::Git.open(dir).status
|
155
|
+
s.added.keys.map { |it| " A #{it}" } +
|
156
|
+
s.changed.keys.map { |it| " M #{it}" } +
|
157
|
+
s.deleted.keys.map { |it| " D #{it}" }
|
158
|
+
end
|
159
|
+
|
160
|
+
# Runs a git reset on all given repositories with some additional formatting
|
161
|
+
def reset_all
|
162
|
+
puts
|
163
|
+
puts 'Resetting each repo'.light_yellow if project_dirs.length > 1
|
164
|
+
project_dirs.each do |project_dir|
|
165
|
+
next unless File.exist?(project_dir)
|
166
|
+
|
167
|
+
repo_basename = File.basename(File.realpath(project_dir))
|
168
|
+
header = " #{repo_basename} (#{original_branches[project_dir]}) "
|
169
|
+
puts center_pad(header).light_green
|
170
|
+
reset(dir: project_dir)
|
171
|
+
puts center_pad.light_green
|
172
|
+
end
|
173
|
+
puts
|
174
|
+
end
|
175
|
+
|
176
|
+
# Runs a git reset on the given repo
|
177
|
+
# Defaults to the current directory
|
178
|
+
def reset(dir: default_project_dir)
|
179
|
+
return unless File.exist?(dir)
|
180
|
+
|
181
|
+
g = ::Git.open(dir)
|
182
|
+
indent g.reset_hard
|
183
|
+
end
|
184
|
+
|
185
|
+
# Checks out the given branch in all repositories with some additional formatting
|
186
|
+
def checkout_all(branch)
|
187
|
+
@success = true
|
188
|
+
puts
|
189
|
+
puts "Checking out #{branch} in each repo".light_yellow if project_dirs.length > 1
|
190
|
+
project_dirs.each do |project_dir|
|
191
|
+
next unless File.exist?(project_dir)
|
192
|
+
|
193
|
+
repo_basename = File.basename(File.realpath(project_dir))
|
194
|
+
header = " #{repo_basename} "
|
195
|
+
puts center_pad(header).light_green
|
196
|
+
@success &= checkout(branch, dir: project_dir)
|
197
|
+
puts center_pad.light_green
|
198
|
+
end
|
199
|
+
puts
|
200
|
+
|
201
|
+
raise "Failed checking out branch #{branch} one or more repositories" unless @success
|
202
|
+
end
|
203
|
+
|
204
|
+
# Checks out the given branch in the given repo
|
205
|
+
# Defaults to the current directory
|
206
|
+
def checkout(branch, dir: default_project_dir)
|
207
|
+
raise 'branch is required' if branch.to_s.strip.empty?
|
208
|
+
return unless File.exist?(dir)
|
209
|
+
|
210
|
+
# Make sure the original branch hash has been created before we change anything
|
211
|
+
original_branches
|
212
|
+
|
213
|
+
g = ::Git.open(dir)
|
214
|
+
g.fetch('origin', prune: true)
|
215
|
+
|
216
|
+
# If the branch we are checking out doesn't exist, check out either the staging branch or the main branch
|
217
|
+
actual_branch = branch
|
218
|
+
unless branch_exists?(dir, branch)
|
219
|
+
actual_branch = [staging_branch, main_branch].uniq.find { |it| branch_exists?(dir, it) }
|
220
|
+
puts "Branch #{branch} not found, checking out #{actual_branch} instead".light_yellow
|
221
|
+
end
|
222
|
+
|
223
|
+
indent g.checkout(actual_branch)
|
224
|
+
indent g.pull('origin', actual_branch)
|
225
|
+
true
|
226
|
+
rescue ::Git::GitExecuteError => e
|
227
|
+
print_errors(e.message)
|
228
|
+
false
|
229
|
+
end
|
230
|
+
|
231
|
+
# Create the given branch in the given repo
|
232
|
+
def create_branch(branch, dir: default_project_dir)
|
233
|
+
raise 'branch is required' if branch.to_s.strip.empty?
|
234
|
+
raise "refusing to create protected branch '#{branch}'" if %w(master develop).any?(branch.to_s.strip)
|
235
|
+
return unless File.exist?(dir)
|
236
|
+
|
237
|
+
# Make sure the original branch hash has been created before we change anything
|
238
|
+
original_branches
|
239
|
+
|
240
|
+
g = ::Git.open(dir)
|
241
|
+
g.fetch('origin', prune: true)
|
242
|
+
|
243
|
+
puts "Fetching the latest changes for base branch #{staging_branch}"
|
244
|
+
g.checkout(staging_branch)
|
245
|
+
g.pull('origin', staging_branch)
|
246
|
+
|
247
|
+
puts "Creating branch #{branch}, pushing to origin, and updating remote tracking"
|
248
|
+
g.branch(branch).checkout
|
249
|
+
g.push('origin', branch)
|
250
|
+
g.config("branch.#{branch}.remote", 'origin')
|
251
|
+
g.config("branch.#{branch}.merge", "refs/heads/#{branch}")
|
252
|
+
puts
|
253
|
+
rescue ::Git::GitExecuteError => e
|
254
|
+
print_errors(e.message)
|
255
|
+
false
|
256
|
+
end
|
257
|
+
|
258
|
+
# Merge the branch into all repositories
|
259
|
+
def merge_all(branch)
|
260
|
+
@success = true
|
261
|
+
puts
|
262
|
+
puts "Merging #{branch} into each repo".light_yellow if project_dirs.length > 1
|
263
|
+
project_dirs.each do |project_dir|
|
264
|
+
next unless File.exist?(project_dir)
|
265
|
+
|
266
|
+
repo_basename = File.basename(File.realpath(project_dir))
|
267
|
+
header = " #{repo_basename} "
|
268
|
+
puts center_pad(header).light_green
|
269
|
+
@success &= merge(branch, dir: project_dir)
|
270
|
+
puts center_pad.light_green
|
271
|
+
end
|
272
|
+
puts
|
273
|
+
|
274
|
+
raise "Failed merging branch #{branch} in one or more repositories" unless @success
|
275
|
+
|
276
|
+
push_all
|
277
|
+
end
|
278
|
+
|
279
|
+
# Merge the given branch into the given repo
|
280
|
+
def merge(branch, dir: default_project_dir)
|
281
|
+
raise 'branch is required' if branch.to_s.strip.empty?
|
282
|
+
return unless File.exist?(dir)
|
283
|
+
|
284
|
+
# Make sure the original branch hash has been created before we change anything
|
285
|
+
original_branches
|
286
|
+
|
287
|
+
g = ::Git.open(dir)
|
288
|
+
g.fetch('origin', prune: true)
|
289
|
+
raise 'branch does not exist' unless branch_exists?(dir, branch)
|
290
|
+
|
291
|
+
# No need to merge into ourself
|
292
|
+
current_branch = branch_name(dir: dir)
|
293
|
+
return true if current_branch == branch
|
294
|
+
|
295
|
+
indent "Merging #{branch} into #{current_branch}"
|
296
|
+
indent g.merge(branch)
|
297
|
+
true
|
298
|
+
rescue ::Git::GitExecuteError => e
|
299
|
+
print_errors(e.message)
|
300
|
+
false
|
301
|
+
end
|
302
|
+
|
303
|
+
# Pull the latest in all repositories
|
304
|
+
def pull_all
|
305
|
+
@success = true
|
306
|
+
puts
|
307
|
+
puts 'Pulling current branch into each repo'.light_yellow if project_dirs.length > 1
|
308
|
+
project_dirs.each do |project_dir|
|
309
|
+
next unless File.exist?(project_dir)
|
310
|
+
|
311
|
+
repo_basename = File.basename(File.realpath(project_dir))
|
312
|
+
header = " #{repo_basename} "
|
313
|
+
puts center_pad(header).light_green
|
314
|
+
@success &= pull(dir: project_dir)
|
315
|
+
puts center_pad.light_green
|
316
|
+
end
|
317
|
+
puts
|
318
|
+
|
319
|
+
raise 'Failed pulling branch in one or more repositories' unless @success
|
320
|
+
end
|
321
|
+
|
322
|
+
# Pull the given repo
|
323
|
+
def pull(dir: default_project_dir)
|
324
|
+
return unless File.exist?(dir)
|
325
|
+
|
326
|
+
g = ::Git.open(dir)
|
327
|
+
g.fetch('origin', prune: true)
|
328
|
+
|
329
|
+
branch = branch_name(dir: dir)
|
330
|
+
indent "Pulling branch #{branch} from origin"
|
331
|
+
indent g.pull('origin', branch)
|
332
|
+
true
|
333
|
+
rescue ::Git::GitExecuteError => e
|
334
|
+
print_errors(e.message)
|
335
|
+
false
|
336
|
+
end
|
337
|
+
|
338
|
+
# Push to remote in all repositories
|
339
|
+
def push_all
|
340
|
+
@success = true
|
341
|
+
puts
|
342
|
+
puts 'Pushing current branch into each repo'.light_yellow if project_dirs.length > 1
|
343
|
+
project_dirs.each do |project_dir|
|
344
|
+
next unless File.exist?(project_dir)
|
345
|
+
|
346
|
+
repo_basename = File.basename(File.realpath(project_dir))
|
347
|
+
header = " #{repo_basename} "
|
348
|
+
puts center_pad(header).light_green
|
349
|
+
@success &= push(dir: project_dir)
|
350
|
+
puts center_pad.light_green
|
351
|
+
end
|
352
|
+
puts
|
353
|
+
|
354
|
+
raise 'Failed pushing branch in one or more repositories' unless @success
|
355
|
+
end
|
356
|
+
|
357
|
+
# Push the given repo
|
358
|
+
def push(dir: default_project_dir)
|
359
|
+
return unless File.exist?(dir)
|
360
|
+
|
361
|
+
g = ::Git.open(dir)
|
362
|
+
g.fetch('origin', prune: true)
|
363
|
+
|
364
|
+
branch = branch_name(dir: dir)
|
365
|
+
indent "Pushing branch #{branch} to origin"
|
366
|
+
indent g.push('origin', branch)
|
367
|
+
true
|
368
|
+
rescue ::Git::GitExecuteError => e
|
369
|
+
print_errors(e.message)
|
370
|
+
false
|
371
|
+
end
|
372
|
+
|
373
|
+
# Clones all repositories
|
374
|
+
def clone_repos
|
375
|
+
info.each { |it| clone_repo(dir: it.path, repo_name: it.name) }
|
376
|
+
end
|
377
|
+
|
378
|
+
# Clones the repo_name into the dir
|
379
|
+
# Optionally specify a repo_org
|
380
|
+
# Optionally specify a branch to check out (defaults to the repository default branch)
|
381
|
+
def clone_repo(dir:, repo_name:, repo_org: 'firespring', branch: nil)
|
382
|
+
if Dir.exist?("#{dir}/.git")
|
383
|
+
puts "#{dir} already cloned".light_green
|
384
|
+
return
|
385
|
+
end
|
386
|
+
|
387
|
+
FileUtils.mkdir_p(dir.to_s)
|
388
|
+
|
389
|
+
puts "Cloning #{dir} from #{ssh_repo_url(repo_name, repo_org)}".light_yellow
|
390
|
+
|
391
|
+
opts = {}
|
392
|
+
opts[:branch] = branch unless branch.to_s.strip.empty?
|
393
|
+
g = ::Git.clone(ssh_repo_url(repo_name, repo_org), dir, opts)
|
394
|
+
g.fetch('origin', prune: true)
|
395
|
+
end
|
396
|
+
|
397
|
+
# Builds an ssh repo URL using the org and repo name given
|
398
|
+
def ssh_repo_url(name, org)
|
399
|
+
"git@github.com:#{org}/#{name}.git"
|
400
|
+
end
|
401
|
+
|
402
|
+
# Split on newlines and add additional padding
|
403
|
+
def indent(string, padding: ' ')
|
404
|
+
string.to_s.split("\n").each { |line| puts "#{padding}#{line}" }
|
405
|
+
end
|
406
|
+
|
407
|
+
# Center the string and pad on either side with the given padding character
|
408
|
+
def center_pad(string = '', pad: '-', len: 80)
|
409
|
+
center_dash = len / 2
|
410
|
+
string = string.to_s
|
411
|
+
center_str = string.length / 2
|
412
|
+
string.rjust(center_dash + center_str - 1, pad).ljust(len - 1, pad)
|
413
|
+
end
|
414
|
+
|
415
|
+
# Exclude the command from the message and print all error lines
|
416
|
+
private def print_errors(message)
|
417
|
+
indent message.split('error:')[1..].join
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Dev
|
2
|
+
class Jira
|
3
|
+
# Contains information and methods representing a Jira issue
|
4
|
+
class Issue
|
5
|
+
# Issue subtypes which do not map to a story type
|
6
|
+
NON_STORY_TYPES = ['review', 'sub-task', 'code review sub-task', 'pre-deploy sub-task', 'deploy sub-task', 'devops sub-task'].freeze
|
7
|
+
|
8
|
+
attr_accessor :data, :project, :id, :title, :points, :assignee, :resolved_date
|
9
|
+
|
10
|
+
def initialize(data)
|
11
|
+
@data = data
|
12
|
+
@project = Jira::Project.new(data)
|
13
|
+
@id = data.key
|
14
|
+
@title = data.summary
|
15
|
+
@points = calculate_points(data)
|
16
|
+
@assignee = Jira::User.lookup(data.assignee&.accountId)
|
17
|
+
@resolved_date = data.resolutiondate
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the value of the jira points field or 0 if the field is not found
|
21
|
+
def calculate_points(data)
|
22
|
+
return data.send(Dev::Jira.config.points_field_name).to_i if Dev::Jira.config.points_field_name && data.respond_to?(Dev::Jira.config.points_field_name)
|
23
|
+
|
24
|
+
0
|
25
|
+
end
|
26
|
+
|
27
|
+
# Converts the jira issue object to a string representation
|
28
|
+
def to_s
|
29
|
+
"[#{id}] #{title} (#{points} pts) (resolved #{resolved_date}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Dev
|
2
|
+
class Jira
|
3
|
+
# Contains information on the Jira project
|
4
|
+
class Project
|
5
|
+
attr_accessor :name
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
@name = data.project.name
|
9
|
+
@name = @name << ' DevOps' if /devops/i.match?(data.issuetype.name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dev
|
2
|
+
class Jira
|
3
|
+
class User
|
4
|
+
# Contains constants representing valid Jira user types
|
5
|
+
class Type
|
6
|
+
# Bot type
|
7
|
+
BOT = :bot
|
8
|
+
|
9
|
+
# Developer type
|
10
|
+
DEVELOPER = :developer
|
11
|
+
|
12
|
+
# "Other" type
|
13
|
+
OTHER = :other
|
14
|
+
|
15
|
+
# Project manager type
|
16
|
+
PM = :pm
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dev
|
2
|
+
class Jira
|
3
|
+
# Contains Jira user information
|
4
|
+
# Jira does not make user information available through their normal api (they have an admin api that you can use)
|
5
|
+
# Therefore, we've provided a "lookup" method which attempts to find the jira user id in a hash of user information
|
6
|
+
# that you can configure
|
7
|
+
class User
|
8
|
+
attr_accessor :name, :email, :id, :type
|
9
|
+
|
10
|
+
def initialize(name:, email:, id:, type: Type::OTHER)
|
11
|
+
@name = name
|
12
|
+
@email = email
|
13
|
+
@id = id
|
14
|
+
@type = type
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns true if the Jira user is categorized as a developer
|
18
|
+
def developer?
|
19
|
+
type == Type::DEVELOPER
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the Jira user object which maps to the give user id
|
23
|
+
# If none is found, it returns a Jira user object with only the id set
|
24
|
+
def self.lookup(id)
|
25
|
+
user = Dev::Jira.config.user_lookup_list&.find { |it| it.id == id }
|
26
|
+
user ||= new(name: '', email: '', id: id)
|
27
|
+
user
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'jira-ruby'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Dev
|
5
|
+
# Class which contains methods to conenct to jira and query issues
|
6
|
+
class Jira
|
7
|
+
# Config object for setting top level jira config options
|
8
|
+
# "points_field_name" is the field holding the value for points on a story. If this is not present, all points will default to 0
|
9
|
+
# "user_lookup_list" should be an array of Jira::User objects representing the usernames, ids, etc for all jira users
|
10
|
+
# This is a bit clumsy but currently the jira api only returns the user id with issues
|
11
|
+
# and there is no way to query this information from Jira directly.
|
12
|
+
Config = Struct.new(:username, :token, :url, :points_field_name, :user_lookup_list, :read_timeout, :http_debug) do
|
13
|
+
def initialize
|
14
|
+
self.username = nil
|
15
|
+
self.token = nil
|
16
|
+
self.url = nil
|
17
|
+
self.points_field_name = nil
|
18
|
+
self.user_lookup_list = []
|
19
|
+
self.read_timeout = 120
|
20
|
+
self.http_debug = false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# Instantiates a new top level config object if one hasn't already been created
|
26
|
+
# Yields that config object to any given block
|
27
|
+
# Returns the resulting config object
|
28
|
+
def config
|
29
|
+
@config ||= Config.new
|
30
|
+
yield(@config) if block_given?
|
31
|
+
@config
|
32
|
+
end
|
33
|
+
|
34
|
+
# Alias the config method to configure for a slightly clearer access syntax
|
35
|
+
alias_method :configure, :config
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_accessor :username, :token, :url, :auth, :client
|
39
|
+
|
40
|
+
# Initialize a new jira client using the given inputs
|
41
|
+
def initialize(username: self.class.config.username, token: self.class.config.token, url: self.class.config.url)
|
42
|
+
@username = username
|
43
|
+
@token = token
|
44
|
+
@url = url
|
45
|
+
@auth = Base64.strict_encode64("#{@username}:#{@token}")
|
46
|
+
|
47
|
+
options = {
|
48
|
+
auth_type: :basic,
|
49
|
+
site: @url,
|
50
|
+
default_headers: {Authorization: "Basic #{@auth}"},
|
51
|
+
context_path: '',
|
52
|
+
read_timeout: self.class.config.read_timeout,
|
53
|
+
use_ssl: true,
|
54
|
+
ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
55
|
+
http_debug: self.class.config.http_debug
|
56
|
+
}
|
57
|
+
|
58
|
+
@client = JIRA::Client.new(options)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Query jira using the given jql and yield each matching result
|
62
|
+
def issues(jql, &block)
|
63
|
+
start_at = 0
|
64
|
+
max_results = 100
|
65
|
+
|
66
|
+
# Query Jira and yield all issues it returns
|
67
|
+
issues = @client.Issue.jql(jql, start_at: start_at, max_results: max_results)
|
68
|
+
issues.map { |data| Issue.new(data) }.each(&block)
|
69
|
+
|
70
|
+
# If we returned the max_results then there may be more - add the max results to where we start at and query again
|
71
|
+
while issues.length >= max_results
|
72
|
+
start_at += max_results
|
73
|
+
issues = @client.Issue.jql(jql, start_at: start_at, max_results: max_results)
|
74
|
+
issues.map { |data| Issue.new(data) }.each(&block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Dev
|
2
|
+
class Node
|
3
|
+
# Class which contains commands and customizations for node security audit reports
|
4
|
+
class Audit
|
5
|
+
attr_accessor :data
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
@data = JSON.parse(Dev::Common.new.strip_non_json(data))
|
9
|
+
end
|
10
|
+
|
11
|
+
# Convert the node audit data to the standardized audit report object
|
12
|
+
def to_report
|
13
|
+
ids = Set.new
|
14
|
+
|
15
|
+
Dev::Audit::Report.new(
|
16
|
+
data['vulnerabilities'].map do |_, vulnerability|
|
17
|
+
# If the via ia a hash and the id is not already recorded, add the item to our report
|
18
|
+
vulnerability['via'].map do |it|
|
19
|
+
next unless it.is_a?(Hash)
|
20
|
+
|
21
|
+
id = it['url']&.split('/')&.last
|
22
|
+
next if ids.include?(id)
|
23
|
+
|
24
|
+
ids << id
|
25
|
+
Dev::Audit::Report::Item.new(
|
26
|
+
id: id,
|
27
|
+
name: vulnerability['name'],
|
28
|
+
title: it['title'],
|
29
|
+
url: it['url'],
|
30
|
+
severity: vulnerability['severity'],
|
31
|
+
version: it['range']
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end.flatten.compact
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|