dependabot-apm 0.400.0 → 0.402.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/lib/dependabot/apm/file_fetcher.rb +2 -2
- data/lib/dependabot/apm/file_parser.rb +127 -38
- data/lib/dependabot/apm/file_updater/lockfile_updater.rb +175 -0
- data/lib/dependabot/apm/file_updater.rb +58 -2
- data/lib/dependabot/apm/git_commit_checker.rb +55 -0
- data/lib/dependabot/apm/lockfile.rb +93 -0
- data/lib/dependabot/apm/native_helpers.rb +54 -0
- data/lib/dependabot/apm/package_specifier.rb +57 -1
- data/lib/dependabot/apm/update_checker.rb +112 -7
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ba8a0fe03d88d3c9a98aba6d2f8fd7c4d235bded82fd3e158a4dac7936def80
|
|
4
|
+
data.tar.gz: 19c19651c79805c932dc12b4e84b9b579bf8db15f6c131a0e0cd82947cefc1b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71dc26756e87e1412601d85d0ae7f72f8849289194abc3ce0e29211000ccd991439ffe846ac60d8a4d1432343ca6e8072732ed1ea919bd83fc60c81e67f6edd9
|
|
7
|
+
data.tar.gz: d15140aaef4a1e2e74eb510d8ab4f25774d36abaf8fffa428af782a46edf782784c1c3742771c0f648d15159d98b5b519c458f6fb4ed54c5ecdc46c64f35f7d7
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
@@ -78,7 +78,7 @@ module Dependabot
|
|
|
78
78
|
return @lockfile if defined?(@lockfile)
|
|
79
79
|
|
|
80
80
|
@lockfile = T.let(
|
|
81
|
-
fetch_file_if_present(LOCKFILE_FILENAME)
|
|
81
|
+
fetch_file_if_present(LOCKFILE_FILENAME),
|
|
82
82
|
T.nilable(Dependabot::DependencyFile)
|
|
83
83
|
)
|
|
84
84
|
end
|
|
@@ -8,6 +8,7 @@ require "dependabot/dependency"
|
|
|
8
8
|
require "dependabot/errors"
|
|
9
9
|
require "dependabot/file_parsers"
|
|
10
10
|
require "dependabot/file_parsers/base"
|
|
11
|
+
require "dependabot/apm/lockfile"
|
|
11
12
|
require "dependabot/apm/package_manager"
|
|
12
13
|
require "dependabot/apm/package_specifier"
|
|
13
14
|
require "dependabot/apm/version"
|
|
@@ -50,6 +51,23 @@ module Dependabot
|
|
|
50
51
|
T::Array[Integer]
|
|
51
52
|
)
|
|
52
53
|
|
|
54
|
+
PIN_TAG = "tag"
|
|
55
|
+
PIN_SHA = "sha"
|
|
56
|
+
PIN_BRANCH = "branch"
|
|
57
|
+
PIN_PRECEDENCE = T.let([PIN_TAG, PIN_SHA, PIN_BRANCH].freeze, T::Array[String])
|
|
58
|
+
|
|
59
|
+
# A manifest scalar entry and the dependency groups its block confers.
|
|
60
|
+
class Declaration < T::Struct
|
|
61
|
+
const :raw_entry, String
|
|
62
|
+
const :span, String
|
|
63
|
+
const :groups, T::Array[String]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class PinnedEntry < T::Struct
|
|
67
|
+
const :pin_type, String
|
|
68
|
+
const :dependency, Dependabot::Dependency
|
|
69
|
+
end
|
|
70
|
+
|
|
53
71
|
sig { override.returns(T::Array[Dependabot::Dependency]) }
|
|
54
72
|
def parse
|
|
55
73
|
# Non-GitHub git hosts (e.g. GitLab) are case-sensitive, so use a
|
|
@@ -57,9 +75,9 @@ module Dependabot
|
|
|
57
75
|
# case distinct. PackageSpecifier canonicalises GitHub owner/repo casing
|
|
58
76
|
# itself, so case-insensitive GitHub paths still deduplicate correctly.
|
|
59
77
|
dependency_set = DependencySet.new(case_sensitive: true)
|
|
60
|
-
# Reading the
|
|
78
|
+
# Reading the manifest first validates it and raises
|
|
61
79
|
# DependencyFileNotParseable before we walk it for source positions.
|
|
62
|
-
|
|
80
|
+
parsed_manifest
|
|
63
81
|
|
|
64
82
|
# When the manifest configures a default registry, APM routes plain
|
|
65
83
|
# string-shorthand entries through that registry rather than Git. We
|
|
@@ -69,28 +87,27 @@ module Dependabot
|
|
|
69
87
|
# never registry-routed and are still updated.
|
|
70
88
|
skip_shorthand = default_registry_configured?
|
|
71
89
|
|
|
72
|
-
DEPENDENCY_BLOCKS.
|
|
73
|
-
apm_entries_in(block_key).
|
|
90
|
+
pinned_entries = DEPENDENCY_BLOCKS.flat_map do |block_key, groups|
|
|
91
|
+
apm_entries_in(block_key).filter_map do |entry, declaration_span|
|
|
74
92
|
# v1 supports the string shorthand form (e.g. "owner/repo#v1.0.0").
|
|
75
93
|
# Object entries (git:/registry:/id:/path:) and `mcp` entries are
|
|
76
94
|
# not yet supported and are skipped by only reading scalar entries.
|
|
77
|
-
|
|
95
|
+
# Shorthands resolve against github.com: APM reads no manifest-level
|
|
96
|
+
# default host (only the GITHUB_HOST env var, which Dependabot does
|
|
97
|
+
# not set for the CLI), so `apm lock` resolves them there too.
|
|
98
|
+
spec = PackageSpecifier.parse(entry)
|
|
78
99
|
next unless spec # local paths and unparseable specs are skipped
|
|
79
100
|
next if skip_shorthand && PackageSpecifier.shorthand?(entry)
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
# and unpinned entries are resolved by APM's own lockfile, so we
|
|
83
|
-
# leave them out of the dependency set entirely rather than have the
|
|
84
|
-
# update checker reach out to the git remote for something we will
|
|
85
|
-
# never bump. The tag may be plain (`v1.2.0`) or package-scoped
|
|
86
|
-
# (`review--v1.2.0`), so resolve it through the shared APM extractor.
|
|
87
|
-
ref = spec.ref
|
|
88
|
-
next unless ref && Version.semver_from_ref(ref, dependency_name: spec.name)
|
|
89
|
-
|
|
90
|
-
dependency_set << build_dependency(spec, entry, declaration_span, groups)
|
|
102
|
+
pinned_entry(spec, entry, declaration_span, groups)
|
|
91
103
|
end
|
|
92
104
|
end
|
|
93
105
|
|
|
106
|
+
preferred_pins = preferred_pin_types(pinned_entries)
|
|
107
|
+
pinned_entries.each do |entry|
|
|
108
|
+
dependency_set << entry.dependency if preferred_pins[entry.dependency.name] == entry.pin_type
|
|
109
|
+
end
|
|
110
|
+
|
|
94
111
|
dependency_set.dependencies
|
|
95
112
|
end
|
|
96
113
|
|
|
@@ -107,21 +124,97 @@ module Dependabot
|
|
|
107
124
|
|
|
108
125
|
private
|
|
109
126
|
|
|
127
|
+
# Classifies a pinned manifest entry and builds its dependency, or returns
|
|
128
|
+
# nil for entries Dependabot doesn't update:
|
|
129
|
+
#
|
|
130
|
+
# * tag pins (`v1.2.0`, or package-scoped `review--v1.2.0`) are versioned
|
|
131
|
+
# by the SemVer core of the tag;
|
|
132
|
+
# * full 40-character SHA pins are versioned by the SHA, matching the
|
|
133
|
+
# pins `apm update` moves between annotated release tags;
|
|
134
|
+
# * any other ref is a branch (or similar moving ref) and is versioned by
|
|
135
|
+
# the commit the lockfile resolved it to, so it is only updatable when
|
|
136
|
+
# the lockfile records a commit for that ref.
|
|
137
|
+
#
|
|
138
|
+
# Unpinned entries and abbreviated SHAs are skipped, as APM leaves those
|
|
139
|
+
# unchanged too.
|
|
110
140
|
sig do
|
|
111
141
|
params(
|
|
112
142
|
spec: Dependabot::Apm::PackageSpecifier,
|
|
113
143
|
raw_entry: String,
|
|
114
144
|
declaration_span: String,
|
|
115
145
|
groups: T::Array[String]
|
|
116
|
-
).returns(
|
|
146
|
+
).returns(T.nilable(PinnedEntry))
|
|
117
147
|
end
|
|
118
|
-
def
|
|
148
|
+
def pinned_entry(spec, raw_entry, declaration_span, groups)
|
|
119
149
|
ref = spec.ref
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
150
|
+
return if ref.nil? || ref.empty?
|
|
151
|
+
|
|
152
|
+
declaration = Declaration.new(raw_entry: raw_entry, span: declaration_span, groups: groups)
|
|
153
|
+
tag_version = Version.semver_from_ref(ref, dependency_name: spec.name)
|
|
154
|
+
if tag_version
|
|
155
|
+
return PinnedEntry.new(
|
|
156
|
+
pin_type: PIN_TAG,
|
|
157
|
+
dependency: build_dependency(spec, declaration, version: Version.new(tag_version).to_s)
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
if ref.match?(PackageSpecifier::FULL_SHA_REGEX)
|
|
161
|
+
return PinnedEntry.new(
|
|
162
|
+
pin_type: PIN_SHA,
|
|
163
|
+
dependency: build_dependency(spec, declaration, version: ref.downcase)
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
return if ref.match?(PackageSpecifier::ABBREVIATED_SHA_REGEX)
|
|
167
|
+
|
|
168
|
+
branch_pinned_entry(spec, declaration, ref)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
sig do
|
|
172
|
+
params(spec: Dependabot::Apm::PackageSpecifier, declaration: Declaration, branch: String)
|
|
173
|
+
.returns(T.nilable(PinnedEntry))
|
|
174
|
+
end
|
|
175
|
+
def branch_pinned_entry(spec, declaration, branch)
|
|
176
|
+
lockfile_key = spec.lockfile_key
|
|
177
|
+
commit = parsed_lockfile&.resolved_commit(lockfile_key, ref: branch)
|
|
178
|
+
return unless commit
|
|
179
|
+
|
|
180
|
+
PinnedEntry.new(
|
|
181
|
+
pin_type: PIN_BRANCH,
|
|
182
|
+
dependency: build_dependency(spec, declaration, version: commit, branch: branch, lockfile_key: lockfile_key)
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# DependencySet merges every declaration of a package into one dependency,
|
|
187
|
+
# but a dependency can only carry one kind of version (a tag version or a
|
|
188
|
+
# commit). When a package is declared with several kinds of pin, only the
|
|
189
|
+
# declarations of the most specific kind (tag, then SHA, then branch) are
|
|
190
|
+
# updated; the others are left as they are.
|
|
191
|
+
sig { params(entries: T::Array[PinnedEntry]).returns(T::Hash[String, String]) }
|
|
192
|
+
def preferred_pin_types(entries)
|
|
193
|
+
entries.group_by { |entry| entry.dependency.name }.transform_values do |pins|
|
|
194
|
+
T.must(pins.map(&:pin_type).min_by { |pin_type| T.must(PIN_PRECEDENCE.index(pin_type)) })
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
sig do
|
|
199
|
+
params(
|
|
200
|
+
spec: Dependabot::Apm::PackageSpecifier,
|
|
201
|
+
declaration: Declaration,
|
|
202
|
+
version: String,
|
|
203
|
+
branch: T.nilable(String),
|
|
204
|
+
lockfile_key: T.nilable(String)
|
|
205
|
+
).returns(Dependabot::Dependency)
|
|
206
|
+
end
|
|
207
|
+
def build_dependency(spec, declaration, version:, branch: nil, lockfile_key: nil)
|
|
208
|
+
# `declaration_span` locates the exact manifest scalar (see
|
|
209
|
+
# `encode_span`) so the file updater rewrites only that occurrence,
|
|
210
|
+
# never an identical string elsewhere in the file. Branch pins also
|
|
211
|
+
# record their lockfile identity so the lockfile updater can move the
|
|
212
|
+
# matching lock entry to the branch's new head commit.
|
|
213
|
+
metadata = T.let(
|
|
214
|
+
{ declaration_string: declaration.raw_entry, declaration_span: declaration.span },
|
|
215
|
+
T::Hash[Symbol, String]
|
|
216
|
+
)
|
|
217
|
+
metadata[:lockfile_key] = lockfile_key if lockfile_key
|
|
125
218
|
|
|
126
219
|
Dependency.new(
|
|
127
220
|
name: spec.name,
|
|
@@ -130,20 +223,14 @@ module Dependabot
|
|
|
130
223
|
requirements: [{
|
|
131
224
|
requirement: nil,
|
|
132
225
|
file: manifest_file.name,
|
|
133
|
-
groups: groups,
|
|
226
|
+
groups: declaration.groups,
|
|
134
227
|
source: {
|
|
135
228
|
type: "git",
|
|
136
229
|
url: spec.git_url,
|
|
137
|
-
ref: ref,
|
|
138
|
-
branch:
|
|
230
|
+
ref: spec.ref,
|
|
231
|
+
branch: branch
|
|
139
232
|
},
|
|
140
|
-
|
|
141
|
-
# `encode_span`) so the file updater rewrites only that occurrence,
|
|
142
|
-
# never an identical string elsewhere in the file.
|
|
143
|
-
metadata: {
|
|
144
|
-
declaration_string: raw_entry,
|
|
145
|
-
declaration_span: declaration_span
|
|
146
|
-
}
|
|
233
|
+
metadata: metadata
|
|
147
234
|
}]
|
|
148
235
|
)
|
|
149
236
|
end
|
|
@@ -232,12 +319,6 @@ module Dependabot
|
|
|
232
319
|
)
|
|
233
320
|
end
|
|
234
321
|
|
|
235
|
-
sig { returns(String) }
|
|
236
|
-
def default_host
|
|
237
|
-
host = parsed_manifest["default_host"]
|
|
238
|
-
host.is_a?(String) && !host.empty? ? host : PackageSpecifier::DEFAULT_HOST
|
|
239
|
-
end
|
|
240
|
-
|
|
241
322
|
# APM routes string-shorthand dependencies through a configured default
|
|
242
323
|
# registry instead of Git. Only the project-level `registries.default`
|
|
243
324
|
# selector (a string naming a configured entry) is visible here; a user's
|
|
@@ -286,6 +367,14 @@ module Dependabot
|
|
|
286
367
|
)
|
|
287
368
|
end
|
|
288
369
|
|
|
370
|
+
sig { returns(T.nilable(Dependabot::Apm::Lockfile)) }
|
|
371
|
+
def parsed_lockfile
|
|
372
|
+
return @parsed_lockfile if defined?(@parsed_lockfile)
|
|
373
|
+
|
|
374
|
+
file = lockfile
|
|
375
|
+
@parsed_lockfile = T.let(file && Lockfile.new(file), T.nilable(Dependabot::Apm::Lockfile))
|
|
376
|
+
end
|
|
377
|
+
|
|
289
378
|
sig { override.void }
|
|
290
379
|
def check_required_files
|
|
291
380
|
return if get_original_file(MANIFEST_FILENAME)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
require "dependabot/errors"
|
|
8
|
+
require "dependabot/shared_helpers"
|
|
9
|
+
require "dependabot/apm/file_updater"
|
|
10
|
+
require "dependabot/apm/lockfile"
|
|
11
|
+
require "dependabot/apm/native_helpers"
|
|
12
|
+
|
|
13
|
+
module Dependabot
|
|
14
|
+
module Apm
|
|
15
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
16
|
+
# Regenerates `apm.lock.yaml` with `apm lock`, which resolves the updated
|
|
17
|
+
# manifest without deploying any package files. APM only re-resolves the
|
|
18
|
+
# entries whose manifest ref changed, keeping every other entry on its
|
|
19
|
+
# locked commit, so tag and SHA updates are picked up from the manifest.
|
|
20
|
+
# A branch pin's ref doesn't change, so its lock entry is first moved to
|
|
21
|
+
# the branch's new head commit for APM to verify and re-hash.
|
|
22
|
+
class LockfileUpdater
|
|
23
|
+
extend T::Sig
|
|
24
|
+
|
|
25
|
+
APM_MODULES_DIRECTORY = "apm_modules"
|
|
26
|
+
|
|
27
|
+
AUTHENTICATION_FAILED_REGEX = /Authentication failed for '(?<url>[^']+)'/
|
|
28
|
+
REPOSITORY_NOT_FOUND_REGEX = /repository '(?<url>[^']+)' not found/i
|
|
29
|
+
# APM replaces git's error with its own when a clone is rejected or the
|
|
30
|
+
# repository is missing, e.g. "Failed to clone repository x/y.
|
|
31
|
+
# Authentication failed for clone on github.com."
|
|
32
|
+
APM_AUTHENTICATION_FAILED_REGEX = /
|
|
33
|
+
Failed\sto\sclone\srepository\s(?<repo>[^\s:]+?)\.\s
|
|
34
|
+
Authentication\sfailed\sfor\s[^\n]*?\son\s(?<host>\S+?)\.(?=\s|\z)
|
|
35
|
+
/x
|
|
36
|
+
FAILURE_PREFIX = /Failed\sto\sdownload\sdependency\s|Failed\sto\sresolve\stransitive\sdep\s/
|
|
37
|
+
# One failed package: its name (with an optional transitive `(via …)`
|
|
38
|
+
# chain) and the diagnostic up to the next failure in APM's output.
|
|
39
|
+
PACKAGE_FAILURE_REGEX = /
|
|
40
|
+
(?:#{FAILURE_PREFIX})(?<name>\S+?)(?:\s\(via\s[^)]*\))?:\s
|
|
41
|
+
(?<detail>.*?)(?=\n\s*\+-\s|#{FAILURE_PREFIX}|\z)
|
|
42
|
+
/mx
|
|
43
|
+
# Diagnostics that show a ref is missing. APM's own "Could not resolve
|
|
44
|
+
# commit/reference" wrapper is deliberately not matched: it wraps any
|
|
45
|
+
# failure, including network errors, so only the detail it carries
|
|
46
|
+
# (APM's or git's) says whether the ref exists.
|
|
47
|
+
MISSING_REFERENCE_REGEX = /
|
|
48
|
+
Remote\sbranch\s\S+\snot\sfound|
|
|
49
|
+
Reference\s'[^']+'\snot\sfound|
|
|
50
|
+
Needed\sa\ssingle\srevision|
|
|
51
|
+
unable\sto\sread\stree|
|
|
52
|
+
did\snot\smatch\sany\sfile\(s\)\sknown\sto\sgit|
|
|
53
|
+
reference\sis\snot\sa\stree
|
|
54
|
+
/x
|
|
55
|
+
|
|
56
|
+
sig do
|
|
57
|
+
params(
|
|
58
|
+
dependencies: T::Array[Dependabot::Dependency],
|
|
59
|
+
lockfile: Dependabot::DependencyFile,
|
|
60
|
+
manifest_content: String,
|
|
61
|
+
credentials: T::Array[Dependabot::Credential],
|
|
62
|
+
repo_contents_path: T.nilable(String)
|
|
63
|
+
).void
|
|
64
|
+
end
|
|
65
|
+
def initialize(dependencies:, lockfile:, manifest_content:, credentials:, repo_contents_path:)
|
|
66
|
+
@dependencies = dependencies
|
|
67
|
+
@lockfile = lockfile
|
|
68
|
+
@manifest_content = manifest_content
|
|
69
|
+
@credentials = credentials
|
|
70
|
+
@repo_contents_path = repo_contents_path
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { returns(String) }
|
|
74
|
+
def updated_lockfile_content
|
|
75
|
+
SharedHelpers.in_a_temporary_repo_directory(lockfile.directory, repo_contents_path) do
|
|
76
|
+
# `apm lock` downloads packages into apm_modules/ and resolves
|
|
77
|
+
# transitive dependencies from what is already there. The git reset
|
|
78
|
+
# between updates doesn't remove that ignored directory, so clear it
|
|
79
|
+
# to keep one update's packages from leaking into the next.
|
|
80
|
+
FileUtils.rm_rf(APM_MODULES_DIRECTORY)
|
|
81
|
+
File.write(FileUpdater::MANIFEST_FILENAME, manifest_content)
|
|
82
|
+
File.write(FileUpdater::LOCKFILE_FILENAME, prepared_lockfile_content)
|
|
83
|
+
|
|
84
|
+
SharedHelpers.with_git_configured(credentials: credentials) do
|
|
85
|
+
NativeHelpers.run_apm_command("lock")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
File.read(FileUpdater::LOCKFILE_FILENAME)
|
|
89
|
+
ensure
|
|
90
|
+
FileUtils.rm_rf(APM_MODULES_DIRECTORY)
|
|
91
|
+
end
|
|
92
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
93
|
+
handle_apm_lock_error(e)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
sig { returns(T::Array[Dependabot::Dependency]) }
|
|
99
|
+
attr_reader :dependencies
|
|
100
|
+
|
|
101
|
+
sig { returns(Dependabot::DependencyFile) }
|
|
102
|
+
attr_reader :lockfile
|
|
103
|
+
|
|
104
|
+
sig { returns(String) }
|
|
105
|
+
attr_reader :manifest_content
|
|
106
|
+
|
|
107
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
|
108
|
+
attr_reader :credentials
|
|
109
|
+
|
|
110
|
+
sig { returns(T.nilable(String)) }
|
|
111
|
+
attr_reader :repo_contents_path
|
|
112
|
+
|
|
113
|
+
sig { returns(String) }
|
|
114
|
+
def prepared_lockfile_content
|
|
115
|
+
branch_updates = updated_branch_pins
|
|
116
|
+
return T.must(lockfile.content) if branch_updates.empty?
|
|
117
|
+
|
|
118
|
+
parsed_lockfile = Lockfile.new(lockfile)
|
|
119
|
+
branch_updates.each { |key, commit| parsed_lockfile.pin_commit(key, commit) }
|
|
120
|
+
parsed_lockfile.to_yaml
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The new commit of each updated branch pin, keyed by its lockfile entry.
|
|
124
|
+
sig { returns(T::Hash[String, String]) }
|
|
125
|
+
def updated_branch_pins
|
|
126
|
+
dependencies.each_with_object({}) do |dependency, pins|
|
|
127
|
+
new_commit = dependency.version
|
|
128
|
+
next if new_commit.nil? || new_commit == dependency.previous_version
|
|
129
|
+
|
|
130
|
+
dependency.requirements.each do |requirement|
|
|
131
|
+
key = requirement.metadata_string("lockfile_key")
|
|
132
|
+
pins[key] = new_commit if key && requirement.source_string("branch")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
sig { params(error: SharedHelpers::HelperSubprocessFailed).returns(T.noreturn) }
|
|
138
|
+
def handle_apm_lock_error(error)
|
|
139
|
+
message = error.message
|
|
140
|
+
|
|
141
|
+
unreachable_url = unreachable_repository_url(message)
|
|
142
|
+
raise Dependabot::GitDependenciesNotReachable, unreachable_url if unreachable_url
|
|
143
|
+
|
|
144
|
+
missing_reference = package_with_missing_reference(message)
|
|
145
|
+
raise Dependabot::GitDependencyReferenceNotFound, missing_reference if missing_reference
|
|
146
|
+
|
|
147
|
+
raise Dependabot::DependencyFileNotResolvable, message
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
sig { params(message: String).returns(T.nilable(String)) }
|
|
151
|
+
def unreachable_repository_url(message)
|
|
152
|
+
git_url = message.match(AUTHENTICATION_FAILED_REGEX)&.[](:url) ||
|
|
153
|
+
message.match(REPOSITORY_NOT_FOUND_REGEX)&.[](:url)
|
|
154
|
+
return git_url if git_url
|
|
155
|
+
|
|
156
|
+
apm_failure = message.match(APM_AUTHENTICATION_FAILED_REGEX)
|
|
157
|
+
"https://#{apm_failure[:host]}/#{apm_failure[:repo]}" if apm_failure
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Checks each failed package's own diagnostic, so a missing ref is
|
|
161
|
+
# attributed to the package it belongs to rather than an earlier failure.
|
|
162
|
+
sig { params(message: String).returns(T.nilable(String)) }
|
|
163
|
+
def package_with_missing_reference(message)
|
|
164
|
+
position = 0
|
|
165
|
+
while (failure = PACKAGE_FAILURE_REGEX.match(message, position))
|
|
166
|
+
return failure[:name] if failure[:detail].to_s.match?(MISSING_REFERENCE_REGEX)
|
|
167
|
+
|
|
168
|
+
position = failure.end(0)
|
|
169
|
+
end
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -13,6 +13,13 @@ module Dependabot
|
|
|
13
13
|
extend T::Sig
|
|
14
14
|
|
|
15
15
|
MANIFEST_FILENAME = "apm.yml"
|
|
16
|
+
LOCKFILE_FILENAME = "apm.lock.yaml"
|
|
17
|
+
|
|
18
|
+
# What may follow a SHA pin on its line for `apm update`'s `# <tag>`
|
|
19
|
+
# comment to be written: nothing but whitespace or an existing comment.
|
|
20
|
+
LINE_REMAINDER_REGEX = /\A[ \t]*(?:#[^\r\n]*)?(?=\r?\n|\z)/
|
|
21
|
+
|
|
22
|
+
require_relative "file_updater/lockfile_updater"
|
|
16
23
|
|
|
17
24
|
# A single ref-bump edit, located by an absolute [start_offset, end_offset)
|
|
18
25
|
# character range in the original manifest content.
|
|
@@ -24,7 +31,7 @@ module Dependabot
|
|
|
24
31
|
|
|
25
32
|
sig { returns(T::Array[Regexp]) }
|
|
26
33
|
def self.updated_files_regex
|
|
27
|
-
[/^apm\.yml$/]
|
|
34
|
+
[/^apm\.yml$/, /^apm\.lock\.yaml$/]
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
|
|
@@ -35,6 +42,9 @@ module Dependabot
|
|
|
35
42
|
updated_file(file: file, content: updated_manifest_content(file))
|
|
36
43
|
end
|
|
37
44
|
|
|
45
|
+
updated_lockfile = updated_lockfile(updated_files)
|
|
46
|
+
updated_files << updated_lockfile if updated_lockfile
|
|
47
|
+
|
|
38
48
|
raise "No files changed!" if updated_files.none?
|
|
39
49
|
|
|
40
50
|
updated_files
|
|
@@ -59,6 +69,31 @@ module Dependabot
|
|
|
59
69
|
dependencies.any? { |dep| requirement_changed?(file, dep) }
|
|
60
70
|
end
|
|
61
71
|
|
|
72
|
+
# Regenerates the lockfile against the updated manifest. A branch pin
|
|
73
|
+
# update changes only the lockfile, as the manifest still names the branch.
|
|
74
|
+
# The lockfile is only ever updated, never created.
|
|
75
|
+
sig do
|
|
76
|
+
params(updated_manifests: T::Array[Dependabot::DependencyFile])
|
|
77
|
+
.returns(T.nilable(Dependabot::DependencyFile))
|
|
78
|
+
end
|
|
79
|
+
def updated_lockfile(updated_manifests)
|
|
80
|
+
lockfile = get_original_file(LOCKFILE_FILENAME)
|
|
81
|
+
manifest = get_original_file(MANIFEST_FILENAME)
|
|
82
|
+
return unless lockfile && manifest
|
|
83
|
+
|
|
84
|
+
manifest_content = updated_manifests.find { |f| f.name == manifest.name }&.content || manifest.content
|
|
85
|
+
content = LockfileUpdater.new(
|
|
86
|
+
dependencies: dependencies,
|
|
87
|
+
lockfile: lockfile,
|
|
88
|
+
manifest_content: T.must(manifest_content),
|
|
89
|
+
credentials: credentials,
|
|
90
|
+
repo_contents_path: repo_contents_path
|
|
91
|
+
).updated_lockfile_content
|
|
92
|
+
return if content == lockfile.content
|
|
93
|
+
|
|
94
|
+
updated_file(file: lockfile, content: content)
|
|
95
|
+
end
|
|
96
|
+
|
|
62
97
|
sig { params(file: Dependabot::DependencyFile).returns(String) }
|
|
63
98
|
def updated_manifest_content(file)
|
|
64
99
|
original_content = T.must(file.content)
|
|
@@ -125,11 +160,32 @@ module Dependabot
|
|
|
125
160
|
return unless original.include?(declaration)
|
|
126
161
|
|
|
127
162
|
new_declaration = declaration.sub(ref_at_end, "##{new_ref}\\1")
|
|
128
|
-
Substitution.new(
|
|
163
|
+
substitution = Substitution.new(
|
|
129
164
|
start_offset: start_offset,
|
|
130
165
|
end_offset: end_offset,
|
|
131
166
|
text: original.sub(declaration, new_declaration)
|
|
132
167
|
)
|
|
168
|
+
with_release_tag_comment(content, substitution, new_req.metadata_string("release_tag"))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Like `apm update`, annotates a bumped SHA pin with the release it now
|
|
172
|
+
# points at, replacing any comment already on the line. The comment is
|
|
173
|
+
# only written when nothing but whitespace or a comment follows the entry
|
|
174
|
+
# on its line, so entries in a flow sequence are left uncommented.
|
|
175
|
+
sig do
|
|
176
|
+
params(content: String, substitution: Substitution, release_tag: T.nilable(String)).returns(Substitution)
|
|
177
|
+
end
|
|
178
|
+
def with_release_tag_comment(content, substitution, release_tag)
|
|
179
|
+
return substitution unless release_tag
|
|
180
|
+
|
|
181
|
+
line_remainder = content[substitution.end_offset..]&.match(LINE_REMAINDER_REGEX)
|
|
182
|
+
return substitution unless line_remainder
|
|
183
|
+
|
|
184
|
+
Substitution.new(
|
|
185
|
+
start_offset: substitution.start_offset,
|
|
186
|
+
end_offset: substitution.end_offset + line_remainder[0].to_s.length,
|
|
187
|
+
text: "#{substitution.text} # #{release_tag}"
|
|
188
|
+
)
|
|
133
189
|
end
|
|
134
190
|
|
|
135
191
|
# Applies the substitutions from the end of the file backwards. Rewriting in
|
|
@@ -49,8 +49,63 @@ module Dependabot
|
|
|
49
49
|
to_local_tag(max_version_tag)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# The release a commit-SHA pin should move to, following `apm update`:
|
|
53
|
+
# the highest non-prerelease version tag in any of APM's tag forms
|
|
54
|
+
# (plain or package-scoped), considering only annotated tags. Lightweight
|
|
55
|
+
# tags and branches are never used, since they are not deliberate
|
|
56
|
+
# releases. Ignore conditions and cooldown apply as for tag pins.
|
|
57
|
+
#
|
|
58
|
+
# APM moves a SHA pin to that release even when the pinned commit is
|
|
59
|
+
# newer. When the pinned commit is itself a tagged release, Dependabot
|
|
60
|
+
# additionally requires a strictly higher version so it never proposes a
|
|
61
|
+
# downgrade.
|
|
62
|
+
sig do
|
|
63
|
+
params(cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions))
|
|
64
|
+
.returns(T.nilable(Dependabot::GitTagDetails))
|
|
65
|
+
end
|
|
66
|
+
def latest_annotated_release_tag(cooldown_options = nil)
|
|
67
|
+
candidates = allowed_versions(local_tags, filter_by_prefix: false).select { |tag| annotated_release?(tag) }
|
|
68
|
+
pinned_version = current_version
|
|
69
|
+
candidates = candidates.select { |tag| version_from_tag(tag) > pinned_version } if pinned_version
|
|
70
|
+
|
|
71
|
+
filtered = apply_cooldown(candidates, cooldown_options)
|
|
72
|
+
return if filtered.nil?
|
|
73
|
+
|
|
74
|
+
max_local_tag(filtered)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A SHA pin carries no version of its own, so the inherited lookup (which
|
|
78
|
+
# reads `dependency.version`) yields nil and cooldown would fall back to
|
|
79
|
+
# `default_days` for every candidate. When the pinned commit is a tagged
|
|
80
|
+
# release, use that version so SemVer-specific cooldown days apply.
|
|
81
|
+
sig { override.returns(T.nilable(Gem::Version)) }
|
|
82
|
+
def current_version
|
|
83
|
+
super || pinned_commit_version
|
|
84
|
+
end
|
|
85
|
+
|
|
52
86
|
private
|
|
53
87
|
|
|
88
|
+
# An annotated tag is advertised as a tag object whose peeled (`^{}`)
|
|
89
|
+
# commit differs from it; a lightweight tag points at the commit directly.
|
|
90
|
+
sig { params(tag: Dependabot::GitRef).returns(T::Boolean) }
|
|
91
|
+
def annotated_release?(tag)
|
|
92
|
+
return false if tag.ref_sha.nil? || tag.ref_sha == tag.commit_sha
|
|
93
|
+
|
|
94
|
+
!version_from_tag(tag).prerelease?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The highest version tagged on the pinned commit, if any.
|
|
98
|
+
sig { returns(T.nilable(Gem::Version)) }
|
|
99
|
+
def pinned_commit_version
|
|
100
|
+
pinned_sha = ref&.downcase
|
|
101
|
+
return unless pinned_sha
|
|
102
|
+
|
|
103
|
+
local_tags
|
|
104
|
+
.select { |tag| tag.commit_sha == pinned_sha && version_tag?(tag.name) }
|
|
105
|
+
.map { |tag| version_from_tag(tag) }
|
|
106
|
+
.max
|
|
107
|
+
end
|
|
108
|
+
|
|
54
109
|
# Recognise a tag as a version only when it is a valid APM SemVer ref
|
|
55
110
|
# (optionally package-scoped), so non-SemVer tags (e.g. `v1.2.3.4`) are
|
|
56
111
|
# dropped before they can raise an ArgumentError, while build-metadata
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
require "dependabot/dependency_file"
|
|
8
|
+
require "dependabot/errors"
|
|
9
|
+
require "dependabot/apm/package_specifier"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Apm
|
|
13
|
+
# A parsed `apm.lock.yaml`, with its `dependencies` entries addressable by
|
|
14
|
+
# APM's package identity (see PackageSpecifier#lockfile_key).
|
|
15
|
+
class Lockfile
|
|
16
|
+
extend T::Sig
|
|
17
|
+
|
|
18
|
+
sig { params(file: Dependabot::DependencyFile).void }
|
|
19
|
+
def initialize(file)
|
|
20
|
+
@document = T.let(parse(file), T::Hash[Object, Object])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The commit the lockfile pins for `key`, provided it was resolved from
|
|
24
|
+
# `ref`. A lock entry resolved from a different ref is stale (APM
|
|
25
|
+
# re-resolves it on the next run), so it is not reported as the
|
|
26
|
+
# dependency's current commit.
|
|
27
|
+
sig { params(key: String, ref: String).returns(T.nilable(String)) }
|
|
28
|
+
def resolved_commit(key, ref:)
|
|
29
|
+
entry = entry_for(key)
|
|
30
|
+
return unless entry && entry["resolved_ref"] == ref
|
|
31
|
+
|
|
32
|
+
commit = entry["resolved_commit"]
|
|
33
|
+
commit.is_a?(String) && !commit.empty? ? commit : nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Moves the entry for `key` to `commit`. Its `content_hash` describes the
|
|
37
|
+
# previous commit's contents, and APM fails a hash mismatch as a potential
|
|
38
|
+
# supply-chain attack, so it is dropped for APM to recompute. The rest of
|
|
39
|
+
# the entry (e.g. `deployed_files`) is kept.
|
|
40
|
+
sig { params(key: String, commit: String).void }
|
|
41
|
+
def pin_commit(key, commit)
|
|
42
|
+
entry = entry_for(key)
|
|
43
|
+
return unless entry
|
|
44
|
+
|
|
45
|
+
entry["resolved_commit"] = commit
|
|
46
|
+
entry.delete("content_hash")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { returns(String) }
|
|
50
|
+
def to_yaml
|
|
51
|
+
@document.to_yaml
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
sig { params(key: String).returns(T.nilable(T::Hash[Object, Object])) }
|
|
57
|
+
def entry_for(key)
|
|
58
|
+
entries = @document["dependencies"]
|
|
59
|
+
return unless entries.is_a?(Array)
|
|
60
|
+
|
|
61
|
+
entries.each do |entry|
|
|
62
|
+
return entry if entry.is_a?(Hash) && entry_key(entry) == key
|
|
63
|
+
end
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(entry: T::Hash[Object, Object]).returns(T.nilable(String)) }
|
|
68
|
+
def entry_key(entry)
|
|
69
|
+
repo_url = entry["repo_url"]
|
|
70
|
+
return unless repo_url.is_a?(String)
|
|
71
|
+
|
|
72
|
+
host = entry["host"]
|
|
73
|
+
virtual_path = entry["virtual_path"]
|
|
74
|
+
PackageSpecifier.lockfile_key(
|
|
75
|
+
host: host.is_a?(String) && !host.empty? ? host : PackageSpecifier::DEFAULT_HOST,
|
|
76
|
+
repo_url: repo_url,
|
|
77
|
+
virtual_path: virtual_path.is_a?(String) && !virtual_path.empty? ? virtual_path : nil
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Hash[Object, Object]) }
|
|
82
|
+
def parse(file)
|
|
83
|
+
parsed = YAML.safe_load(T.must(file.content))
|
|
84
|
+
parsed = {} if parsed.nil?
|
|
85
|
+
raise Dependabot::DependencyFileNotParseable, file.path unless parsed.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
parsed
|
|
88
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
|
|
89
|
+
raise Dependabot::DependencyFileNotParseable, file.path
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
require "dependabot/shared_helpers"
|
|
8
|
+
|
|
9
|
+
module Dependabot
|
|
10
|
+
module Apm
|
|
11
|
+
module NativeHelpers
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
# APM renders its output through a terminal-width-aware console, which
|
|
15
|
+
# wraps long error messages mid-sentence. A wide, colourless console keeps
|
|
16
|
+
# each message on one line so errors can be matched reliably.
|
|
17
|
+
COMMAND_ENV = T.let(
|
|
18
|
+
{
|
|
19
|
+
"COLUMNS" => "1000",
|
|
20
|
+
"NO_COLOR" => "1",
|
|
21
|
+
"GIT_TERMINAL_PROMPT" => "0"
|
|
22
|
+
}.freeze,
|
|
23
|
+
T::Hash[String, String]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
sig { returns(String) }
|
|
27
|
+
def self.apm_path
|
|
28
|
+
clean_path(File.join(native_helpers_root, "apm/bin/apm"))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Runs an APM CLI command (e.g. `lock`) in the current directory, raising
|
|
32
|
+
# SharedHelpers::HelperSubprocessFailed with APM's output on failure.
|
|
33
|
+
sig { params(command: String).returns(String) }
|
|
34
|
+
def self.run_apm_command(command)
|
|
35
|
+
SharedHelpers.run_shell_command(
|
|
36
|
+
"#{apm_path} #{command}",
|
|
37
|
+
env: COMMAND_ENV,
|
|
38
|
+
fingerprint: "apm #{command}"
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { returns(String) }
|
|
43
|
+
def self.native_helpers_root
|
|
44
|
+
default_path = File.join(__dir__, "../../../helpers/install-dir")
|
|
45
|
+
ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { params(path: String).returns(String) }
|
|
49
|
+
def self.clean_path(path)
|
|
50
|
+
Pathname.new(path).cleanpath.to_path
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -37,7 +37,9 @@ module Dependabot
|
|
|
37
37
|
# path is treated as the repository so nested subgroups resolve to the
|
|
38
38
|
# correct remote; virtual packages there are out of scope for v1, as is
|
|
39
39
|
# self-hosted GHES on an arbitrary hostname that cannot be recognised from
|
|
40
|
-
# the host alone.
|
|
40
|
+
# the host alone. As in APM, explicit clone URLs never carry a virtual path,
|
|
41
|
+
# so a URL with segments beyond `owner/repo` on a GitHub-family host
|
|
42
|
+
# resolves to `nil`.
|
|
41
43
|
#
|
|
42
44
|
# Azure DevOps hosts (`dev.azure.com`, `ssh.dev.azure.com` and legacy
|
|
43
45
|
# `*.visualstudio.com`) expose repositories at `org/project/_git/repo`, a
|
|
@@ -52,6 +54,12 @@ module Dependabot
|
|
|
52
54
|
|
|
53
55
|
DEFAULT_HOST = "github.com"
|
|
54
56
|
|
|
57
|
+
# APM only moves refs that are full commit SHAs (matched
|
|
58
|
+
# case-insensitively). Abbreviated SHAs are left alone, since they cannot
|
|
59
|
+
# be told apart from a branch name reliably.
|
|
60
|
+
FULL_SHA_REGEX = /\A[0-9a-f]{40}\z/i
|
|
61
|
+
ABBREVIATED_SHA_REGEX = /\A[0-9a-f]{7,39}\z/i
|
|
62
|
+
|
|
55
63
|
# Azure DevOps Services hosts. Their repositories live at
|
|
56
64
|
# `org/project/_git/repo`, a structure this generic owner/repo builder
|
|
57
65
|
# cannot express, so ADO entries are out of scope for v1 (see README) and
|
|
@@ -98,6 +106,7 @@ module Dependabot
|
|
|
98
106
|
spec, _, ref = entry.partition("#")
|
|
99
107
|
host, path = split_host_and_path(spec, default_host)
|
|
100
108
|
return nil unless path
|
|
109
|
+
return nil if explicit_git_form?(spec) && embedded_sub_path?(host, path)
|
|
101
110
|
|
|
102
111
|
build(host: host, path: path, ref: ref, default_host: default_host)
|
|
103
112
|
end
|
|
@@ -233,6 +242,24 @@ module Dependabot
|
|
|
233
242
|
T.must(match[:port]).to_i == 443 ? T.must(match[:hostname]) : host
|
|
234
243
|
end
|
|
235
244
|
|
|
245
|
+
sig { params(spec: String).returns(T::Boolean) }
|
|
246
|
+
def self.explicit_git_form?(spec)
|
|
247
|
+
spec.match?(SCP_STYLE) || spec.match?(URL_STYLE)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# APM never infers a virtual package from an explicit clone URL: virtual
|
|
251
|
+
# packages must use the shorthand or the object form's `path:` key. A
|
|
252
|
+
# GitHub-family URL with segments beyond `owner/repo`, or any URL with a
|
|
253
|
+
# path after a `.git` segment, is rejected by `apm` (or names a repository
|
|
254
|
+
# that cannot exist), so such entries are skipped rather than updated.
|
|
255
|
+
sig { params(host: String, path: String).returns(T::Boolean) }
|
|
256
|
+
def self.embedded_sub_path?(host, path)
|
|
257
|
+
segments = path.split("/").reject(&:empty?)
|
|
258
|
+
return true if segments[0...-1]&.any? { |segment| segment.end_with?(".git") }
|
|
259
|
+
|
|
260
|
+
github_family?(host.downcase) && segments.length > 2
|
|
261
|
+
end
|
|
262
|
+
|
|
236
263
|
sig { params(entry: String).returns(T::Boolean) }
|
|
237
264
|
def self.local_path?(entry)
|
|
238
265
|
entry.start_with?("./", "../", "/", "~/", ".\\", "..\\", "~\\") ||
|
|
@@ -342,6 +369,35 @@ module Dependabot
|
|
|
342
369
|
virtual_path = sub_path
|
|
343
370
|
virtual_path ? "#{repo_name}/#{virtual_path}" : repo_name
|
|
344
371
|
end
|
|
372
|
+
|
|
373
|
+
# The key APM uses to identify this package in `apm.lock.yaml`. Unlike
|
|
374
|
+
# `name`, it ignores the manifest's `default_host` and any port: APM keys
|
|
375
|
+
# entries by `repo_url`, prefixed by the host for anything other than
|
|
376
|
+
# github.com, plus the virtual path for virtual packages. The virtual path
|
|
377
|
+
# is joined with `//`, which never appears in a repository path, so a
|
|
378
|
+
# virtual package only matches a lock entry with the same repository: a
|
|
379
|
+
# GitLab `acme/prompts/skills/review` repository doesn't match the
|
|
380
|
+
# `acme/prompts` repository's `skills/review` package.
|
|
381
|
+
sig { returns(String) }
|
|
382
|
+
def lockfile_key
|
|
383
|
+
self.class.lockfile_key(
|
|
384
|
+
host: self.class.hostname_without_port(host),
|
|
385
|
+
repo_url: "#{owner}/#{repo}",
|
|
386
|
+
virtual_path: sub_path
|
|
387
|
+
)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Builds a lockfile key from a lock entry's coordinates (or a manifest
|
|
391
|
+
# entry's, via #lockfile_key). GitHub-family identifiers are
|
|
392
|
+
# case-insensitive, so they are lowercased; other hosts keep their casing,
|
|
393
|
+
# as APM does.
|
|
394
|
+
sig { params(host: String, repo_url: String, virtual_path: T.nilable(String)).returns(String) }
|
|
395
|
+
def self.lockfile_key(host:, repo_url:, virtual_path: nil)
|
|
396
|
+
hostname = host.downcase
|
|
397
|
+
repo_url = repo_url.downcase if github_family?(hostname)
|
|
398
|
+
key = hostname == DEFAULT_HOST ? repo_url : "#{hostname}/#{repo_url}"
|
|
399
|
+
virtual_path ? "#{key}//#{virtual_path}" : key
|
|
400
|
+
end
|
|
345
401
|
end
|
|
346
402
|
end
|
|
347
403
|
end
|
|
@@ -9,6 +9,7 @@ require "dependabot/git_tag_details"
|
|
|
9
9
|
require "dependabot/update_checkers"
|
|
10
10
|
require "dependabot/update_checkers/base"
|
|
11
11
|
require "dependabot/apm/git_commit_checker"
|
|
12
|
+
require "dependabot/apm/package_specifier"
|
|
12
13
|
require "dependabot/apm/requirement"
|
|
13
14
|
require "dependabot/apm/version"
|
|
14
15
|
|
|
@@ -34,8 +35,12 @@ module Dependabot
|
|
|
34
35
|
|
|
35
36
|
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
36
37
|
def latest_resolvable_version_with_no_unlock
|
|
37
|
-
#
|
|
38
|
-
#
|
|
38
|
+
# A branch pin moves to the branch's new head commit through the
|
|
39
|
+
# lockfile alone, without touching the manifest requirement.
|
|
40
|
+
return latest_version if branch_pinned?
|
|
41
|
+
|
|
42
|
+
# Every other APM update rewrites its manifest requirement, so there is
|
|
43
|
+
# no newer version reachable "without unlocking".
|
|
39
44
|
dependency.version
|
|
40
45
|
end
|
|
41
46
|
|
|
@@ -59,6 +64,7 @@ module Dependabot
|
|
|
59
64
|
sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
60
65
|
def updated_requirements
|
|
61
66
|
return dependency.requirements unless git_commit_checker.git_dependency?
|
|
67
|
+
return updated_sha_pin_requirements if sha_pinned?
|
|
62
68
|
|
|
63
69
|
dependency.requirements.map do |req|
|
|
64
70
|
source = req.source_hash
|
|
@@ -98,6 +104,25 @@ module Dependabot
|
|
|
98
104
|
raise NotImplementedError
|
|
99
105
|
end
|
|
100
106
|
|
|
107
|
+
# The inherited check compares only the merged `dependency.version` (the
|
|
108
|
+
# first declaration's SHA) with the latest release commit. When that first
|
|
109
|
+
# declaration is already at the release but a later one is pinned to an
|
|
110
|
+
# older SHA, both resolve to the same commit and the outdated declaration
|
|
111
|
+
# would never be moved, so judge SHA pins by their individual declarations.
|
|
112
|
+
sig { returns(T::Boolean) }
|
|
113
|
+
def sha1_version_up_to_date?
|
|
114
|
+
return super unless sha_pinned?
|
|
115
|
+
|
|
116
|
+
updated_sha_pin_requirements == dependency.requirements
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { returns(T::Boolean) }
|
|
120
|
+
def preferred_version_resolvable_with_unlock?
|
|
121
|
+
return super unless sha_pinned?
|
|
122
|
+
|
|
123
|
+
!sha1_version_up_to_date?
|
|
124
|
+
end
|
|
125
|
+
|
|
101
126
|
# `latest_version` deliberately reports the MAX tag reachable by ANY family
|
|
102
127
|
# so the base `can_update?` gate still fires when only a higher family can
|
|
103
128
|
# move. But `DependencySet` defines a merged dependency's version as its
|
|
@@ -111,8 +136,12 @@ module Dependabot
|
|
|
111
136
|
# version separate.
|
|
112
137
|
sig { returns(Dependabot::Dependency) }
|
|
113
138
|
def updated_dependency_with_own_req_unlock
|
|
139
|
+
# A branch pin is versioned by commit, so the base behaviour of
|
|
140
|
+
# reporting the latest commit as the new version applies to it.
|
|
141
|
+
return super unless tag_pinned? || sha_pinned?
|
|
142
|
+
|
|
114
143
|
new_requirements = updated_requirements
|
|
115
|
-
new_version = pinned_versions(new_requirements).min
|
|
144
|
+
new_version = sha_pinned? ? merged_sha_version(new_requirements) : pinned_versions(new_requirements).min
|
|
116
145
|
|
|
117
146
|
Dependabot::Dependency.new(
|
|
118
147
|
name: dependency.name,
|
|
@@ -126,13 +155,21 @@ module Dependabot
|
|
|
126
155
|
)
|
|
127
156
|
end
|
|
128
157
|
|
|
158
|
+
# DependencySet keeps the first declaration's version when merging SHA
|
|
159
|
+
# pins, so that's the version the updated manifest parses to.
|
|
160
|
+
sig { params(requirements: T::Array[Dependabot::DependencyRequirement]).returns(T.nilable(String)) }
|
|
161
|
+
def merged_sha_version(requirements)
|
|
162
|
+
requirements.first&.source_string("ref")&.downcase
|
|
163
|
+
end
|
|
164
|
+
|
|
129
165
|
sig { returns(T.nilable(T.any(String, Gem::Version))) }
|
|
130
166
|
def fetch_latest_version
|
|
131
167
|
return dependency.version unless git_commit_checker.git_dependency?
|
|
168
|
+
return latest_branch_commit if branch_pinned?
|
|
169
|
+
return latest_sha_pin_commit if sha_pinned?
|
|
132
170
|
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
# APM regenerates itself, so Dependabot leaves them untouched.
|
|
171
|
+
# Unpinned entries are never parsed, so any other dependency is
|
|
172
|
+
# expected to be pinned to a version tag.
|
|
136
173
|
return dependency.version unless git_commit_checker.pinned_ref_looks_like_version?
|
|
137
174
|
|
|
138
175
|
# A merged dependency can hold several ref families (e.g. `review--v*`
|
|
@@ -146,6 +183,74 @@ module Dependabot
|
|
|
146
183
|
end.max || dependency.version
|
|
147
184
|
end
|
|
148
185
|
|
|
186
|
+
sig { returns(T::Boolean) }
|
|
187
|
+
def branch_pinned?
|
|
188
|
+
dependency.requirements.any? { |req| req.source_string("branch") }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
sig { returns(T::Boolean) }
|
|
192
|
+
def sha_pinned?
|
|
193
|
+
refs = dependency.requirements.map { |req| req.source_string("ref") }
|
|
194
|
+
refs.any? && refs.all? { |ref| ref&.match?(PackageSpecifier::FULL_SHA_REGEX) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
sig { returns(T::Boolean) }
|
|
198
|
+
def tag_pinned?
|
|
199
|
+
!branch_pinned? && !sha_pinned?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A branch pin is versioned by the commit the lockfile resolved, and moves
|
|
203
|
+
# to the branch's current head commit.
|
|
204
|
+
sig { returns(T.nilable(String)) }
|
|
205
|
+
def latest_branch_commit
|
|
206
|
+
git_commit_checker.head_commit_for_current_branch
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The commit of the release each SHA-pinned declaration moves to. All of a
|
|
210
|
+
# dependency's declarations share one repository, so they resolve to the
|
|
211
|
+
# same release unless a declaration is already pinned beyond it.
|
|
212
|
+
sig { returns(T.nilable(String)) }
|
|
213
|
+
def latest_sha_pin_commit
|
|
214
|
+
dependency.requirements.filter_map { |req| sha_pin_release(req)&.commit_sha }.first || dependency.version
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Moves each SHA pin to its release commit, recording the release tag so
|
|
218
|
+
# the manifest can be annotated with it as `apm update` does.
|
|
219
|
+
sig { returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
220
|
+
def updated_sha_pin_requirements
|
|
221
|
+
dependency.requirements.map do |req|
|
|
222
|
+
release = sha_pin_release(req)
|
|
223
|
+
source = req.source_hash
|
|
224
|
+
next req unless release && source
|
|
225
|
+
|
|
226
|
+
Dependabot::DependencyRequirement.create(
|
|
227
|
+
req.merge(
|
|
228
|
+
source: source.merge(ref: release.commit_sha),
|
|
229
|
+
metadata: (req.metadata || {}).merge(release_tag: release.tag)
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(T.nilable(Dependabot::GitTagDetails)) }
|
|
236
|
+
def sha_pin_release(req)
|
|
237
|
+
source = req.source_hash
|
|
238
|
+
pinned_sha = req.source_string("ref")&.downcase
|
|
239
|
+
return unless source && pinned_sha&.match?(PackageSpecifier::FULL_SHA_REGEX)
|
|
240
|
+
|
|
241
|
+
releases = sha_pin_releases
|
|
242
|
+
return releases[pinned_sha] if releases.key?(pinned_sha)
|
|
243
|
+
|
|
244
|
+
release = git_commit_checker_for(source).latest_annotated_release_tag(update_cooldown)
|
|
245
|
+
release = nil if release&.commit_sha.nil? || release&.commit_sha == pinned_sha
|
|
246
|
+
releases[pinned_sha] = release
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
sig { returns(T::Hash[String, T.nilable(Dependabot::GitTagDetails)]) }
|
|
250
|
+
def sha_pin_releases
|
|
251
|
+
@sha_pin_releases ||= T.let({}, T.nilable(T::Hash[String, T.nilable(Dependabot::GitTagDetails)]))
|
|
252
|
+
end
|
|
253
|
+
|
|
149
254
|
# The tag a single requirement should move to: the latest tag resolved
|
|
150
255
|
# within that requirement's own ref family (via a checker scoped to its
|
|
151
256
|
# ref). Returns nil when there is no strictly-higher tag, so a requirement
|
|
@@ -235,7 +340,7 @@ module Dependabot
|
|
|
235
340
|
params(
|
|
236
341
|
source: Dependabot::DependencyRequirement::ObjectHash,
|
|
237
342
|
ref_version: T.nilable(String)
|
|
238
|
-
).returns(Dependabot::GitCommitChecker)
|
|
343
|
+
).returns(Dependabot::Apm::GitCommitChecker)
|
|
239
344
|
end
|
|
240
345
|
def git_commit_checker_for(source, ref_version = nil)
|
|
241
346
|
scoped_dependency =
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-apm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.402.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.402.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.402.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -245,8 +245,11 @@ files:
|
|
|
245
245
|
- lib/dependabot/apm/file_fetcher.rb
|
|
246
246
|
- lib/dependabot/apm/file_parser.rb
|
|
247
247
|
- lib/dependabot/apm/file_updater.rb
|
|
248
|
+
- lib/dependabot/apm/file_updater/lockfile_updater.rb
|
|
248
249
|
- lib/dependabot/apm/git_commit_checker.rb
|
|
250
|
+
- lib/dependabot/apm/lockfile.rb
|
|
249
251
|
- lib/dependabot/apm/metadata_finder.rb
|
|
252
|
+
- lib/dependabot/apm/native_helpers.rb
|
|
250
253
|
- lib/dependabot/apm/package_manager.rb
|
|
251
254
|
- lib/dependabot/apm/package_specifier.rb
|
|
252
255
|
- lib/dependabot/apm/requirement.rb
|
|
@@ -257,7 +260,7 @@ licenses:
|
|
|
257
260
|
- MIT
|
|
258
261
|
metadata:
|
|
259
262
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
260
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
263
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.402.0
|
|
261
264
|
rdoc_options: []
|
|
262
265
|
require_paths:
|
|
263
266
|
- lib
|