spm_version_updates 1.1.1 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 922e9383223719cafb033ce65ca8b710013bc1654217973443b1b1b99eead693
4
- data.tar.gz: b6a5fa439183d0cfcb8192b050565020b8f314e18f573d88b8e4ce40fb619bfb
3
+ metadata.gz: 53afb3742bddcef276c343767b4570a4d428cefe7560ba2460d461dfcf66c3c1
4
+ data.tar.gz: bde5dd5228e9c7ae5188efa42ef0343bc1def8b45bb22f16a9ce343fb9f94e4e
5
5
  SHA512:
6
- metadata.gz: cc90410091871b58726de87d0f8e992e3abf08c75799dc1901c1afd893ca5736f39c02076b136523a3ba437a5796e51cf196abcd728e513b4e1e27dd9332945f
7
- data.tar.gz: a7329c84f09b9615cf25872e4e3181694b306e717dc978b36b8edbd8334543339f68d4bd2402ae84c15e425cad2005e6ace00475c41598c36ca81309f1f3f95a
6
+ metadata.gz: 80f629e2ef3cc2c5a2162a61cedf485948bbd88616451d666b82776fc4034744cd2b9cb8d78c2c02628446f32027206e9d1913b0fe495ca51cf53619b599ede1
7
+ data.tar.gz: 50adb15cd7e1703487bc386a7f0bc81d022698d172ece4008b20ab33b9602c578bf33622a5a26340c713d1e02a5fd5af2d0d7fb9ec2db8fbcfe083ba8f3ace0a
data/README.md CHANGED
@@ -42,6 +42,92 @@ Behavior is configurable through accessors on `SpmChecker` — for example
42
42
  `report_pre_releases`, `ignore_repos`, and allow-host restrictions. See the
43
43
  class documentation for the full list.
44
44
 
45
+ ## Errors
46
+
47
+ Everything the gem raises descends from one of two roots (defined in
48
+ `lib/spm_version_updates/errors.rb`), so callers can rescue by failure
49
+ category instead of enumerating concrete classes:
50
+
51
+ - **`SpmVersionUpdates::Error < StandardError`**
52
+ - **`FileNotFoundError`** — a required file is missing:
53
+ - `ManifestParser::CouldNotFindManifest` — a `Package.swift` path does
54
+ not exist.
55
+ - `ManifestParser::CouldNotFindResolvedFile` — an expected
56
+ `Package.resolved` is missing in manifest mode; the message names the
57
+ missing file(s). Raised rather than silently reporting incomplete
58
+ results.
59
+ - `XcodeParser::CouldNotFindResolvedFile` — no `Package.resolved` was
60
+ found in the Xcode workspace locations.
61
+ - **`ParseError`** — a file exists but could not be read:
62
+ - `PackageResolved::MalformedFileError` — a corrupt or unrecognized
63
+ `Package.resolved`.
64
+ - **`NetworkError`** — git lookup failures:
65
+ - `GitOperations::LsRemoteError` — `git ls-remote` failed after bounded
66
+ retries (unreachable host, authentication failure). Messages are
67
+ credential-redacted.
68
+ - **`PolicyError`** — security-gate violations:
69
+ - `SpmChecker::DisallowedRepositoryHost` — `allow_hosts` is configured
70
+ and a dependency's host is not on the list. Raised before git is
71
+ contacted.
72
+ - **`SpmVersionUpdates::ConfigurationError < ArgumentError`** — invalid
73
+ caller-supplied configuration: `ManifestParser::ManifestPathMustBeSet`,
74
+ `XcodeParser::XcodeprojPathMustBeSet`, `allow_hosts` entries that don't
75
+ parse as hostnames, and every invalid repo-rules YAML shape. It inherits
76
+ `ArgumentError` (not `Error`) so existing callers that rescue
77
+ `ArgumentError` keep working — rescue it alongside
78
+ `SpmVersionUpdates::Error` when catching everything the gem raises:
79
+
80
+ ```ruby
81
+ begin
82
+ checker.check_manifests(["Modules/Package.swift"])
83
+ rescue SpmVersionUpdates::ConfigurationError, SpmVersionUpdates::Error => error
84
+ abort(error.message)
85
+ end
86
+ ```
87
+
88
+ ## Continuing past per-dependency failures
89
+
90
+ By default, the first failed git lookup or malformed `Package.resolved`
91
+ raises and aborts the run. Two optional handlers turn those into callbacks so
92
+ the remaining dependencies keep being checked:
93
+
94
+ ```ruby
95
+ # Called as (package, error) instead of raising GitOperations::LsRemoteError.
96
+ # A dependency shared by several manifests is reported only once per run.
97
+ checker.lookup_failure_handler = ->(package, error) {
98
+ puts("Skipping #{package.name}: #{error.message}")
99
+ }
100
+
101
+ # Called as (resolved_path, error) instead of raising
102
+ # PackageResolved::MalformedFileError; the file's pins are skipped.
103
+ checker.malformed_resolved_handler = ->(path, error) {
104
+ puts("Ignoring #{path}: #{error.message}")
105
+ }
106
+ ```
107
+
108
+ ## Parse warnings
109
+
110
+ A `.package(...)` declaration whose version requirement isn't recognized (or
111
+ that has unbalanced parentheses) is skipped rather than guessed at. Each skip
112
+ is recorded on the checker — separate from update warnings, so update counts
113
+ and fail-on thresholds are unaffected:
114
+
115
+ ```ruby
116
+ checker.check_manifests(["Modules/Package.swift"])
117
+
118
+ checker.parse_warnings.each do |record|
119
+ # String-keyed hash: "type" ("parse_warning"), "reason", "source"
120
+ # (the manifest path), "snippet" (credential-redacted, truncated),
121
+ # and a human-readable "message".
122
+ puts(record["message"])
123
+ puts(ParseWarning.describe_reason(record)) # the reason as a readable phrase
124
+ puts(ParseWarning.issue_link(record)) # pre-filled GitHub new-issue URL
125
+ end
126
+ ```
127
+
128
+ The snippet is redacted and shown in the report only — it is never embedded
129
+ in the issue URL, where it could leak private repository URLs.
130
+
45
131
  ## License
46
132
 
47
133
  MIT — see [LICENSE.txt](LICENSE.txt).
@@ -4,6 +4,7 @@ require_relative "git_host_normalizer"
4
4
  require_relative "git_operations"
5
5
 
6
6
  # Normalizes user-provided allow-host entries into hostnames.
7
+ # @api private
7
8
  class AllowHostNormalizer
8
9
  MALFORMED_SCHEME_PATTERN = %r{\A[a-z][a-z0-9+\-.]*//}i
9
10
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Category base classes for every error raised by spm_version_updates, so
4
+ # callers can rescue by failure kind instead of enumerating each concrete
5
+ # class. The concrete classes (e.g. ManifestParser::CouldNotFindManifest)
6
+ # keep their existing names and namespaces; only their superclasses point
7
+ # here.
8
+ module SpmVersionUpdates
9
+ # Base class for all errors raised by spm_version_updates.
10
+ class Error < StandardError; end
11
+
12
+ # Invalid user-supplied configuration or inputs. Inherits ArgumentError (not
13
+ # Error) so existing callers that rescue ArgumentError keep working; rescue
14
+ # it alongside Error when catching everything this gem raises.
15
+ class ConfigurationError < ArgumentError; end
16
+
17
+ # A required file (manifest, Package.resolved) could not be found.
18
+ class FileNotFoundError < Error; end
19
+
20
+ # A file exists but could not be parsed.
21
+ class ParseError < Error; end
22
+
23
+ # git or network lookup failures.
24
+ class NetworkError < Error; end
25
+
26
+ # Policy violations, e.g. a repository host blocked by allow-hosts.
27
+ class PolicyError < Error; end
28
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "errors"
3
4
  require_relative "update_severity"
4
5
 
5
6
  # Parses fail-on inputs and evaluates whether reported updates should fail.
@@ -34,7 +35,7 @@ module FailOnThreshold
34
35
  return ANY if normalized == "true"
35
36
  return normalized if UpdateSeverity.threshold?(normalized)
36
37
 
37
- raise(ArgumentError, "#{input_name} must be false, true, major, minor, or patch")
38
+ raise(SpmVersionUpdates::ConfigurationError, "#{input_name} must be false, true, major, minor, or patch")
38
39
  end
39
40
 
40
41
  def self.build_message(threshold, count)
@@ -4,6 +4,7 @@ require "ipaddr"
4
4
  require "uri"
5
5
 
6
6
  # Extracts and normalizes hostnames from common git remote URL forms.
7
+ # @api private
7
8
  module GitHostNormalizer
8
9
  HOST_PATTERN = /\A[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\z/i
9
10
  BRACKETED_IPV6_PATTERN = /\A\[(?<address>[^\]]+)\](?::\d+)?\z/
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "open3"
4
4
  require_relative "credential_redactor"
5
+ require_relative "errors"
5
6
  require_relative "git_host_normalizer"
6
7
  require_relative "semver"
7
8
 
@@ -16,7 +17,7 @@ module GitOperations
16
17
  TAG_REF_PATTERNS = ["[0-9]*.[0-9]*", "v[0-9]*.[0-9]*"].freeze
17
18
 
18
19
  # Raised when git cannot complete a remote reference lookup.
19
- class LsRemoteError < StandardError; end
20
+ class LsRemoteError < SpmVersionUpdates::NetworkError; end
20
21
 
21
22
  # Removes protocol and trailing .git from a repo URL
22
23
  # @param [String] repo_url The URL of the repository
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "errors"
3
4
  require_relative "git_operations"
4
5
  require_relative "package_resolved"
5
6
 
@@ -31,20 +32,26 @@ module ManifestParser
31
32
  # scheme-bearing `repository_url` is retained for git operations.
32
33
  #
33
34
  # @param [String] manifest_path The path to a `Package.swift` file
35
+ # @yield [Hash] optionally receives `{ reason:, snippet: }` for each
36
+ # `.package(...)` declaration that had to be skipped, so callers can
37
+ # surface parse warnings instead of dropping dependencies silently
34
38
  # @raise [ManifestPathMustBeSet] if the manifest_path is blank
35
39
  # @raise [CouldNotFindManifest] if the file does not exist
36
40
  # @return [Hash<String, Hash>] normalized URL => { "repository_url", "requirement" }
37
- def self.get_packages(manifest_path)
41
+ def self.get_packages(manifest_path, &on_skip)
38
42
  raise(ManifestPathMustBeSet) if manifest_path.nil? || manifest_path.empty?
39
43
  raise(CouldNotFindManifest, manifest_path) unless File.exist?(manifest_path)
40
44
 
41
45
  content = strip_comments(File.read(manifest_path))
42
- package_calls(content).each_with_object({}) { |call, packages|
46
+ package_calls(content, &on_skip).each_with_object({}) { |call, packages|
43
47
  url = call[/\burl\s*:\s*"([^"]+)"/, 1]
44
48
  next if url.nil? # local package (path:) or otherwise unrecognized
45
49
 
46
50
  requirement = requirement_for(call)
47
- next if requirement.nil?
51
+ if requirement.nil?
52
+ on_skip&.call({ reason: "unrecognized_requirement", snippet: call })
53
+ next
54
+ end
48
55
 
49
56
  packages[GitOperations.trim_repo_url(url)] = { "repository_url" => url, "requirement" => requirement }
50
57
  }
@@ -69,15 +76,21 @@ module ManifestParser
69
76
  # Extract the argument body of each `.package( ... )` call, honoring nested
70
77
  # parentheses (e.g. `.upToNextMajor(from: "1.0.0")`) and string literals.
71
78
  #
79
+ # An unclosed call cannot be skipped safely (there is no closing paren to
80
+ # resume after), so scanning stops there; the skip callback says so.
81
+ #
72
82
  # @param [String] content The (comment-stripped) manifest source
73
83
  # @return [Array<String>]
74
- def self.package_calls(content)
84
+ def self.package_calls(content, &on_skip)
75
85
  calls = []
76
86
  search_start = 0
77
87
  while (marker_index = content.index(PACKAGE_CALL, search_start))
78
88
  open_index = marker_index + PACKAGE_CALL.length - 1
79
89
  close_index = matching_paren(content, open_index)
80
- break if close_index.nil?
90
+ if close_index.nil?
91
+ on_skip&.call({ reason: "unbalanced_parentheses", snippet: content[marker_index, 300] })
92
+ break
93
+ end
81
94
 
82
95
  calls << content[(open_index + 1)...close_index]
83
96
  search_start = close_index + 1
@@ -245,14 +258,26 @@ module ManifestParser
245
258
  :skip_block_comment
246
259
 
247
260
  # Raised when manifest mode is invoked without a manifest path.
248
- class ManifestPathMustBeSet < StandardError
261
+ class ManifestPathMustBeSet < SpmVersionUpdates::ConfigurationError
262
+ def initialize(message = "package-manifest-paths must be set")
263
+ super
264
+ end
249
265
  end
250
266
 
251
267
  # Raised when a configured Package.swift manifest is missing.
252
- class CouldNotFindManifest < StandardError
268
+ class CouldNotFindManifest < SpmVersionUpdates::FileNotFoundError
269
+ def initialize(path)
270
+ super("Could not find Package.swift manifest: #{path}")
271
+ end
253
272
  end
254
273
 
255
274
  # Raised when manifest mode cannot find an expected Package.resolved file.
256
- class CouldNotFindResolvedFile < StandardError
275
+ class CouldNotFindResolvedFile < SpmVersionUpdates::FileNotFoundError
276
+ def initialize(paths)
277
+ super(
278
+ "Could not find any Package.resolved file (looked in: #{paths}). " \
279
+ "Commit a Package.resolved next to each manifest or set package-resolved-paths."
280
+ )
281
+ end
257
282
  end
258
283
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "errors"
4
5
  require_relative "git_operations"
5
6
 
6
7
  # Parsing for `Package.resolved` files.
@@ -10,7 +11,7 @@ require_relative "git_operations"
10
11
  # and the Swift package manifest source mode.
11
12
  module PackageResolved
12
13
  # Raised when a `Package.resolved` file exists but is not valid JSON.
13
- class MalformedFileError < StandardError
14
+ class MalformedFileError < SpmVersionUpdates::ParseError
14
15
  attr_reader :path
15
16
 
16
17
  def initialize(path, parse_message)
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require_relative "credential_redactor"
5
+
6
+ # Builds the structured records used to report `.package(...)` declarations
7
+ # that the manifest parser had to skip, plus the pre-filled GitHub issue link
8
+ # shown alongside them. The manifest snippet is redacted and shown in the
9
+ # report only — never embedded in the issue URL, where it could leak private
10
+ # repository URLs through logs or referrer headers.
11
+ module ParseWarning
12
+ ISSUE_URL = "https://github.com/hbmartin/github-action-spm_version_updates/issues/new"
13
+ SNIPPET_LIMIT = 200
14
+
15
+ REASONS = {
16
+ "unrecognized_requirement" => "its version requirement was not recognized",
17
+ "unbalanced_parentheses" => "it has unbalanced parentheses, so the remainder of this manifest was not scanned"
18
+ }.freeze
19
+
20
+ # @param reason [String] a REASONS key
21
+ # @param source [String] the manifest path the declaration came from
22
+ # @param snippet [String] the raw declaration text (redacted and truncated here)
23
+ # @return [Hash] type / reason / source / snippet / message, string-keyed
24
+ def self.record(reason:, source:, snippet:)
25
+ reason = reason.to_s
26
+ {
27
+ "type" => "parse_warning",
28
+ "reason" => reason,
29
+ "source" => source,
30
+ "snippet" => truncated_snippet(snippet),
31
+ "message" => message_for(reason, source)
32
+ }
33
+ end
34
+
35
+ # A GitHub new-issue URL pre-filled with everything except the manifest
36
+ # content, which the template asks the reporter to paste in themselves.
37
+ # @param record [Hash] a {record} hash
38
+ # @return [String]
39
+ def self.issue_link(record)
40
+ reason = record["reason"]
41
+ query = URI.encode_www_form(
42
+ title: "Manifest parse failure: #{reason}",
43
+ body: issue_body(reason)
44
+ )
45
+ "#{ISSUE_URL}?#{query}"
46
+ end
47
+
48
+ # @param record [Hash] a {record} hash
49
+ # @return [String] the reason as a readable phrase
50
+ def self.describe_reason(record)
51
+ reason = record["reason"]
52
+ REASONS.fetch(reason, reason)
53
+ end
54
+
55
+ def self.message_for(reason, source)
56
+ "Could not parse a `.package(...)` declaration in #{source} because " \
57
+ "#{REASONS.fetch(reason, reason)}. Updates for the affected " \
58
+ "dependency were not checked. If this is valid Swift, please open an issue."
59
+ end
60
+
61
+ def self.truncated_snippet(snippet)
62
+ redacted = CredentialRedactor.redact(snippet.to_s.strip).to_s
63
+ return redacted if redacted.length <= SNIPPET_LIMIT
64
+
65
+ "#{redacted[0, SNIPPET_LIMIT]}…"
66
+ end
67
+
68
+ def self.issue_body(reason)
69
+ <<~BODY
70
+ A `.package(...)` declaration in my `Package.swift` could not be parsed (reason: #{reason}).
71
+
72
+ Please paste the declaration below (remove any credentials or private URLs first):
73
+
74
+ ```swift
75
+ (paste the .package(...) declaration here)
76
+ ```
77
+ BODY
78
+ end
79
+
80
+ private_class_method :message_for, :truncated_snippet, :issue_body
81
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require_relative "errors"
4
5
  require_relative "git_operations"
5
6
  require_relative "semver"
6
7
  require_relative "update_severity"
@@ -63,13 +64,13 @@ class RepositoryUpdateRules
63
64
  yaml_config = YAML.safe_load_file(path, permitted_classes: [], permitted_symbols: [], aliases: false) || {}
64
65
  from_hash(yaml_config, source: path)
65
66
  rescue Psych::Exception => error
66
- raise(ArgumentError, "repo-rules YAML is invalid in #{path}: #{error.message}")
67
+ raise(SpmVersionUpdates::ConfigurationError, "repo-rules YAML is invalid in #{path}: #{error.message}")
67
68
  end
68
69
 
69
70
  def self.from_hash(config = {}, source: "repo rules", **keyword_config)
70
71
  effective_config = keyword_config.empty? ? config : keyword_config
71
72
  effective_config ||= {}
72
- raise(ArgumentError, "#{source} must contain a YAML mapping") unless effective_config.kind_of?(Hash)
73
+ raise(SpmVersionUpdates::ConfigurationError, "#{source} must contain a YAML mapping") unless effective_config.kind_of?(Hash)
73
74
 
74
75
  new(parse_repositories(repositories_from(effective_config, source), source))
75
76
  end
@@ -84,7 +85,7 @@ class RepositoryUpdateRules
84
85
  repositories.each_with_object({}).with_index(1) { |(entry, rules), index|
85
86
  rule = parse_entry(entry, "#{source} repositories[#{index}]")
86
87
  normalized_url = rule.normalized_url
87
- raise(ArgumentError, "duplicate repo-rules entry for #{normalized_url}") if rules.key?(normalized_url)
88
+ raise(SpmVersionUpdates::ConfigurationError, "duplicate repo-rules entry for #{normalized_url}") if rules.key?(normalized_url)
88
89
 
89
90
  rules[normalized_url] = rule
90
91
  }
@@ -99,8 +100,8 @@ class RepositoryUpdateRules
99
100
 
100
101
  def self.validated_file_path(path)
101
102
  path = path.to_s.strip
102
- raise(ArgumentError, "repo-rules-path was set but no file path was provided") if path.empty?
103
- raise(ArgumentError, "repo-rules-path file does not exist: #{path}") unless File.file?(path)
103
+ raise(SpmVersionUpdates::ConfigurationError, "repo-rules-path was set but no file path was provided") if path.empty?
104
+ raise(SpmVersionUpdates::ConfigurationError, "repo-rules-path file does not exist: #{path}") unless File.file?(path)
104
105
 
105
106
  path
106
107
  end
@@ -109,13 +110,13 @@ class RepositoryUpdateRules
109
110
  string_keys = config.transform_keys(&:to_s)
110
111
  validate_keys!(string_keys, VALID_YAML_KEYS.fetch(:root), "#{source} root")
111
112
  repositories = string_keys.compact.fetch(yaml_key(:repositories), [])
112
- raise(ArgumentError, "#{source} repositories must be a list") unless repositories.kind_of?(Array)
113
+ raise(SpmVersionUpdates::ConfigurationError, "#{source} repositories must be a list") unless repositories.kind_of?(Array)
113
114
 
114
115
  repositories
115
116
  end
116
117
 
117
118
  def self.rule_entry_from(entry, source)
118
- raise(ArgumentError, "#{source} must be a mapping") unless entry.kind_of?(Hash)
119
+ raise(SpmVersionUpdates::ConfigurationError, "#{source} must be a mapping") unless entry.kind_of?(Hash)
119
120
 
120
121
  entry.transform_keys(&:to_s).tap { |string_keys| validate_keys!(string_keys, VALID_YAML_KEYS.fetch(:entry), source) }
121
122
  end
@@ -124,7 +125,7 @@ class RepositoryUpdateRules
124
125
  normalized_url = normalized_url_for(required_value(string_keys, yaml_key(:url), source))
125
126
  ignore_until = parse_ignore_until(string_keys, source)
126
127
  allowed_updates = parse_allowed_updates(string_keys, source)
127
- raise(ArgumentError, "#{source} must set #{yaml_key(:ignore_until)} or #{yaml_key(:allowed_updates)}") unless ignore_until || allowed_updates
128
+ raise(SpmVersionUpdates::ConfigurationError, "#{source} must set #{yaml_key(:ignore_until)} or #{yaml_key(:allowed_updates)}") unless ignore_until || allowed_updates
128
129
 
129
130
  { normalized_url:, ignore_until:, allowed_updates: }
130
131
  end
@@ -133,19 +134,19 @@ class RepositoryUpdateRules
133
134
  unknown = values.keys - allowed
134
135
  return if unknown.empty?
135
136
 
136
- raise(ArgumentError, "#{source} contains unknown key(s): #{unknown.join(', ')}")
137
+ raise(SpmVersionUpdates::ConfigurationError, "#{source} contains unknown key(s): #{unknown.join(', ')}")
137
138
  end
138
139
 
139
140
  def self.required_value(values, key, source)
140
141
  value = values[key].to_s.strip
141
- raise(ArgumentError, "#{source} #{key} must be set") if value.empty?
142
+ raise(SpmVersionUpdates::ConfigurationError, "#{source} #{key} must be set") if value.empty?
142
143
 
143
144
  value
144
145
  end
145
146
 
146
147
  def self.normalized_url_for(value)
147
148
  normalized = GitOperations.trim_repo_url(value)
148
- raise(ArgumentError, "repo-rules url must normalize to a repository URL") if normalized.empty?
149
+ raise(SpmVersionUpdates::ConfigurationError, "repo-rules url must normalize to a repository URL") if normalized.empty?
149
150
 
150
151
  normalized
151
152
  end
@@ -155,7 +156,7 @@ class RepositoryUpdateRules
155
156
  return unless values.key?(key)
156
157
 
157
158
  semver(values[key].to_s.strip).tap { |version|
158
- raise(ArgumentError, "#{source} #{key} must be a semantic version") unless version
159
+ raise(SpmVersionUpdates::ConfigurationError, "#{source} #{key} must be a semantic version") unless version
159
160
  }
160
161
  end
161
162
 
@@ -166,7 +167,7 @@ class RepositoryUpdateRules
166
167
  value = values[key].to_s.strip.downcase
167
168
  return value if SEVERITY_RANK.key?(value)
168
169
 
169
- raise(ArgumentError, "#{source} #{key} must be patch, minor, or major")
170
+ raise(SpmVersionUpdates::ConfigurationError, "#{source} #{key} must be patch, minor, or major")
170
171
  end
171
172
 
172
173
  def self.record_value(record, key)
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "semverify"
4
4
 
5
+ # Namespace for the core gem's published constants (gem version and the
6
+ # {SpmVersionUpdates::Semver} value object).
5
7
  module SpmVersionUpdates
6
8
  # SemVer value object used by both the GitHub Action and legacy Danger plugin.
7
9
  class Semver
@@ -2,9 +2,11 @@
2
2
 
3
3
  require_relative "allow_host_normalizer"
4
4
  require_relative "credential_redactor"
5
+ require_relative "errors"
5
6
  require_relative "git_operations"
6
7
  require_relative "manifest_parser"
7
8
  require_relative "package_resolved"
9
+ require_relative "parse_warning"
8
10
  require_relative "repository_update_rules"
9
11
  require_relative "semver"
10
12
  require_relative "spm_package_context"
@@ -18,13 +20,18 @@ class SpmChecker
18
20
  VERSION_TAG_WORKER_COUNT = 8
19
21
 
20
22
  # Raised when allow-hosts blocks a repository before git is contacted.
21
- class DisallowedRepositoryHost < StandardError; end
23
+ class DisallowedRepositoryHost < SpmVersionUpdates::PolicyError; end
22
24
 
23
25
  # Structured facts about each warning, used by the GitHub Action comment
24
26
  # renderer. `check_for_updates` and `check_manifests` still return the legacy
25
27
  # string warnings for compatibility with existing plugin-style callers.
26
28
  attr_reader :warning_details
27
29
 
30
+ # ParseWarning records for `.package(...)` declarations the manifest parser
31
+ # had to skip. Kept separate from update warnings so reported update counts
32
+ # and fail-on thresholds are unaffected.
33
+ attr_reader :parse_warnings
34
+
28
35
  attr_accessor :allow_hosts,
29
36
  :check_branches,
30
37
  :check_revisions,
@@ -60,6 +67,7 @@ class SpmChecker
60
67
  @allow_hosts = []
61
68
  @warnings = []
62
69
  @warning_details = []
70
+ @parse_warnings = []
63
71
  @version_tags_cache = {}
64
72
  @version_tag_lookup_errors = {}
65
73
  @reported_lookup_failures = {}
@@ -110,14 +118,17 @@ class SpmChecker
110
118
  puts("Found resolved versions for #{resolved_versions.size} packages")
111
119
 
112
120
  manifest_paths.each { |manifest_path|
113
- remote_packages = ManifestParser.get_packages(manifest_path)
114
- check_packages(remote_packages, resolved_versions, manifest_path)
121
+ check_packages(manifest_packages(manifest_path), resolved_versions, manifest_path)
115
122
  }
116
123
  @warnings
117
124
  end
118
125
 
119
126
  private
120
127
 
128
+ def manifest_packages(manifest_path)
129
+ ManifestParser.get_packages(manifest_path) { |skip| record_parse_warning(skip, manifest_path) }
130
+ end
131
+
121
132
  def normalize_ignore_repos
122
133
  @ignore_repos = Array(@ignore_repos).map { |repo| GitOperations.trim_repo_url(repo) }
123
134
  end
@@ -127,7 +138,7 @@ class SpmChecker
127
138
  @allow_hosts = raw_allow_hosts.filter_map { |host| AllowHostNormalizer.normalize(host) }
128
139
  return unless invalid_allow_hosts_configuration?(raw_allow_hosts)
129
140
 
130
- raise(ArgumentError, "allow-hosts was configured, but no entries could be parsed as hostnames")
141
+ raise(SpmVersionUpdates::ConfigurationError, "allow-hosts was configured, but no entries could be parsed as hostnames")
131
142
  end
132
143
 
133
144
  def configured_allow_hosts
@@ -141,6 +152,13 @@ class SpmChecker
141
152
  def clear_warnings
142
153
  @warnings.clear
143
154
  @warning_details.clear
155
+ @parse_warnings.clear
156
+ end
157
+
158
+ def record_parse_warning(skip, manifest_path)
159
+ record = ParseWarning.record(**skip, source: manifest_path)
160
+ @parse_warnings << record
161
+ puts("WARNING: #{record['message']}")
144
162
  end
145
163
 
146
164
  def warn_for_empty_xcode_project(remote_packages, resolved_versions, xcodeproj_path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpmVersionUpdates
4
- VERSION = "1.1.1"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "git_operations"
4
4
 
5
5
  # Fetches git tag versions concurrently for cache-key/repository URL lookup pairs.
6
+ # @api private
6
7
  class VersionTagFetcher
7
8
  # Thread-safe result/error accumulator shared by fetcher workers.
8
9
  FetchState = Struct.new(:mutex, :results, :errors, keyword_init: true) {
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "errors"
3
4
  require_relative "git_operations"
4
5
  require_relative "package_resolved"
5
6
  require_relative "xcode_project_package_reader"
@@ -62,10 +63,16 @@ module XcodeParser
62
63
  private_class_method :find_packages_resolved_file
63
64
 
64
65
  # Raised when Xcode project mode is invoked without a project path.
65
- class XcodeprojPathMustBeSet < StandardError
66
+ class XcodeprojPathMustBeSet < SpmVersionUpdates::ConfigurationError
67
+ def initialize(message = "Invalid Xcode project path")
68
+ super
69
+ end
66
70
  end
67
71
 
68
72
  # Raised when an Xcode project does not have a Package.resolved file.
69
- class CouldNotFindResolvedFile < StandardError
73
+ class CouldNotFindResolvedFile < SpmVersionUpdates::FileNotFoundError
74
+ def initialize(message = "Could not find a Package.resolved file for the Xcode project")
75
+ super
76
+ end
70
77
  end
71
78
  end
@@ -2,11 +2,13 @@
2
2
 
3
3
  require_relative "spm_version_updates/allow_host_normalizer"
4
4
  require_relative "spm_version_updates/credential_redactor"
5
+ require_relative "spm_version_updates/errors"
5
6
  require_relative "spm_version_updates/fail_on_threshold"
6
7
  require_relative "spm_version_updates/git_host_normalizer"
7
8
  require_relative "spm_version_updates/git_operations"
8
9
  require_relative "spm_version_updates/manifest_parser"
9
10
  require_relative "spm_version_updates/package_resolved"
11
+ require_relative "spm_version_updates/parse_warning"
10
12
  require_relative "spm_version_updates/repository_link"
11
13
  require_relative "spm_version_updates/repository_update_rules"
12
14
  require_relative "spm_version_updates/semver"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spm_version_updates
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harold Martin
@@ -36,11 +36,13 @@ files:
36
36
  - lib/spm_version_updates.rb
37
37
  - lib/spm_version_updates/allow_host_normalizer.rb
38
38
  - lib/spm_version_updates/credential_redactor.rb
39
+ - lib/spm_version_updates/errors.rb
39
40
  - lib/spm_version_updates/fail_on_threshold.rb
40
41
  - lib/spm_version_updates/git_host_normalizer.rb
41
42
  - lib/spm_version_updates/git_operations.rb
42
43
  - lib/spm_version_updates/manifest_parser.rb
43
44
  - lib/spm_version_updates/package_resolved.rb
45
+ - lib/spm_version_updates/parse_warning.rb
44
46
  - lib/spm_version_updates/repository_link.rb
45
47
  - lib/spm_version_updates/repository_update_rules.rb
46
48
  - lib/spm_version_updates/semver.rb