licensed 3.0.0 → 3.2.1

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +19 -0
  3. data/.github/workflows/release.yml +4 -4
  4. data/.github/workflows/test.yml +180 -47
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +60 -1
  7. data/README.md +25 -79
  8. data/docker/Dockerfile.build-linux +1 -1
  9. data/docs/adding_a_new_source.md +11 -8
  10. data/docs/commands/README.md +59 -0
  11. data/docs/commands/cache.md +35 -0
  12. data/docs/commands/env.md +10 -0
  13. data/docs/commands/list.md +23 -0
  14. data/docs/commands/migrate.md +10 -0
  15. data/docs/commands/notices.md +12 -0
  16. data/docs/commands/status.md +74 -0
  17. data/docs/commands/version.md +3 -0
  18. data/docs/configuration/README.md +11 -0
  19. data/docs/configuration/allowed_licenses.md +17 -0
  20. data/docs/configuration/application_name.md +63 -0
  21. data/docs/configuration/application_source.md +64 -0
  22. data/docs/configuration/configuration_root.md +27 -0
  23. data/docs/configuration/configuring_multiple_apps.md +58 -0
  24. data/docs/configuration/dependency_source_enumerators.md +28 -0
  25. data/docs/configuration/ignoring_dependencies.md +19 -0
  26. data/docs/configuration/metadata_cache.md +106 -0
  27. data/docs/configuration/reviewing_dependencies.md +18 -0
  28. data/docs/configuration.md +9 -161
  29. data/docs/sources/swift.md +4 -0
  30. data/lib/licensed/cli.rb +2 -2
  31. data/lib/licensed/commands/cache.rb +19 -20
  32. data/lib/licensed/commands/command.rb +104 -72
  33. data/lib/licensed/commands/environment.rb +12 -11
  34. data/lib/licensed/commands/list.rb +0 -19
  35. data/lib/licensed/commands/notices.rb +0 -19
  36. data/lib/licensed/commands/status.rb +13 -15
  37. data/lib/licensed/configuration.rb +105 -12
  38. data/lib/licensed/report.rb +44 -0
  39. data/lib/licensed/reporters/cache_reporter.rb +48 -64
  40. data/lib/licensed/reporters/json_reporter.rb +19 -21
  41. data/lib/licensed/reporters/list_reporter.rb +45 -58
  42. data/lib/licensed/reporters/notices_reporter.rb +33 -46
  43. data/lib/licensed/reporters/reporter.rb +37 -104
  44. data/lib/licensed/reporters/status_reporter.rb +58 -56
  45. data/lib/licensed/reporters/yaml_reporter.rb +19 -21
  46. data/lib/licensed/sources/bundler/definition.rb +36 -0
  47. data/lib/licensed/sources/bundler/missing_specification.rb +1 -1
  48. data/lib/licensed/sources/bundler.rb +38 -86
  49. data/lib/licensed/sources/dep.rb +2 -2
  50. data/lib/licensed/sources/go.rb +3 -3
  51. data/lib/licensed/sources/gradle.rb +2 -2
  52. data/lib/licensed/sources/helpers/content_versioning.rb +2 -1
  53. data/lib/licensed/sources/npm.rb +4 -3
  54. data/lib/licensed/sources/nuget.rb +56 -27
  55. data/lib/licensed/sources/swift.rb +69 -0
  56. data/lib/licensed/sources.rb +1 -0
  57. data/lib/licensed/version.rb +1 -1
  58. data/lib/licensed.rb +1 -0
  59. data/licensed.gemspec +4 -4
  60. data/script/source-setup/go +1 -1
  61. data/script/source-setup/swift +22 -0
  62. metadata +48 -13
  63. data/docs/commands.md +0 -95
@@ -29,12 +29,6 @@ module Licensed
29
29
  end
30
30
  end
31
31
 
32
- def run(**options)
33
- super do |report|
34
- report["git_repo"] = Licensed::Git.git_repo?
35
- end
36
- end
37
-
38
32
  # Returns the default reporter to use during the command run
39
33
  #
40
34
  # options - The options the command was run with
@@ -46,11 +40,18 @@ module Licensed
46
40
 
47
41
  protected
48
42
 
49
- def run_app(app)
50
- reporter.report_app(app) do |report|
51
- report.merge! AppEnvironment.new(app).to_h
52
- true
53
- end
43
+ def run_command(report)
44
+ report["git_repo"] = Licensed::Git.git_repo?
45
+ super
46
+ end
47
+
48
+ def run_app(app, report)
49
+ report.merge! AppEnvironment.new(app).to_h
50
+ super
51
+ end
52
+
53
+ def run_source(app, source, report)
54
+ true
54
55
  end
55
56
  end
56
57
  end
@@ -13,25 +13,6 @@ module Licensed
13
13
 
14
14
  protected
15
15
 
16
- # Run the command for all enumerated dependencies found in a dependency source,
17
- # recording results in a report.
18
- # Enumerating dependencies in the source is skipped if a :sources option
19
- # is provided and the evaluated `source.class.type` is not in the :sources values
20
- #
21
- # app - The application configuration for the source
22
- # source - A dependency source enumerator
23
- #
24
- # Returns whether the command succeeded for the dependency source enumerator
25
- def run_source(app, source)
26
- super do |report|
27
- next if Array(options[:sources]).empty?
28
- next if options[:sources].include?(source.class.type)
29
-
30
- report.warnings << "skipped source"
31
- :skip
32
- end
33
- end
34
-
35
16
  # Listing dependencies requires no extra work.
36
17
  #
37
18
  # app - The application configuration for the dependency
@@ -13,25 +13,6 @@ module Licensed
13
13
 
14
14
  protected
15
15
 
16
- # Run the command for all enumerated dependencies found in a dependency source,
17
- # recording results in a report.
18
- # Enumerating dependencies in the source is skipped if a :sources option
19
- # is provided and the evaluated `source.class.type` is not in the :sources values
20
- #
21
- # app - The application configuration for the source
22
- # source - A dependency source enumerator
23
- #
24
- # Returns whether the command succeeded for the dependency source enumerator
25
- def run_source(app, source)
26
- super do |report|
27
- next if Array(options[:sources]).empty?
28
- next if options[:sources].include?(source.class.type)
29
-
30
- report.warnings << "skipped source"
31
- :skip
32
- end
33
- end
34
-
35
16
  # Load stored dependency record data to add to the notices report.
36
17
  #
37
18
  # app - The application configuration for the dependency
@@ -15,22 +15,17 @@ module Licensed
15
15
 
16
16
  protected
17
17
 
18
- # Run the command for all enumerated dependencies found in a dependency source,
19
- # recording results in a report.
20
- # Enumerating dependencies in the source is skipped if a :sources option
21
- # is provided and the evaluated `source.class.type` is not in the :sources values
18
+ # Run the comand and set an error message to review the documentation
19
+ # when any errors have been reported
22
20
  #
23
- # app - The application configuration for the source
24
- # source - A dependency source enumerator
21
+ # report - A Licensed::Report object for this command
25
22
  #
26
- # Returns whether the command succeeded for the dependency source enumerator
27
- def run_source(app, source)
28
- super do |report|
29
- next if Array(options[:sources]).empty?
30
- next if options[:sources].include?(source.class.type)
31
-
32
- report.warnings << "skipped source"
33
- :skip
23
+ # Returns whether the command succeeded based on the call to super
24
+ def run_command(report)
25
+ super do |result|
26
+ next if result
27
+
28
+ report.errors << "Licensed found errors during source enumeration. Please see https://github.com/github/licensed/tree/master/docs/commands/status.md#status-errors-and-resolutions for possible resolutions."
34
29
  end
35
30
  end
36
31
 
@@ -47,11 +42,14 @@ module Licensed
47
42
  def evaluate_dependency(app, source, dependency, report)
48
43
  filename = app.cache_path.join(source.class.type, "#{dependency.name}.#{DependencyRecord::EXTENSION}")
49
44
  report["filename"] = filename
45
+ report["version"] = dependency.version
50
46
 
51
47
  cached_record = cached_record(filename)
52
48
  if cached_record.nil?
49
+ report["license"] = nil
53
50
  report.errors << "cached dependency record not found"
54
51
  else
52
+ report["license"] = cached_record["license"]
55
53
  report.errors << "cached dependency record out of date" if cached_record["version"] != dependency.version
56
54
  report.errors << "missing license text" if cached_record.licenses.empty?
57
55
  if cached_record["review_changed_license"]
@@ -61,7 +59,7 @@ module Licensed
61
59
  end
62
60
  end
63
61
 
64
- report.errors.empty?
62
+ report["allowed"] = report.errors.empty?
65
63
  end
66
64
 
67
65
  # Returns true if a cached record needs further review based on the
@@ -3,9 +3,14 @@ require "pathname"
3
3
 
4
4
  module Licensed
5
5
  class AppConfiguration < Hash
6
+ DIRECTORY_NAME_GENERATOR_KEY = "directory_name".freeze
7
+ RELATIVE_PATH_GENERATOR_KEY = "relative_path".freeze
8
+ DEFAULT_RELATIVE_PATH_NAME_SEPARATOR = "-".freeze
9
+ ALL_NAME_GENERATOR_KEYS = [DIRECTORY_NAME_GENERATOR_KEY, RELATIVE_PATH_GENERATOR_KEY].freeze
10
+
6
11
  DEFAULT_CACHE_PATH = ".licenses".freeze
7
12
 
8
- # Returns the root for a configuration in following order of precendence:
13
+ # Returns the root for a configuration in following order of precedence:
9
14
  # 1. explicitly configured "root" property
10
15
  # 2. a found git repository root
11
16
  # 3. the current directory
@@ -28,9 +33,9 @@ module Licensed
28
33
  self["ignored"] ||= {}
29
34
  self["allowed"] ||= []
30
35
  self["root"] = AppConfiguration.root_for(self)
31
- # defaults to the directory name of the source path if not set
32
- self["name"] ||= File.basename(self["source_path"])
33
- # setting the cache path might need a valid app name
36
+ self["name"] = generate_app_name
37
+ # setting the cache path might need a valid app name.
38
+ # this must come after setting self["name"]
34
39
  self["cache_path"] = detect_cache_path(options, inherited_options)
35
40
  end
36
41
 
@@ -105,8 +110,9 @@ module Licensed
105
110
 
106
111
  # Returns the cache path for the application based on:
107
112
  # 1. An explicitly set cache path for the application, if set
108
- # 2. An inherited root cache path joined with the app name
109
- # 3. The default cache path joined with the app name
113
+ # 2. An inherited shared cache path
114
+ # 3. An inherited cache path joined with the app name if not shared
115
+ # 4. The default cache path joined with the app name
110
116
  def detect_cache_path(options, inherited_options)
111
117
  return options["cache_path"] unless options["cache_path"].to_s.empty?
112
118
 
@@ -124,6 +130,65 @@ module Licensed
124
130
  raise Licensed::Configuration::LoadError,
125
131
  "App #{self["name"]} is missing required property #{property}"
126
132
  end
133
+
134
+ # Returns a name for the application as one of:
135
+ # 1. An explicitly configured app name, if set
136
+ # 2. A generated app name based on an configured "name" options hash
137
+ # 3. A default value - the source_path directory name
138
+ def generate_app_name
139
+ # use default_app_name if a name value is not set
140
+ return source_path_directory_app_name if self["name"].to_s.empty?
141
+ # keep the same name value unless a hash is given with naming options
142
+ return self["name"] unless self["name"].is_a?(Hash)
143
+
144
+ generator = self.dig("name", "generator")
145
+ case generator
146
+ when nil, DIRECTORY_NAME_GENERATOR_KEY
147
+ source_path_directory_app_name
148
+ when RELATIVE_PATH_GENERATOR_KEY
149
+ relative_path_app_name
150
+ else
151
+ raise Licensed::Configuration::LoadError,
152
+ "Invalid value configured for name.generator: #{generator}. Value must be one of #{ALL_NAME_GENERATOR_KEYS.join(",")}"
153
+ end
154
+ end
155
+
156
+ # Returns an app name from the directory name of the configured source path
157
+ def source_path_directory_app_name
158
+ File.basename(self["source_path"])
159
+ end
160
+
161
+ # Returns an app name from the relative path from the configured app root
162
+ # to the configured app source path.
163
+ def relative_path_app_name
164
+ source_path_parts = File.expand_path(self["source_path"]).split("/")
165
+ root_path_parts = File.expand_path(self["root"]).split("/")
166
+
167
+ # if the source path is equivalent to the root path,
168
+ # return the root directory name
169
+ return root_path_parts[-1] if source_path_parts == root_path_parts
170
+
171
+ if source_path_parts[0..root_path_parts.size-1] != root_path_parts
172
+ raise Licensed::Configuration::LoadError,
173
+ "source_path must be a descendent of the app root to generate an app name from the relative source_path"
174
+ end
175
+
176
+ name_parts = source_path_parts[root_path_parts.size..-1]
177
+
178
+ separator = self.dig("name", "separator") || DEFAULT_RELATIVE_PATH_NAME_SEPARATOR
179
+ depth = self.dig("name", "depth") || 0
180
+ if depth < 0
181
+ raise Licensed::Configuration::LoadError, "name.depth configuration value cannot be less than -1"
182
+ end
183
+
184
+ # offset the depth value by -1 to work as an offset from the end of the array
185
+ # 0 becomes -1, with a start index of (-1 - -1) = 0, or the full array
186
+ # 1 becomes 0, with a start index of (-1 - 0) = -1, or only the last element
187
+ # and so on...
188
+ depth = depth - 1
189
+ start_index = depth >= name_parts.length ? 0 : -1 - depth
190
+ name_parts[start_index..-1].join(separator)
191
+ end
127
192
  end
128
193
 
129
194
  class Configuration
@@ -151,6 +216,11 @@ module Licensed
151
216
  def initialize(options = {})
152
217
  apps = options.delete("apps") || []
153
218
  apps << default_options.merge(options) if apps.empty?
219
+
220
+ # apply a root setting to all app configurations so that it's available
221
+ # when expanding app source paths
222
+ apps.each { |app| app["root"] ||= options["root"] if options["root"] }
223
+
154
224
  apps = apps.flat_map { |app| self.class.expand_app_source_path(app) }
155
225
  @apps = apps.map { |app| AppConfiguration.new(app, options) }
156
226
  end
@@ -158,25 +228,48 @@ module Licensed
158
228
  private
159
229
 
160
230
  def self.expand_app_source_path(app_config)
161
- return app_config if app_config["source_path"].to_s.empty?
231
+ # map a source_path configuration value to an array of non-empty values
232
+ source_path_array = Array(app_config["source_path"])
233
+ .reject { |path| path.to_s.empty? }
234
+ .compact
235
+ app_root = AppConfiguration.root_for(app_config)
236
+ return app_config.merge("source_path" => app_root) if source_path_array.empty?
162
237
 
163
238
  # check if the source path maps to an existing directory
164
- source_path = File.expand_path(app_config["source_path"], AppConfiguration.root_for(app_config))
165
- return app_config if Dir.exist?(source_path)
239
+ if source_path_array.length == 1
240
+ source_path = File.expand_path(source_path_array[0], app_root)
241
+ return app_config.merge("source_path" => source_path) if Dir.exist?(source_path)
242
+ end
166
243
 
167
244
  # try to expand the source path for glob patterns
168
- expanded_source_paths = Dir.glob(source_path).select { |p| File.directory?(p) }
245
+ expanded_source_paths = source_path_array.reduce(Set.new) do |matched_paths, pattern|
246
+ current_matched_paths = if pattern.start_with?("!")
247
+ # if the pattern is an exclusion, remove all matching files
248
+ # from the result
249
+ matched_paths - Dir.glob(pattern[1..-1])
250
+ else
251
+ # if the pattern is an inclusion, add all matching files
252
+ # to the result
253
+ matched_paths + Dir.glob(pattern)
254
+ end
255
+
256
+ current_matched_paths.select { |p| File.directory?(p) }
257
+ end
258
+
169
259
  configs = expanded_source_paths.map { |path| app_config.merge("source_path" => path) }
170
260
 
171
261
  # if no directories are found for the source path, return the original config
172
- return app_config if configs.size == 0
262
+ if configs.size == 0
263
+ app_config["source_path"] = app_root if app_config["source_path"].is_a?(Array)
264
+ return app_config
265
+ end
173
266
 
174
267
  # update configured values for name and cache_path for uniqueness.
175
268
  # this is only needed when values are explicitly set, AppConfiguration
176
269
  # will handle configurations that don't have these explicitly set
177
270
  configs.each do |config|
178
271
  dir_name = File.basename(config["source_path"])
179
- config["name"] = "#{config["name"]}-#{dir_name}" if config["name"]
272
+ config["name"] = "#{config["name"]}-#{dir_name}" if config["name"].is_a?(String)
180
273
 
181
274
  # if a cache_path is set and is not marked as shared, append the app name
182
275
  # to the end of the cache path to make a unique cache path for the app
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Licensed
4
+ class Report < Hash
5
+ attr_reader :name
6
+ attr_reader :target
7
+ def initialize(name:, target:)
8
+ super()
9
+ @name = name
10
+ @target = target
11
+ end
12
+
13
+ def reports
14
+ @reports ||= []
15
+ end
16
+
17
+ def errors
18
+ @errors ||= []
19
+ end
20
+
21
+ def warnings
22
+ @warnings ||= []
23
+ end
24
+
25
+ def all_reports
26
+ result = []
27
+ result << self
28
+ result.push(*reports.flat_map(&:all_reports))
29
+ end
30
+
31
+ # Returns the data from the report as a hash
32
+ def to_h
33
+ # add name, errors and warnings if they have real data
34
+ output = {}
35
+ output["name"] = name unless name.to_s.empty?
36
+ output["errors"] = errors.dup if errors.any?
37
+ output["warnings"] = warnings.dup if warnings.any?
38
+
39
+ # merge the hash data from the report. command-specified data always
40
+ # overwrites local data
41
+ output.merge(super)
42
+ end
43
+ end
44
+ end
@@ -2,89 +2,73 @@
2
2
  module Licensed
3
3
  module Reporters
4
4
  class CacheReporter < Reporter
5
- # Reports on an application configuration in a cache command run
5
+ # Reports the start of caching records for an app
6
6
  #
7
7
  # app - An application configuration
8
- #
9
- # Returns the result of the yielded method
10
- # Note - must be called from inside the `report_run` scope
11
- def report_app(app)
12
- super do |report|
13
- shell.info "Caching dependency records for #{app["name"]}"
14
- yield report
15
- end
8
+ # report - A report containing information about the app evaluation
9
+ def begin_report_app(app, report)
10
+ shell.info "Caching dependency records for #{app["name"]}"
16
11
  end
17
12
 
18
- # Reports on a dependency source enumerator in a cache command run.
19
- # Shows the type and count of dependencies found by the source.
13
+ # Reports the start of caching records for a dependency source
20
14
  #
21
15
  # source - A dependency source enumerator
22
- #
23
- # Returns the result of the yielded method
24
- # Note - must be called from inside the `report_run` scope
25
- def report_source(source)
26
- super do |report|
27
- shell.info " #{source.class.type}"
28
- result = yield report
16
+ # report - A report containing information about the source evaluation
17
+ def begin_report_source(source, report)
18
+ shell.info " #{source.class.type}"
19
+ end
29
20
 
30
- warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
31
- if warning_reports.any?
32
- shell.newline
33
- shell.warn " * Warnings:"
34
- warning_reports.each do |r|
35
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
21
+ # Reports warnings and errors found while evaluating the dependency source
22
+ #
23
+ # source - A dependency source enumerator
24
+ # report - A report containing information about the source evaluation
25
+ def end_report_source(source, report)
26
+ warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
27
+ if warning_reports.any?
28
+ shell.newline
29
+ shell.warn " * Warnings:"
30
+ warning_reports.each do |r|
31
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
36
32
 
37
- shell.warn " * #{r.name}"
38
- shell.warn " #{display_metadata}" unless display_metadata.empty?
39
- r.warnings.each do |warning|
40
- shell.warn " - #{warning}"
41
- end
42
- shell.newline
33
+ shell.warn " * #{r.name}"
34
+ shell.warn " #{display_metadata}" unless display_metadata.empty?
35
+ r.warnings.each do |warning|
36
+ shell.warn " - #{warning}"
43
37
  end
38
+ shell.newline
44
39
  end
40
+ end
45
41
 
46
- errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
47
- if errored_reports.any?
48
- shell.newline
49
- shell.error " * Errors:"
50
- errored_reports.each do |r|
51
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
42
+ errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
43
+ if errored_reports.any?
44
+ shell.newline
45
+ shell.error " * Errors:"
46
+ errored_reports.each do |r|
47
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
52
48
 
53
- shell.error " * #{r.name}"
54
- shell.error " #{display_metadata}" unless display_metadata.empty?
55
- r.errors.each do |error|
56
- shell.error " - #{error}"
57
- end
58
- shell.newline
49
+ shell.error " * #{r.name}"
50
+ shell.error " #{display_metadata}" unless display_metadata.empty?
51
+ r.errors.each do |error|
52
+ shell.error " - #{error}"
59
53
  end
60
- else
61
- shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
54
+ shell.newline
62
55
  end
63
-
64
- result
56
+ else
57
+ shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
65
58
  end
66
59
  end
67
60
 
68
- # Reports on a dependency in a cache command run.
69
- # Shows whether the dependency's record was cached or reused.
61
+ # Reports whether the dependency's record was cached or reused.
70
62
  #
71
63
  # dependency - An application dependency
72
- #
73
- # Returns the result of the yielded method
74
- # Note - must be called from inside the `report_run` scope
75
- def report_dependency(dependency)
76
- super do |report|
77
- result = yield report
78
-
79
- if report.errors.any?
80
- shell.error " Error #{dependency.name} (#{dependency.version})"
81
- elsif report["cached"]
82
- shell.info " Caching #{dependency.name} (#{dependency.version})"
83
- else
84
- shell.info " Using #{dependency.name} (#{dependency.version})"
85
- end
86
-
87
- result
64
+ # report - A report containing information about the dependency evaluation
65
+ def end_report_dependency(dependency, report)
66
+ if report.errors.any?
67
+ shell.error " Error #{dependency.name} (#{dependency.version})"
68
+ elsif report["cached"]
69
+ shell.info " Caching #{dependency.name} (#{dependency.version})"
70
+ else
71
+ shell.info " Using #{dependency.name} (#{dependency.version})"
88
72
  end
89
73
  end
90
74
  end