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