dependabot-apm 0.400.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 +7 -0
- data/lib/dependabot/apm/file_fetcher.rb +95 -0
- data/lib/dependabot/apm/file_parser.rb +313 -0
- data/lib/dependabot/apm/file_updater.rb +174 -0
- data/lib/dependabot/apm/git_commit_checker.rb +91 -0
- data/lib/dependabot/apm/metadata_finder.rb +91 -0
- data/lib/dependabot/apm/package_manager.rb +75 -0
- data/lib/dependabot/apm/package_specifier.rb +347 -0
- data/lib/dependabot/apm/requirement.rb +68 -0
- data/lib/dependabot/apm/update_checker.rb +303 -0
- data/lib/dependabot/apm/version.rb +228 -0
- data/lib/dependabot/apm.rb +28 -0
- metadata +278 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/git_commit_checker"
|
|
7
|
+
require "dependabot/git_metadata_fetcher"
|
|
8
|
+
require "dependabot/git_tag_details"
|
|
9
|
+
require "dependabot/update_checkers"
|
|
10
|
+
require "dependabot/update_checkers/base"
|
|
11
|
+
require "dependabot/apm/git_commit_checker"
|
|
12
|
+
require "dependabot/apm/requirement"
|
|
13
|
+
require "dependabot/apm/version"
|
|
14
|
+
|
|
15
|
+
module Dependabot
|
|
16
|
+
module Apm
|
|
17
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
21
|
+
def latest_version
|
|
22
|
+
@latest_version ||= T.let(
|
|
23
|
+
fetch_latest_version,
|
|
24
|
+
T.nilable(T.any(String, Gem::Version))
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
29
|
+
def latest_resolvable_version
|
|
30
|
+
# APM dependencies have no resolution graph: each manifest ref is
|
|
31
|
+
# independent, so the latest version is always resolvable.
|
|
32
|
+
latest_version
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
36
|
+
def latest_resolvable_version_with_no_unlock
|
|
37
|
+
# Updating an APM dependency always rewrites its manifest requirement,
|
|
38
|
+
# so there is no newer version reachable "without unlocking".
|
|
39
|
+
dependency.version
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# APM packages have no GitHub Advisory Database coverage, so Dependabot
|
|
43
|
+
# never supplies security advisories for them and the security-update flow
|
|
44
|
+
# (gated on `vulnerable?`, which is false without advisories) never runs.
|
|
45
|
+
# We have no visibility into whether APM security updates will be offered
|
|
46
|
+
# in the future; if they are, this needs a real implementation. The base
|
|
47
|
+
# declares it abstract, so it is stubbed to nil to satisfy the contract
|
|
48
|
+
# rather than carrying unreachable resolution logic.
|
|
49
|
+
sig { override.returns(T.nilable(Gem::Version)) }
|
|
50
|
+
def lowest_security_fix_version
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
sig { override.returns(T.nilable(Gem::Version)) }
|
|
55
|
+
def lowest_resolvable_security_fix_version
|
|
56
|
+
lowest_security_fix_version
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
60
|
+
def updated_requirements
|
|
61
|
+
return dependency.requirements unless git_commit_checker.git_dependency?
|
|
62
|
+
|
|
63
|
+
dependency.requirements.map do |req|
|
|
64
|
+
source = req.source_hash
|
|
65
|
+
current_ref = req.source_string("ref")
|
|
66
|
+
# Only rewrite requirements pinned to a semver tag (plain or
|
|
67
|
+
# package-scoped, e.g. `review--v1.0.0`); branch- and SHA-pinned
|
|
68
|
+
# entries are left as-is. Normalise the ref to its SemVer core through
|
|
69
|
+
# the shared APM extractor so scoped tags are recognised and compared.
|
|
70
|
+
ref_version = current_ref && Version.semver_from_ref(current_ref, dependency_name: dependency.name)
|
|
71
|
+
next req unless source && ref_version
|
|
72
|
+
|
|
73
|
+
# DependencySet merges repeated declarations of the same package into a
|
|
74
|
+
# single dependency with several requirements whose refs need not share
|
|
75
|
+
# a tag family (e.g. `review--v1.0.0` and `review-v1.0.0`). Resolve each
|
|
76
|
+
# requirement independently -- within its own family and above its own
|
|
77
|
+
# pinned version -- so a declaration is only ever bumped within its own
|
|
78
|
+
# family, and a higher declaration is never rewritten down to a lower
|
|
79
|
+
# family's tag.
|
|
80
|
+
new_tag = resolved_tag_for_requirement(source, ref_version)&.tag
|
|
81
|
+
next req unless new_tag
|
|
82
|
+
|
|
83
|
+
new_source = source.merge(ref: new_tag)
|
|
84
|
+
Dependabot::DependencyRequirement.create(req.merge(source: new_source))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
sig { override.returns(T::Boolean) }
|
|
91
|
+
def latest_version_resolvable_with_full_unlock?
|
|
92
|
+
# APM has no concept of a full unlock (no transitive resolution).
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { override.returns(T::Array[Dependabot::Dependency]) }
|
|
97
|
+
def updated_dependencies_after_full_unlock
|
|
98
|
+
raise NotImplementedError
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# `latest_version` deliberately reports the MAX tag reachable by ANY family
|
|
102
|
+
# so the base `can_update?` gate still fires when only a higher family can
|
|
103
|
+
# move. But `DependencySet` defines a merged dependency's version as its
|
|
104
|
+
# LOWEST pin, and `updated_requirements` can leave families on different
|
|
105
|
+
# tags (e.g. `review-v1.4.0` alongside `review--v1.5.0`). Deriving the
|
|
106
|
+
# resulting version from `preferred_resolvable_version` (the max) would
|
|
107
|
+
# report a `1.5.0` update while the merged dependency is really `1.4.0`,
|
|
108
|
+
# desyncing the reported version (and its update type / PR metadata) from
|
|
109
|
+
# the rewritten requirements. Derive it from the post-update refs with the
|
|
110
|
+
# same lowest-pin rule instead, keeping the update gate and the reported
|
|
111
|
+
# version separate.
|
|
112
|
+
sig { returns(Dependabot::Dependency) }
|
|
113
|
+
def updated_dependency_with_own_req_unlock
|
|
114
|
+
new_requirements = updated_requirements
|
|
115
|
+
new_version = pinned_versions(new_requirements).min
|
|
116
|
+
|
|
117
|
+
Dependabot::Dependency.new(
|
|
118
|
+
name: dependency.name,
|
|
119
|
+
version: new_version || dependency.version,
|
|
120
|
+
requirements: new_requirements,
|
|
121
|
+
previous_version: dependency.version,
|
|
122
|
+
previous_requirements: dependency.requirements,
|
|
123
|
+
package_manager: dependency.package_manager,
|
|
124
|
+
metadata: dependency.metadata,
|
|
125
|
+
subdependency_metadata: dependency.subdependency_metadata
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
sig { returns(T.nilable(T.any(String, Gem::Version))) }
|
|
130
|
+
def fetch_latest_version
|
|
131
|
+
return dependency.version unless git_commit_checker.git_dependency?
|
|
132
|
+
|
|
133
|
+
# Only entries pinned to a semver tag are bumped in the manifest.
|
|
134
|
+
# Branch- and SHA-pinned entries are resolved via the lockfile, which
|
|
135
|
+
# APM regenerates itself, so Dependabot leaves them untouched.
|
|
136
|
+
return dependency.version unless git_commit_checker.pinned_ref_looks_like_version?
|
|
137
|
+
|
|
138
|
+
# A merged dependency can hold several ref families (e.g. `review--v*`
|
|
139
|
+
# and `review-v*`). Report the highest tag reachable by ANY requirement,
|
|
140
|
+
# each resolved within its own family and above its own pinned version,
|
|
141
|
+
# so the base `can_update?` isn't short-circuited when only a non-first
|
|
142
|
+
# family has a newer tag.
|
|
143
|
+
semver_requirement_checkers.filter_map do |ref_version, checker|
|
|
144
|
+
tag_version = latest_version_tag(checker)&.version
|
|
145
|
+
tag_version if tag_version && Version.new(ref_version) < tag_version
|
|
146
|
+
end.max || dependency.version
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The tag a single requirement should move to: the latest tag resolved
|
|
150
|
+
# within that requirement's own ref family (via a checker scoped to its
|
|
151
|
+
# ref). Returns nil when there is no strictly-higher tag, so a requirement
|
|
152
|
+
# is never downgraded -- a latest capped by an ignore rule must leave an
|
|
153
|
+
# already-higher declaration untouched.
|
|
154
|
+
sig do
|
|
155
|
+
params(source: Dependabot::DependencyRequirement::ObjectHash, ref_version: String)
|
|
156
|
+
.returns(T.nilable(Dependabot::GitTagDetails))
|
|
157
|
+
end
|
|
158
|
+
def resolved_tag_for_requirement(source, ref_version)
|
|
159
|
+
checker = git_commit_checker_for(source, ref_version)
|
|
160
|
+
return unless checker.pinned_ref_looks_like_version?
|
|
161
|
+
|
|
162
|
+
parsed = Version.new(ref_version)
|
|
163
|
+
tag = latest_version_tag(checker)
|
|
164
|
+
tag_version = tag&.version
|
|
165
|
+
return unless tag_version
|
|
166
|
+
return if parsed >= tag_version
|
|
167
|
+
|
|
168
|
+
tag
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Each git-semver-pinned requirement paired with a checker scoped to its
|
|
172
|
+
# own ref (all sharing one remote fetch via shared_git_metadata_fetcher).
|
|
173
|
+
# Drives the cross-family latest-version reporting that gates can_update?,
|
|
174
|
+
# so the gate still fires when only a non-first family has a newer tag.
|
|
175
|
+
sig { returns(T::Array[[String, Dependabot::GitCommitChecker]]) }
|
|
176
|
+
def semver_requirement_checkers
|
|
177
|
+
dependency.requirements.filter_map do |req|
|
|
178
|
+
source = req.source_hash
|
|
179
|
+
current_ref = req.source_string("ref")
|
|
180
|
+
ref_version = current_ref && Version.semver_from_ref(current_ref, dependency_name: dependency.name)
|
|
181
|
+
next unless source && ref_version
|
|
182
|
+
|
|
183
|
+
[ref_version, git_commit_checker_for(source, ref_version)]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The parsed SemVer core of every requirement pinned to a version tag
|
|
188
|
+
# (plain or package-scoped, e.g. `review--v1.0.0`). Branch- and SHA-pinned
|
|
189
|
+
# requirements carry no comparable version and are skipped. Used to derive
|
|
190
|
+
# the merged post-update version (its lowest pin).
|
|
191
|
+
sig { params(requirements: T::Array[Dependabot::DependencyRequirement]).returns(T::Array[Dependabot::Version]) }
|
|
192
|
+
def pinned_versions(requirements)
|
|
193
|
+
requirements.filter_map do |req|
|
|
194
|
+
current_ref = req.source_string("ref")
|
|
195
|
+
ref_version = current_ref && Version.semver_from_ref(current_ref, dependency_name: dependency.name)
|
|
196
|
+
ref_version && Version.new(ref_version)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
sig { params(checker: Dependabot::GitCommitChecker).returns(T.nilable(Dependabot::GitTagDetails)) }
|
|
201
|
+
def latest_version_tag(checker = git_commit_checker)
|
|
202
|
+
return unless checker.git_dependency?
|
|
203
|
+
return unless checker.pinned_ref_looks_like_version?
|
|
204
|
+
|
|
205
|
+
checker.local_tag_for_latest_version(update_cooldown)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
sig { returns(Dependabot::GitCommitChecker) }
|
|
209
|
+
def git_commit_checker
|
|
210
|
+
@git_commit_checker ||= T.let(
|
|
211
|
+
Dependabot::Apm::GitCommitChecker.new(
|
|
212
|
+
dependency: dependency,
|
|
213
|
+
credentials: credentials,
|
|
214
|
+
ignored_versions: ignored_versions,
|
|
215
|
+
raise_on_ignored: raise_on_ignored,
|
|
216
|
+
consider_version_branches_pinned: false,
|
|
217
|
+
git_metadata_fetcher: shared_git_metadata_fetcher
|
|
218
|
+
),
|
|
219
|
+
T.nilable(Dependabot::GitCommitChecker)
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# A checker scoped to a single requirement's source (its own ref), sharing
|
|
224
|
+
# one remote metadata fetch across every requirement so per-requirement
|
|
225
|
+
# resolution does not re-fetch the upload pack for each declaration. The
|
|
226
|
+
# scoped ref makes the parent's family filtering (`same_prefix?`) select
|
|
227
|
+
# tags in that declaration's own family. When `ref_version` is given the
|
|
228
|
+
# checker's dependency version is scoped to this requirement's own pinned
|
|
229
|
+
# ref, so GitCommitChecker#current_version (and therefore cooldown's
|
|
230
|
+
# SemVer-distance classification) reflects THIS declaration rather than the
|
|
231
|
+
# merged dependency's lowest pin -- e.g. a `v2.1.0` candidate for a `v2.0.0`
|
|
232
|
+
# pin stays a minor bump even when a sibling `v1.4.0` pin lowers the merged
|
|
233
|
+
# version to 1.4.0.
|
|
234
|
+
sig do
|
|
235
|
+
params(
|
|
236
|
+
source: Dependabot::DependencyRequirement::ObjectHash,
|
|
237
|
+
ref_version: T.nilable(String)
|
|
238
|
+
).returns(Dependabot::GitCommitChecker)
|
|
239
|
+
end
|
|
240
|
+
def git_commit_checker_for(source, ref_version = nil)
|
|
241
|
+
scoped_dependency =
|
|
242
|
+
if ref_version
|
|
243
|
+
Dependabot::Dependency.new(
|
|
244
|
+
name: dependency.name,
|
|
245
|
+
version: ref_version,
|
|
246
|
+
requirements: dependency.requirements,
|
|
247
|
+
package_manager: dependency.package_manager,
|
|
248
|
+
metadata: dependency.metadata,
|
|
249
|
+
subdependency_metadata: dependency.subdependency_metadata
|
|
250
|
+
)
|
|
251
|
+
else
|
|
252
|
+
dependency
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
Dependabot::Apm::GitCommitChecker.new(
|
|
256
|
+
dependency: scoped_dependency,
|
|
257
|
+
credentials: credentials,
|
|
258
|
+
ignored_versions: ignored_versions,
|
|
259
|
+
raise_on_ignored: raise_on_ignored,
|
|
260
|
+
consider_version_branches_pinned: false,
|
|
261
|
+
dependency_source_details: symbolized_source_details(source),
|
|
262
|
+
git_metadata_fetcher: shared_git_metadata_fetcher
|
|
263
|
+
)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# One metadata fetcher for the dependency's git remote, shared by the main
|
|
267
|
+
# and per-requirement checkers so the upload pack is fetched once. All
|
|
268
|
+
# requirements of a merged dependency share the same clone URL (differing
|
|
269
|
+
# ports/paths keep them as separate dependencies), so a single fetcher is
|
|
270
|
+
# correct. Nil for non-git dependencies, where no fetch is needed.
|
|
271
|
+
sig { returns(T.nilable(Dependabot::GitMetadataFetcher)) }
|
|
272
|
+
def shared_git_metadata_fetcher
|
|
273
|
+
return @shared_git_metadata_fetcher if defined?(@shared_git_metadata_fetcher)
|
|
274
|
+
|
|
275
|
+
url = dependency.source_string("url", allowed_types: ["git"])
|
|
276
|
+
@shared_git_metadata_fetcher = T.let(
|
|
277
|
+
url && Dependabot::GitMetadataFetcher.new(url: url, credentials: credentials),
|
|
278
|
+
T.nilable(Dependabot::GitMetadataFetcher)
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# A symbol-keyed source-details hash for GitCommitChecker, holding the git
|
|
283
|
+
# coordinate fields it reads (type/url/ref/branch). Rebuilt explicitly so a
|
|
284
|
+
# requirement's mixed-key source hash conforms to the checker's expected
|
|
285
|
+
# `{Symbol => Object}` shape.
|
|
286
|
+
sig do
|
|
287
|
+
params(source: Dependabot::DependencyRequirement::ObjectHash)
|
|
288
|
+
.returns(T::Hash[Symbol, Object])
|
|
289
|
+
end
|
|
290
|
+
def symbolized_source_details(source)
|
|
291
|
+
details = T.let({}, T::Hash[Symbol, Object])
|
|
292
|
+
%i(type url ref branch).each do |key|
|
|
293
|
+
value = source[key] || source[key.to_s]
|
|
294
|
+
details[key] = value if value.is_a?(String)
|
|
295
|
+
end
|
|
296
|
+
details
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
Dependabot::UpdateCheckers
|
|
303
|
+
.register("apm", Dependabot::Apm::UpdateChecker)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/utils"
|
|
7
|
+
require "dependabot/version"
|
|
8
|
+
|
|
9
|
+
module Dependabot
|
|
10
|
+
module Apm
|
|
11
|
+
# APM pins dependencies to git refs. When that ref is a SemVer 2.0.0 tag
|
|
12
|
+
# (optionally `v`-prefixed, e.g. `v1.2.0`, `1.2.0-alpha.1` or `1.2.0+build.5`)
|
|
13
|
+
# we treat it as the dependency version.
|
|
14
|
+
#
|
|
15
|
+
# The parent class uses RubyGems semantics, which differ from SemVer in ways
|
|
16
|
+
# that matter for git tags, so this subclass enforces SemVer 2.0.0 instead:
|
|
17
|
+
# * validation requires MAJOR.MINOR.PATCH, so partial refs (`v1`, `1.2`) or
|
|
18
|
+
# branch names are never mistaken for versions;
|
|
19
|
+
# * `+build` metadata is accepted and ignored for precedence;
|
|
20
|
+
# * prerelease identifiers follow SemVer precedence (numeric identifiers
|
|
21
|
+
# rank below alphanumeric ones, so `1.0.0-alpha.1 < 1.0.0-alpha.beta` —
|
|
22
|
+
# the opposite of RubyGems, which compares them the other way around).
|
|
23
|
+
class Version < Dependabot::Version
|
|
24
|
+
extend T::Sig
|
|
25
|
+
|
|
26
|
+
# Official SemVer 2.0.0 grammar (https://semver.org), anchored end to end so
|
|
27
|
+
# partial or branch-like refs are rejected.
|
|
28
|
+
SEMVER_REGEX = /
|
|
29
|
+
\A
|
|
30
|
+
(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)
|
|
31
|
+
(?:-(?<prerelease>
|
|
32
|
+
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
|
|
33
|
+
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
|
|
34
|
+
))?
|
|
35
|
+
(?:\+(?<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
|
|
36
|
+
\z
|
|
37
|
+
/x
|
|
38
|
+
|
|
39
|
+
# The separators APM places between a package name and the SemVer core in
|
|
40
|
+
# package-scoped tags (`{name}_v1.2.3`, `{name}--v1.2.3`, `{name}-v1.2.3`).
|
|
41
|
+
# Each already ends in the `v` that precedes the version, so the remaining
|
|
42
|
+
# suffix is a bare SemVer string. None is a prefix of another for a given
|
|
43
|
+
# name (`foo--v1.2.3` does not start with `foo-v`), so match order is
|
|
44
|
+
# irrelevant.
|
|
45
|
+
SCOPED_TAG_SEPARATORS = %w(_v --v -v).freeze
|
|
46
|
+
|
|
47
|
+
sig { returns(Integer) }
|
|
48
|
+
attr_reader :major
|
|
49
|
+
|
|
50
|
+
sig { returns(Integer) }
|
|
51
|
+
attr_reader :minor
|
|
52
|
+
|
|
53
|
+
sig { returns(Integer) }
|
|
54
|
+
attr_reader :patch
|
|
55
|
+
|
|
56
|
+
sig { returns(T.nilable(String)) }
|
|
57
|
+
attr_reader :prerelease_info
|
|
58
|
+
|
|
59
|
+
sig { returns(T.nilable(String)) }
|
|
60
|
+
attr_reader :build_info
|
|
61
|
+
|
|
62
|
+
sig { override.params(version: VersionParameter).void }
|
|
63
|
+
def initialize(version)
|
|
64
|
+
normalized = Version.remove_leading_v(version).to_s
|
|
65
|
+
match = normalized.match(SEMVER_REGEX)
|
|
66
|
+
raise ArgumentError, "Malformed version string - #{version}" unless match
|
|
67
|
+
|
|
68
|
+
@version_string = T.let(normalized, String)
|
|
69
|
+
@major = T.let(T.must(match[:major]).to_i, Integer)
|
|
70
|
+
@minor = T.let(T.must(match[:minor]).to_i, Integer)
|
|
71
|
+
@patch = T.let(T.must(match[:patch]).to_i, Integer)
|
|
72
|
+
@prerelease_info = T.let(match[:prerelease], T.nilable(String))
|
|
73
|
+
@build_info = T.let(match[:build], T.nilable(String))
|
|
74
|
+
|
|
75
|
+
# Hand RubyGems the release (plus any prerelease) without the build
|
|
76
|
+
# metadata, which is not part of precedence, so requirement matching and
|
|
77
|
+
# the inherited helpers keep working.
|
|
78
|
+
super(T.must(normalized.split("+").first))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { override.params(version: VersionParameter).returns(Dependabot::Apm::Version) }
|
|
82
|
+
def self.new(version)
|
|
83
|
+
T.cast(super, Dependabot::Apm::Version)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { params(version: VersionParameter).returns(VersionParameter) }
|
|
87
|
+
def self.remove_leading_v(version)
|
|
88
|
+
return version unless version.to_s.match?(/\Av\d/)
|
|
89
|
+
|
|
90
|
+
version.to_s.delete_prefix("v")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
sig { override.params(version: VersionParameter).returns(T::Boolean) }
|
|
94
|
+
def self.correct?(version)
|
|
95
|
+
return false if version.to_s.strip.empty?
|
|
96
|
+
|
|
97
|
+
Version.remove_leading_v(version).to_s.match?(SEMVER_REGEX)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Extracts the SemVer core from a git ref for the package `dependency_name`,
|
|
101
|
+
# or nil when the ref is not a version tag for that package. A plain
|
|
102
|
+
# `v?<semver>` ref keeps any build metadata; a package-scoped ref
|
|
103
|
+
# (`{name}_v…`, `{name}--v…`, `{name}-v…`) is accepted only when `{name}`
|
|
104
|
+
# matches the package's own name -- the final `/`-separated segment of
|
|
105
|
+
# `dependency_name`, since a virtual package `org/mono/skills/review` is
|
|
106
|
+
# released as `review--v1.2.3`. Scoping to that name keeps a monorepo's
|
|
107
|
+
# `security--v2.0.0` from resolving against a `review` dependency. This is
|
|
108
|
+
# the single reader of APM's tag grammar, shared by the git commit checker,
|
|
109
|
+
# the file parser and the update checker so ref handling never diverges.
|
|
110
|
+
sig { params(ref: String, dependency_name: String).returns(T.nilable(String)) }
|
|
111
|
+
def self.semver_from_ref(ref, dependency_name:)
|
|
112
|
+
return remove_leading_v(ref).to_s if correct?(ref)
|
|
113
|
+
|
|
114
|
+
name = dependency_name.split("/").last.to_s
|
|
115
|
+
return nil if name.empty?
|
|
116
|
+
|
|
117
|
+
SCOPED_TAG_SEPARATORS.each do |separator|
|
|
118
|
+
prefix = "#{name}#{separator}"
|
|
119
|
+
next unless ref.start_with?(prefix)
|
|
120
|
+
|
|
121
|
+
candidate = ref[prefix.length..].to_s
|
|
122
|
+
return candidate if candidate.match?(SEMVER_REGEX)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { override.returns(String) }
|
|
129
|
+
def to_s
|
|
130
|
+
@version_string
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
sig { override.returns(String) }
|
|
134
|
+
def inspect
|
|
135
|
+
"#<#{self.class} #{@version_string}>"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# `Gem::Requirement` evaluates a `~> x.y.z` bound by comparing candidates
|
|
139
|
+
# against this operand's `bump`. The inherited `Gem::Version#bump` drops
|
|
140
|
+
# the last segment (`1.2.3` -> `1.3`) and rebuilds `self.class` from it,
|
|
141
|
+
# but `Apm::Version` rejects that partial value and raises while filtering
|
|
142
|
+
# tags. Return the SemVer upper bound (`x.(y+1).0`) instead, so a
|
|
143
|
+
# pessimistic ignore rule such as `~> 1.2.3` evaluates as
|
|
144
|
+
# `>= 1.2.3, < 1.3.0` rather than crashing.
|
|
145
|
+
sig { returns(Dependabot::Apm::Version) }
|
|
146
|
+
def bump
|
|
147
|
+
Version.new("#{major}.#{minor + 1}.0")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
sig { params(other: Object).returns(T.nilable(Integer)) }
|
|
151
|
+
def <=>(other)
|
|
152
|
+
other_version = Version.coerce(other)
|
|
153
|
+
return super unless other_version
|
|
154
|
+
|
|
155
|
+
release = compare_release(other_version)
|
|
156
|
+
return release unless release.zero?
|
|
157
|
+
|
|
158
|
+
Version.compare_prerelease(prerelease_info, other_version.prerelease_info)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Compares the MAJOR.MINOR.PATCH triples numerically.
|
|
162
|
+
sig { params(other: Dependabot::Apm::Version).returns(Integer) }
|
|
163
|
+
def compare_release(other)
|
|
164
|
+
[[major, other.major], [minor, other.minor], [patch, other.patch]].each do |mine, theirs|
|
|
165
|
+
comparison = mine <=> theirs
|
|
166
|
+
return comparison unless comparison.zero?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
0
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Coerces a comparison operand into an Apm::Version when it is a strict
|
|
173
|
+
# SemVer value, or nil when it is not (so `<=>` can fall back to RubyGems
|
|
174
|
+
# semantics for e.g. a bare `Gem::Version` requirement bound).
|
|
175
|
+
sig { params(other: Object).returns(T.nilable(Dependabot::Apm::Version)) }
|
|
176
|
+
def self.coerce(other)
|
|
177
|
+
return other if other.is_a?(Dependabot::Apm::Version)
|
|
178
|
+
return unless other.is_a?(String) || other.is_a?(Gem::Version)
|
|
179
|
+
return unless correct?(other.to_s)
|
|
180
|
+
|
|
181
|
+
new(other.to_s)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# SemVer prerelease precedence (spec rule 11): a version WITH a prerelease
|
|
185
|
+
# ranks below the same version without one, and equal releases are ordered
|
|
186
|
+
# by their prerelease identifiers.
|
|
187
|
+
sig { params(left: T.nilable(String), right: T.nilable(String)).returns(Integer) }
|
|
188
|
+
def self.compare_prerelease(left, right)
|
|
189
|
+
return 0 if left == right
|
|
190
|
+
return 1 if left.nil? # a normal version outranks any prerelease
|
|
191
|
+
return -1 if right.nil?
|
|
192
|
+
|
|
193
|
+
compare_prerelease_identifiers(left.split("."), right.split("."))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Compares dot-separated prerelease identifiers left to right; a longer set
|
|
197
|
+
# of identifiers wins when the shorter one is a prefix of it.
|
|
198
|
+
sig { params(left: T::Array[String], right: T::Array[String]).returns(Integer) }
|
|
199
|
+
def self.compare_prerelease_identifiers(left, right)
|
|
200
|
+
left.zip(right).each do |l, r|
|
|
201
|
+
return 1 if r.nil? # left carries an extra identifier
|
|
202
|
+
|
|
203
|
+
comparison = compare_identifier(l, r)
|
|
204
|
+
return comparison unless comparison.zero?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
left.length <=> right.length
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# A single prerelease identifier: numeric identifiers compare numerically
|
|
211
|
+
# and always rank below alphanumeric ones, which compare lexically (ASCII).
|
|
212
|
+
sig { params(left: String, right: String).returns(Integer) }
|
|
213
|
+
def self.compare_identifier(left, right)
|
|
214
|
+
left_numeric = left.match?(/\A\d+\z/)
|
|
215
|
+
right_numeric = right.match?(/\A\d+\z/)
|
|
216
|
+
|
|
217
|
+
return left.to_i <=> right.to_i if left_numeric && right_numeric
|
|
218
|
+
return -1 if left_numeric
|
|
219
|
+
return 1 if right_numeric
|
|
220
|
+
|
|
221
|
+
T.must(left <=> right)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
Dependabot::Utils
|
|
228
|
+
.register_version_class("apm", Dependabot::Apm::Version)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# These all need to be required so the various classes can be registered in a
|
|
5
|
+
# lookup table of package manager names to concrete classes.
|
|
6
|
+
require "dependabot/apm/file_fetcher"
|
|
7
|
+
require "dependabot/apm/file_parser"
|
|
8
|
+
require "dependabot/apm/update_checker"
|
|
9
|
+
require "dependabot/apm/file_updater"
|
|
10
|
+
require "dependabot/apm/metadata_finder"
|
|
11
|
+
require "dependabot/apm/requirement"
|
|
12
|
+
require "dependabot/apm/version"
|
|
13
|
+
|
|
14
|
+
require "dependabot/pull_request_creator/labeler"
|
|
15
|
+
Dependabot::PullRequestCreator::Labeler
|
|
16
|
+
.register_label_details("apm", name: "apm", colour: "0e8a16")
|
|
17
|
+
|
|
18
|
+
require "dependabot/dependency"
|
|
19
|
+
Dependabot::Dependency
|
|
20
|
+
.register_production_check(
|
|
21
|
+
"apm",
|
|
22
|
+
# Production when the explicit "dependencies" marker is present (or when a
|
|
23
|
+
# dependency carries no group information at all). Presence-based rather than
|
|
24
|
+
# "not development", so a package merged from both `dependencies.apm` and
|
|
25
|
+
# `devDependencies.apm` -- whose flattened groups include both markers --
|
|
26
|
+
# stays production, while a `devDependencies`-only entry does not.
|
|
27
|
+
->(groups) { groups.empty? || groups.include?("dependencies") }
|
|
28
|
+
)
|