abide_dev_utils 0.12.2 → 0.14.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 +4 -4
- data/.github/workflows/mend_ruby.yaml +39 -0
- data/Gemfile.lock +43 -29
- data/abide_dev_utils.gemspec +2 -1
- data/lib/abide_dev_utils/cem/benchmark.rb +8 -5
- data/lib/abide_dev_utils/cem/generate/coverage_report.rb +9 -7
- data/lib/abide_dev_utils/cem/generate/reference.rb +176 -8
- data/lib/abide_dev_utils/cem/validate/strings/base_validator.rb +130 -0
- data/lib/abide_dev_utils/cem/validate/strings/puppet_class_validator.rb +102 -0
- data/lib/abide_dev_utils/cem/validate/strings/puppet_defined_type_validator.rb +18 -0
- data/lib/abide_dev_utils/cem/validate/strings/validation_finding.rb +31 -0
- data/lib/abide_dev_utils/cem/validate/strings.rb +82 -0
- data/lib/abide_dev_utils/cem/validate.rb +2 -1
- data/lib/abide_dev_utils/cem.rb +1 -0
- data/lib/abide_dev_utils/cli/cem.rb +63 -5
- data/lib/abide_dev_utils/cli/jira.rb +3 -2
- data/lib/abide_dev_utils/errors/jira.rb +4 -0
- data/lib/abide_dev_utils/jira.rb +107 -50
- data/lib/abide_dev_utils/markdown.rb +4 -0
- data/lib/abide_dev_utils/output.rb +23 -9
- data/lib/abide_dev_utils/ppt/code_introspection.rb +14 -1
- data/lib/abide_dev_utils/ppt/facter_utils.rb +272 -71
- data/lib/abide_dev_utils/ppt/hiera.rb +5 -4
- data/lib/abide_dev_utils/ppt/strings.rb +183 -0
- data/lib/abide_dev_utils/ppt.rb +10 -10
- data/lib/abide_dev_utils/puppet_strings.rb +108 -0
- data/lib/abide_dev_utils/validate.rb +8 -0
- data/lib/abide_dev_utils/version.rb +1 -1
- data/lib/abide_dev_utils/xccdf/parser/objects.rb +1 -1
- metadata +26 -4
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'validation_finding'
|
4
|
+
|
5
|
+
module AbideDevUtils
|
6
|
+
module CEM
|
7
|
+
module Validate
|
8
|
+
module Strings
|
9
|
+
# Base class for validating Puppet Strings objects. This class can be used directly, but it is
|
10
|
+
# recommended to use a subclass of this class to provide more specific validation logic. Each
|
11
|
+
# subclass should implement a `validate_<type>` method that will be called by the `validate` method
|
12
|
+
# of this class. The `validate_<type>` method should contain the validation logic for the
|
13
|
+
# corresponding type of Puppet Strings object.
|
14
|
+
class BaseValidator
|
15
|
+
SAFE_OBJECT_METHODS = %i[
|
16
|
+
type
|
17
|
+
name
|
18
|
+
file
|
19
|
+
line
|
20
|
+
docstring
|
21
|
+
tags
|
22
|
+
parameters
|
23
|
+
source
|
24
|
+
].freeze
|
25
|
+
PDK_SUMMARY_REGEX = %r{^A short summary of the purpose.*}.freeze
|
26
|
+
PDK_DESCRIPTION_REGEX = %r{^A description of what this.*}.freeze
|
27
|
+
|
28
|
+
attr_reader :findings
|
29
|
+
|
30
|
+
def initialize(strings_object)
|
31
|
+
@object = strings_object
|
32
|
+
@findings = []
|
33
|
+
# Define instance methods for each of the SAFE_OBJECT_METHODS
|
34
|
+
SAFE_OBJECT_METHODS.each do |method|
|
35
|
+
define_singleton_method(method) { safe_method_call(@object, method) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def errors
|
40
|
+
@findings.select { |f| f.type == :error }
|
41
|
+
end
|
42
|
+
|
43
|
+
def warnings
|
44
|
+
@findings.select { |f| f.type == :warning }
|
45
|
+
end
|
46
|
+
|
47
|
+
def errors?
|
48
|
+
!errors.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
def warnings?
|
52
|
+
!warnings.empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_tag_name(tag_name)
|
56
|
+
tags&.find { |t| t.tag_name == tag_name }
|
57
|
+
end
|
58
|
+
|
59
|
+
def select_tag_name(tag_name)
|
60
|
+
return [] if tags.nil?
|
61
|
+
|
62
|
+
tags.select { |t| t.tag_name == tag_name }
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_parameter(param_name)
|
66
|
+
parameters&.find { |p| p[0] == param_name }
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate
|
70
|
+
license_tag?
|
71
|
+
non_generic_summary?
|
72
|
+
non_generic_description?
|
73
|
+
send("validate_#{type}".to_sym) if respond_to?("validate_#{type}".to_sym)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Checks if the object has a license tag and if it is formatted correctly.
|
77
|
+
# Comparison is not case sensitive.
|
78
|
+
def license_tag?
|
79
|
+
see_tags = select_tag_name('see')
|
80
|
+
if see_tags.empty? || see_tags.none? { |t| t.name.casecmp('LICENSE.pdf') && t.text.casecmp('for license') }
|
81
|
+
new_finding(
|
82
|
+
:error,
|
83
|
+
:no_license_tag,
|
84
|
+
remediation: 'Add "@see LICENSE.pdf for license" to the class documentation'
|
85
|
+
)
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Checks if the summary is not the default PDK summary.
|
92
|
+
def non_generic_summary?
|
93
|
+
summary = find_tag_name('summary')&.text
|
94
|
+
return true if summary.nil?
|
95
|
+
|
96
|
+
if summary.match?(PDK_SUMMARY_REGEX)
|
97
|
+
new_finding(:warning, :generic_summary, remediation: 'Add a more descriptive summary')
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
# Checks if the description is not the default PDK description.
|
104
|
+
def non_generic_description?
|
105
|
+
description = docstring
|
106
|
+
return true if description.nil?
|
107
|
+
|
108
|
+
if description.match?(PDK_DESCRIPTION_REGEX)
|
109
|
+
new_finding(:warning, :generic_description, remediation: 'Add a more descriptive description')
|
110
|
+
return false
|
111
|
+
end
|
112
|
+
true
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def safe_method_call(obj, method, *args)
|
118
|
+
obj.send(method, *args)
|
119
|
+
rescue NoMethodError
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def new_finding(type, title, **data)
|
124
|
+
@findings << ValidationFinding.new(type, title.to_sym, data)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_validator'
|
4
|
+
require_relative '../../../validate'
|
5
|
+
|
6
|
+
module AbideDevUtils
|
7
|
+
module CEM
|
8
|
+
module Validate
|
9
|
+
module Strings
|
10
|
+
# Validates a Puppet Class from a Puppet Strings hash
|
11
|
+
class PuppetClassValidator < BaseValidator
|
12
|
+
def validate_puppet_class
|
13
|
+
check_text_or_summary
|
14
|
+
check_params
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Hash] Hash of basic class data to be used in findings
|
18
|
+
def finding_data(**data)
|
19
|
+
data
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Checks if the class has a description or summary
|
25
|
+
def check_text_or_summary
|
26
|
+
valid_desc = AbideDevUtils::Validate.populated_string?(docstring)
|
27
|
+
valid_summary = AbideDevUtils::Validate.populated_string?(find_tag_name('summary')&.text)
|
28
|
+
return if valid_desc || valid_summary
|
29
|
+
|
30
|
+
new_finding(
|
31
|
+
:error,
|
32
|
+
:no_description_or_summary,
|
33
|
+
finding_data(valid_description: valid_desc, valid_summary: valid_summary),
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Checks if the class has parameters and if they are documented
|
38
|
+
def check_params
|
39
|
+
return if parameters.nil? || parameters.empty? # No params
|
40
|
+
|
41
|
+
param_tags = select_tag_name('param')
|
42
|
+
if param_tags.empty?
|
43
|
+
new_finding(:error, :no_parameter_documentation, finding_data(class_parameters: parameters))
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
parameters.each do |param|
|
48
|
+
param_name, def_val = param
|
49
|
+
check_param(param_name, def_val, param_tags)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks if a parameter is documented properly and if it has a correct default value
|
54
|
+
def check_param(param_name, def_val = nil, param_tags = select_tag_name('param'))
|
55
|
+
param_tag = param_tags.find { |t| t.name == param_name }
|
56
|
+
return unless param_documented?(param_name, param_tag)
|
57
|
+
|
58
|
+
valid_param_description?(param_tag)
|
59
|
+
valid_param_types?(param_tag)
|
60
|
+
valid_param_default?(param_tag, def_val)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Checks if a parameter is documented
|
64
|
+
def param_documented?(param_name, param_tag)
|
65
|
+
return true if param_tag
|
66
|
+
|
67
|
+
new_finding(:error, :param_not_documented, finding_data(param: param_name))
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
# Checks if a parameter has a description
|
72
|
+
def valid_param_description?(param)
|
73
|
+
return true if AbideDevUtils::Validate.populated_string?(param.text)
|
74
|
+
|
75
|
+
new_finding(:error, :param_missing_description, finding_data(param: param.name))
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
# Checks if a parameter is typed
|
80
|
+
def valid_param_types?(param)
|
81
|
+
unless param.types&.any?
|
82
|
+
new_finding(:error, :param_missing_types, finding_data(param: param.name))
|
83
|
+
return false
|
84
|
+
end
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
# Checks if a parameter has a default value and if it is correct for the type
|
89
|
+
def valid_param_default?(param, def_val)
|
90
|
+
return true if def_val.nil?
|
91
|
+
|
92
|
+
if param.types.first.start_with?('Optional[') && def_val != 'undef'
|
93
|
+
new_finding(:error, :param_optional_without_undef_default, param: param.name, default_value: def_val, name: name, file: file)
|
94
|
+
return false
|
95
|
+
end
|
96
|
+
true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'puppet_class_validator'
|
4
|
+
|
5
|
+
module AbideDevUtils
|
6
|
+
module CEM
|
7
|
+
module Validate
|
8
|
+
module Strings
|
9
|
+
# Validates Puppet Defined Type strings objects
|
10
|
+
class PuppetDefinedTypeValidator < PuppetClassValidator
|
11
|
+
def validate_puppet_defined_type
|
12
|
+
validate_puppet_class
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AbideDevUtils
|
4
|
+
module CEM
|
5
|
+
module Validate
|
6
|
+
module Strings
|
7
|
+
# Represents a validation finding (warning or error)
|
8
|
+
class ValidationFinding
|
9
|
+
attr_reader :type, :title, :data
|
10
|
+
|
11
|
+
def initialize(type, title, data)
|
12
|
+
raise ArgumentError, 'type must be :error or :warning' unless %i[error warning].include?(type)
|
13
|
+
|
14
|
+
@type = type.to_sym
|
15
|
+
@title = title.to_sym
|
16
|
+
@data = data
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"#{@type}: #{@title}: #{@data}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_hash
|
24
|
+
{ type: @type, title: @title, data: @data }
|
25
|
+
end
|
26
|
+
alias to_h to_hash
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../ppt/strings'
|
4
|
+
require_relative 'strings/puppet_class_validator'
|
5
|
+
require_relative 'strings/puppet_defined_type_validator'
|
6
|
+
|
7
|
+
module AbideDevUtils
|
8
|
+
module CEM
|
9
|
+
module Validate
|
10
|
+
# Validation objects and methods for Puppet Strings
|
11
|
+
module Strings
|
12
|
+
# Convenience method to validate Puppet Strings of current module
|
13
|
+
def self.validate(**opts)
|
14
|
+
output = Validator.new(nil, **opts).validate
|
15
|
+
output.transform_values do |results|
|
16
|
+
results.select { |r| r[:errors].any? || r[:warnings].any? }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Holds various validation methods for a AbideDevUtils::Ppt::Strings object
|
21
|
+
class Validator
|
22
|
+
def initialize(puppet_strings = nil, **opts)
|
23
|
+
unless puppet_strings.nil? || puppet_strings.is_a?(AbideDevUtils::Ppt::Strings)
|
24
|
+
raise ArgumentError, 'If puppet_strings is supplied, it must be a AbideDevUtils::Ppt::Strings object'
|
25
|
+
end
|
26
|
+
|
27
|
+
puppet_strings = AbideDevUtils::Ppt::Strings.new(**opts) if puppet_strings.nil?
|
28
|
+
@puppet_strings = puppet_strings
|
29
|
+
end
|
30
|
+
|
31
|
+
# Associate validators with each Puppet Strings object and calls #validate on each
|
32
|
+
# @return [Hash] Hash of validation results
|
33
|
+
def validate
|
34
|
+
AbideDevUtils::Ppt::Strings::REGISTRY_TYPES.each_with_object({}) do |rtype, hsh|
|
35
|
+
next unless rtype.to_s.start_with?('puppet_') && @puppet_strings.respond_to?(rtype)
|
36
|
+
|
37
|
+
hsh[rtype] = @puppet_strings.send(rtype).map do |item|
|
38
|
+
item.validator = validator_for(item)
|
39
|
+
item.validate
|
40
|
+
validation_output(item)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Returns the appropriate validator for a given Puppet Strings object
|
48
|
+
def validator_for(item)
|
49
|
+
case item.type
|
50
|
+
when :puppet_class
|
51
|
+
PuppetClassValidator.new(item)
|
52
|
+
when :puppet_defined_type
|
53
|
+
PuppetDefinedTypeValidator.new(item)
|
54
|
+
else
|
55
|
+
BaseValidator.new(item)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validation_output(item)
|
60
|
+
{
|
61
|
+
name: item.name,
|
62
|
+
file: item.file,
|
63
|
+
line: item.line,
|
64
|
+
errors: item.errors,
|
65
|
+
warnings: item.warnings,
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Validate Puppet Class strings hashes.
|
70
|
+
# @return [Hash] Hash of class names and errors
|
71
|
+
def validate_classes!
|
72
|
+
@puppet_strings.puppet_classes.map! do |klass|
|
73
|
+
klass.validator = PuppetClassValidator.new(klass)
|
74
|
+
klass.validate
|
75
|
+
klass
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/abide_dev_utils/cem.rb
CHANGED
@@ -17,6 +17,7 @@ module Abide
|
|
17
17
|
super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: true)
|
18
18
|
add_command(CemGenerate.new)
|
19
19
|
add_command(CemUpdateConfig.new)
|
20
|
+
add_command(CemValidate.new)
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
@@ -69,11 +70,11 @@ module Abide
|
|
69
70
|
quiet = @data.fetch(:quiet, false)
|
70
71
|
console = @data.fetch(:verbose, false) && !quiet
|
71
72
|
generate_opts = {
|
72
|
-
benchmark: @data
|
73
|
-
profile: @data
|
74
|
-
level: @data
|
73
|
+
benchmark: @data[:benchmark],
|
74
|
+
profile: @data[:profile],
|
75
|
+
level: @data[:level],
|
75
76
|
ignore_benchmark_errors: @data.fetch(:ignore_all, false),
|
76
|
-
xccdf_dir: @data
|
77
|
+
xccdf_dir: @data[:xccdf_dir],
|
77
78
|
}
|
78
79
|
AbideDevUtils::Output.simple('Generating coverage report...') unless quiet
|
79
80
|
coverage = AbideDevUtils::CEM::Generate::CoverageReport.generate(format_func: :to_h, opts: generate_opts)
|
@@ -104,11 +105,14 @@ module Abide
|
|
104
105
|
@data[:format] = f
|
105
106
|
end
|
106
107
|
options.on('-v', '--verbose', 'Verbose output') do
|
107
|
-
@data[:
|
108
|
+
@data[:debug] = true
|
108
109
|
end
|
109
110
|
options.on('-q', '--quiet', 'Quiet output') do
|
110
111
|
@data[:quiet] = true
|
111
112
|
end
|
113
|
+
options.on('-s', '--strict', 'Fails if there are any errors') do
|
114
|
+
@data[:strict] = true
|
115
|
+
end
|
112
116
|
end
|
113
117
|
|
114
118
|
def execute
|
@@ -167,5 +171,59 @@ module Abide
|
|
167
171
|
AbideDevUtils::Output.simple(change_report) unless @data[:quiet]
|
168
172
|
end
|
169
173
|
end
|
174
|
+
|
175
|
+
class CemValidate < AbideCommand
|
176
|
+
CMD_NAME = 'validate'
|
177
|
+
CMD_SHORT = 'Validation commands for CEM modules'
|
178
|
+
CMD_LONG = 'Validation commands for CEM modules'
|
179
|
+
def initialize
|
180
|
+
super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: true)
|
181
|
+
add_command(CemValidatePuppetStrings.new)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class CemValidatePuppetStrings < AbideCommand
|
186
|
+
CMD_NAME = 'puppet-strings'
|
187
|
+
CMD_SHORT = 'Validates the Puppet Strings documentation'
|
188
|
+
CMD_LONG = 'Validates the Puppet Strings documentation'
|
189
|
+
def initialize
|
190
|
+
super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false)
|
191
|
+
options.on('-v', '--verbose', 'Verbose output') do
|
192
|
+
@data[:verbose] = true
|
193
|
+
end
|
194
|
+
options.on('-q', '--quiet', 'Quiet output') do
|
195
|
+
@data[:quiet] = true
|
196
|
+
end
|
197
|
+
options.on('-f [FORMAT]', '--format [FORMAT]', 'Format for output (text, json, yaml)') do |f|
|
198
|
+
@data[:format] = f
|
199
|
+
end
|
200
|
+
options.on('-o [FILE]', '--out-file [FILE]', 'Path to save the updated config file') do |o|
|
201
|
+
@data[:out_file] = o
|
202
|
+
end
|
203
|
+
options.on('-s', '--strict', 'Exits with exit code 1 if there are any warnings') do
|
204
|
+
@data[:strict] = true
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def execute
|
209
|
+
@data[:format] ||= 'text'
|
210
|
+
AbideDevUtils::Validate.puppet_module_directory
|
211
|
+
output = AbideDevUtils::CEM::Validate::Strings.validate(**@data)
|
212
|
+
has_errors = false
|
213
|
+
has_warnings = false
|
214
|
+
output.each do |_, i|
|
215
|
+
has_errors = true if i.any? { |j| j[:errors].any? }
|
216
|
+
has_warnings = true if i.any? { |j| j[:warnings].any? }
|
217
|
+
end
|
218
|
+
AbideDevUtils::Output.send(
|
219
|
+
@data[:format].to_sym,
|
220
|
+
output,
|
221
|
+
console: !@data[:quiet],
|
222
|
+
file: @data[:out_file],
|
223
|
+
stringify: true,
|
224
|
+
)
|
225
|
+
exit 1 if has_errors || (has_warnings && @data[:strict])
|
226
|
+
end
|
227
|
+
end
|
170
228
|
end
|
171
229
|
end
|
@@ -123,8 +123,9 @@ module Abide
|
|
123
123
|
super(CMD_NAME, takes_commands: false)
|
124
124
|
short_desc(CMD_SHORT)
|
125
125
|
long_desc(CMD_LONG)
|
126
|
-
argument_desc(PATH: 'An XCCDF file
|
126
|
+
argument_desc(PATH: 'An XCCDF file', PROJECT: 'A Jira project')
|
127
127
|
options.on('-d', '--dry-run', 'Print to console instead of saving objects') { |_| @data[:dry_run] = true }
|
128
|
+
options.on('-e [EPIC]', '--epic [EPIC]', 'If given, tasks will be created and assigned to this epic. Takes form <PROJECT>-<NUM>') { |e| @data[:epic] = e }
|
128
129
|
end
|
129
130
|
|
130
131
|
def execute(path, project)
|
@@ -132,7 +133,7 @@ module Abide
|
|
132
133
|
@data[:dry_run] = false if @data[:dry_run].nil?
|
133
134
|
client = JIRA.client(options: {})
|
134
135
|
proj = JIRA.project(client, project)
|
135
|
-
JIRA.new_issues_from_xccdf(client, proj, path, dry_run: @data[:dry_run])
|
136
|
+
JIRA.new_issues_from_xccdf(client, proj, path, epic: @data[:epic], dry_run: @data[:dry_run])
|
136
137
|
end
|
137
138
|
end
|
138
139
|
end
|
@@ -9,6 +9,10 @@ module AbideDevUtils
|
|
9
9
|
@default = 'Failed to create Jira issue:'
|
10
10
|
end
|
11
11
|
|
12
|
+
class CreateEpicError < GenericError
|
13
|
+
@default = 'Failed to create Jira epic:'
|
14
|
+
end
|
15
|
+
|
12
16
|
class CreateSubtaskError < GenericError
|
13
17
|
@default = 'Failed to create Jira subtask for issue:'
|
14
18
|
end
|