spm_version_updates 1.1.2 → 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 +4 -4
- data/README.md +86 -0
- data/lib/spm_version_updates/errors.rb +28 -0
- data/lib/spm_version_updates/fail_on_threshold.rb +2 -1
- data/lib/spm_version_updates/git_operations.rb +2 -1
- data/lib/spm_version_updates/manifest_parser.rb +33 -8
- data/lib/spm_version_updates/package_resolved.rb +2 -1
- data/lib/spm_version_updates/parse_warning.rb +81 -0
- data/lib/spm_version_updates/repository_update_rules.rb +14 -13
- data/lib/spm_version_updates/spm_checker.rb +22 -4
- data/lib/spm_version_updates/version.rb +1 -1
- data/lib/spm_version_updates/xcode_parser.rb +9 -2
- data/lib/spm_version_updates.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53afb3742bddcef276c343767b4570a4d428cefe7560ba2460d461dfcf66c3c1
|
|
4
|
+
data.tar.gz: bde5dd5228e9c7ae5188efa42ef0343bc1def8b45bb22f16a9ce343fb9f94e4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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).
|
|
@@ -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(
|
|
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)
|
|
@@ -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 <
|
|
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
|
-
|
|
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
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
103
|
-
raise(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,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 <
|
|
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
|
-
|
|
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(
|
|
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,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 <
|
|
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 <
|
|
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
|
data/lib/spm_version_updates.rb
CHANGED
|
@@ -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.
|
|
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
|