dependabot-nix 0.383.0 → 0.384.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/nix/channel.rb +55 -0
- data/lib/dependabot/nix/file_parser.rb +54 -2
- data/lib/dependabot/nix/flake_nix_parser.rb +64 -30
- data/lib/dependabot/nix/ignore_filter.rb +44 -0
- data/lib/dependabot/nix/metadata_finder.rb +8 -2
- data/lib/dependabot/nix/update_checker/channel_version_finder.rb +126 -0
- data/lib/dependabot/nix/update_checker/versioned_branch_finder.rb +21 -88
- data/lib/dependabot/nix/update_checker.rb +77 -2
- data/lib/dependabot/nix/versioned_name.rb +88 -0
- metadata +9 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc6760fd535e74086902529169aa58860943e51c3b7210d0a2608b12b5cfdf47
|
|
4
|
+
data.tar.gz: 239b9f0415ca75a7451d184baaa88a50c3e95af54c9a7351f0b08bceb2a6694b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47dd160601292ee0357e80c8011e67c18b5cb42afc361d3df5d0b1e59f444a2c2ae0a53a9e1f18c7471d35d77267e79f80ebc8f40ba9e27fc473acec88f67f98
|
|
7
|
+
data.tar.gz: 020f4cfed1fc8b9ada9e1cf97c806614e07ea27dc8b322614928e403709140d61ffb34d9565bdc34e84ef7b3a91c7a2d42957bd46cb6b4a13aee1cc0d8eb5017
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/nix/versioned_name"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module Nix
|
|
10
|
+
# A NixOS channel: a VersionedName plus helpers for channel tarball URLs
|
|
11
|
+
# (channels.nixos.org/<channel>/nixexprs.tar.xz).
|
|
12
|
+
class Channel < VersionedName
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
CHANNEL_HOST = "channels.nixos.org"
|
|
16
|
+
DEFAULT_EXTENSION = "xz"
|
|
17
|
+
|
|
18
|
+
# e.g. https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz
|
|
19
|
+
# The suffix is captured so a bump keeps the flake's existing format.
|
|
20
|
+
CHANNEL_URL_PATTERN = %r{
|
|
21
|
+
\Ahttps?://channels\.nixos\.org/
|
|
22
|
+
(?<channel>[a-zA-Z0-9][a-zA-Z0-9._-]*)
|
|
23
|
+
/nixexprs\.tar\.(?<extension>xz|gz|bz2)\z
|
|
24
|
+
}x
|
|
25
|
+
|
|
26
|
+
sig { params(url: T.nilable(String)).returns(T::Boolean) }
|
|
27
|
+
def self.channel_url?(url)
|
|
28
|
+
return false unless url
|
|
29
|
+
|
|
30
|
+
CHANNEL_URL_PATTERN.match?(url)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(url: T.nilable(String)).returns(T.nilable(String)) }
|
|
34
|
+
def self.channel_name_from_url(url)
|
|
35
|
+
return unless url
|
|
36
|
+
|
|
37
|
+
CHANNEL_URL_PATTERN.match(url)&.[](:channel)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The compression suffix (xz, gz, bz2) of a channel tarball URL.
|
|
41
|
+
sig { params(url: T.nilable(String)).returns(T.nilable(String)) }
|
|
42
|
+
def self.extension_from_url(url)
|
|
43
|
+
return unless url
|
|
44
|
+
|
|
45
|
+
CHANNEL_URL_PATTERN.match(url)&.[](:extension)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Preserves the flake's suffix so a bump keeps its compression format.
|
|
49
|
+
sig { params(channel_name: String, extension: String).returns(String) }
|
|
50
|
+
def self.url_for(channel_name, extension: DEFAULT_EXTENSION)
|
|
51
|
+
"https://#{CHANNEL_HOST}/#{channel_name}/nixexprs.tar.#{extension}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -8,6 +8,7 @@ require "dependabot/dependency"
|
|
|
8
8
|
require "dependabot/file_parsers"
|
|
9
9
|
require "dependabot/file_parsers/base"
|
|
10
10
|
require "dependabot/shared_helpers"
|
|
11
|
+
require "dependabot/nix/channel"
|
|
11
12
|
require "dependabot/nix/package_manager"
|
|
12
13
|
|
|
13
14
|
module Dependabot
|
|
@@ -15,8 +16,8 @@ module Dependabot
|
|
|
15
16
|
class FileParser < Dependabot::FileParsers::Base
|
|
16
17
|
extend T::Sig
|
|
17
18
|
|
|
18
|
-
#
|
|
19
|
-
SUPPORTED_SOURCE_TYPES = T.let(%w(github gitlab sourcehut git).freeze, T::Array[String])
|
|
19
|
+
# Updatable source types: git-backed sources plus NixOS channel tarballs.
|
|
20
|
+
SUPPORTED_SOURCE_TYPES = T.let(%w(github gitlab sourcehut git tarball).freeze, T::Array[String])
|
|
20
21
|
|
|
21
22
|
SUPPORTED_LOCK_VERSION = 7
|
|
22
23
|
|
|
@@ -134,9 +135,28 @@ module Dependabot
|
|
|
134
135
|
source_type = locked.fetch("type", nil)
|
|
135
136
|
return unless SUPPORTED_SOURCE_TYPES.include?(source_type)
|
|
136
137
|
|
|
138
|
+
# Skip inputs pinned to a bare commit SHA: no branch or tag to track.
|
|
139
|
+
return if revision_pinned?(original)
|
|
140
|
+
|
|
137
141
|
rev = locked.fetch("rev", nil)
|
|
138
142
|
return unless rev
|
|
139
143
|
|
|
144
|
+
if source_type == "tarball"
|
|
145
|
+
build_tarball_dependency(input_name, original, rev)
|
|
146
|
+
else
|
|
147
|
+
build_git_dependency(input_name, locked, original, rev)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
sig do
|
|
152
|
+
params(
|
|
153
|
+
input_name: String,
|
|
154
|
+
locked: T::Hash[String, T.untyped],
|
|
155
|
+
original: T::Hash[String, T.untyped],
|
|
156
|
+
rev: String
|
|
157
|
+
).returns(T.nilable(Dependabot::Dependency))
|
|
158
|
+
end
|
|
159
|
+
def build_git_dependency(input_name, locked, original, rev)
|
|
140
160
|
url = build_url(locked)
|
|
141
161
|
return unless url
|
|
142
162
|
|
|
@@ -155,6 +175,38 @@ module Dependabot
|
|
|
155
175
|
)
|
|
156
176
|
end
|
|
157
177
|
|
|
178
|
+
sig { params(original: T::Hash[String, T.untyped]).returns(T::Boolean) }
|
|
179
|
+
def revision_pinned?(original)
|
|
180
|
+
!original.fetch("rev", nil).nil?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Channel tarballs track a channel in the URL (e.g. nixos-26.05), not a git
|
|
184
|
+
# ref. Non-channel tarballs aren't updatable, so they're skipped.
|
|
185
|
+
sig do
|
|
186
|
+
params(
|
|
187
|
+
input_name: String,
|
|
188
|
+
original: T::Hash[String, T.untyped],
|
|
189
|
+
rev: String
|
|
190
|
+
).returns(T.nilable(Dependabot::Dependency))
|
|
191
|
+
end
|
|
192
|
+
def build_tarball_dependency(input_name, original, rev)
|
|
193
|
+
url = original.fetch("url", nil)
|
|
194
|
+
channel = Channel.channel_name_from_url(url)
|
|
195
|
+
return unless channel
|
|
196
|
+
|
|
197
|
+
Dependency.new(
|
|
198
|
+
name: input_name,
|
|
199
|
+
version: rev,
|
|
200
|
+
package_manager: "nix",
|
|
201
|
+
requirements: [{
|
|
202
|
+
requirement: nil,
|
|
203
|
+
file: "flake.lock",
|
|
204
|
+
source: { type: "tarball", url: url, branch: nil, ref: channel },
|
|
205
|
+
groups: []
|
|
206
|
+
}]
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
158
210
|
sig { params(locked: T::Hash[String, T.untyped]).returns(T.nilable(String)) }
|
|
159
211
|
def build_url(locked)
|
|
160
212
|
case locked["type"]
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
5
|
|
|
6
|
+
require "dependabot/nix/channel"
|
|
7
|
+
|
|
6
8
|
module Dependabot
|
|
7
9
|
module Nix
|
|
8
10
|
# Parses flake.nix content to locate input URL declarations and extract
|
|
@@ -62,35 +64,9 @@ module Dependabot
|
|
|
62
64
|
|
|
63
65
|
url_str = match[:url]
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return InputUrl.new(
|
|
69
|
-
full_url: url_str,
|
|
70
|
-
scheme: T.must(url_match[:scheme]),
|
|
71
|
-
owner: T.must(url_match[:owner]),
|
|
72
|
-
repo: T.must(url_match[:repo]),
|
|
73
|
-
ref: url_match[:ref],
|
|
74
|
-
query: url_match[:query],
|
|
75
|
-
match_start: match[:url_start],
|
|
76
|
-
match_end: match[:url_end]
|
|
77
|
-
)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Try indirect/registry shorthand (e.g. nixpkgs/nixos-24.11)
|
|
81
|
-
indirect_match = INDIRECT_URL_PATTERN.match(url_str)
|
|
82
|
-
return unless indirect_match
|
|
83
|
-
|
|
84
|
-
InputUrl.new(
|
|
85
|
-
full_url: url_str,
|
|
86
|
-
scheme: "indirect",
|
|
87
|
-
owner: T.must(indirect_match[:id]),
|
|
88
|
-
repo: "",
|
|
89
|
-
ref: indirect_match[:ref],
|
|
90
|
-
query: nil,
|
|
91
|
-
match_start: match[:url_start],
|
|
92
|
-
match_end: match[:url_end]
|
|
93
|
-
)
|
|
67
|
+
build_shorthand_input(url_str, match) ||
|
|
68
|
+
build_tarball_input(url_str, match) ||
|
|
69
|
+
build_indirect_input(url_str, match)
|
|
94
70
|
end
|
|
95
71
|
|
|
96
72
|
sig { params(new_ref: String).returns(T.nilable(String)) }
|
|
@@ -117,6 +93,60 @@ module Dependabot
|
|
|
117
93
|
sig { returns(String) }
|
|
118
94
|
attr_reader :input_name
|
|
119
95
|
|
|
96
|
+
# Shorthand scheme URLs: github:, gitlab:, sourcehut:
|
|
97
|
+
sig { params(url_str: String, match: T::Hash[Symbol, T.untyped]).returns(T.nilable(InputUrl)) }
|
|
98
|
+
def build_shorthand_input(url_str, match)
|
|
99
|
+
url_match = FLAKE_URL_PATTERN.match(url_str)
|
|
100
|
+
return unless url_match
|
|
101
|
+
|
|
102
|
+
InputUrl.new(
|
|
103
|
+
full_url: url_str,
|
|
104
|
+
scheme: T.must(url_match[:scheme]),
|
|
105
|
+
owner: T.must(url_match[:owner]),
|
|
106
|
+
repo: T.must(url_match[:repo]),
|
|
107
|
+
ref: url_match[:ref],
|
|
108
|
+
query: url_match[:query],
|
|
109
|
+
match_start: match[:url_start],
|
|
110
|
+
match_end: match[:url_end]
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# NixOS channel tarball URL, e.g. channels.nixos.org/nixos-26.05/nixexprs.tar.xz
|
|
115
|
+
sig { params(url_str: String, match: T::Hash[Symbol, T.untyped]).returns(T.nilable(InputUrl)) }
|
|
116
|
+
def build_tarball_input(url_str, match)
|
|
117
|
+
channel = Channel.channel_name_from_url(url_str)
|
|
118
|
+
return unless channel
|
|
119
|
+
|
|
120
|
+
InputUrl.new(
|
|
121
|
+
full_url: url_str,
|
|
122
|
+
scheme: "tarball",
|
|
123
|
+
owner: "",
|
|
124
|
+
repo: "",
|
|
125
|
+
ref: channel,
|
|
126
|
+
query: nil,
|
|
127
|
+
match_start: match[:url_start],
|
|
128
|
+
match_end: match[:url_end]
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Indirect/registry shorthand (e.g. nixpkgs/nixos-24.11)
|
|
133
|
+
sig { params(url_str: String, match: T::Hash[Symbol, T.untyped]).returns(T.nilable(InputUrl)) }
|
|
134
|
+
def build_indirect_input(url_str, match)
|
|
135
|
+
indirect_match = INDIRECT_URL_PATTERN.match(url_str)
|
|
136
|
+
return unless indirect_match
|
|
137
|
+
|
|
138
|
+
InputUrl.new(
|
|
139
|
+
full_url: url_str,
|
|
140
|
+
scheme: "indirect",
|
|
141
|
+
owner: T.must(indirect_match[:id]),
|
|
142
|
+
repo: "",
|
|
143
|
+
ref: indirect_match[:ref],
|
|
144
|
+
query: nil,
|
|
145
|
+
match_start: match[:url_start],
|
|
146
|
+
match_end: match[:url_end]
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
120
150
|
# Returns a hash with :url, :url_start, :url_end if found, or nil.
|
|
121
151
|
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
122
152
|
def find_url_match
|
|
@@ -180,8 +210,12 @@ module Dependabot
|
|
|
180
210
|
|
|
181
211
|
sig { params(input_url: InputUrl, new_ref: String).returns(String) }
|
|
182
212
|
def build_updated_url(input_url, new_ref)
|
|
183
|
-
|
|
213
|
+
case input_url.scheme
|
|
214
|
+
when "indirect"
|
|
184
215
|
"#{input_url.owner}/#{new_ref}"
|
|
216
|
+
when "tarball"
|
|
217
|
+
old_channel = T.must(input_url.ref)
|
|
218
|
+
input_url.full_url.sub("/#{old_channel}/", "/#{new_ref}/")
|
|
185
219
|
else
|
|
186
220
|
base = "#{input_url.scheme}:#{input_url.owner}/#{input_url.repo}/#{new_ref}"
|
|
187
221
|
input_url.query ? "#{base}?#{input_url.query}" : base
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/nix/requirement"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module Nix
|
|
10
|
+
# Tests YY.MM version strings against Dependabot ignore conditions.
|
|
11
|
+
class IgnoreFilter
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { params(ignored_versions: T::Array[String]).void }
|
|
15
|
+
def initialize(ignored_versions)
|
|
16
|
+
@ignored_versions = ignored_versions
|
|
17
|
+
@requirements = T.let(nil, T.nilable(T::Array[Gem::Requirement]))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig { params(version_str: T.nilable(String)).returns(T::Boolean) }
|
|
21
|
+
def ignored?(version_str)
|
|
22
|
+
return false unless version_str
|
|
23
|
+
return false if requirements.empty?
|
|
24
|
+
|
|
25
|
+
gem_version = Gem::Version.new(version_str)
|
|
26
|
+
requirements.any? { |req| req.satisfied_by?(gem_version) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
sig { returns(T::Array[String]) }
|
|
32
|
+
attr_reader :ignored_versions
|
|
33
|
+
|
|
34
|
+
sig { returns(T::Array[Gem::Requirement]) }
|
|
35
|
+
def requirements
|
|
36
|
+
@requirements ||= ignored_versions.flat_map do |req|
|
|
37
|
+
Dependabot::Nix::Requirement.requirements_array(req)
|
|
38
|
+
rescue Gem::Requirement::BadRequirementError
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -5,18 +5,24 @@ require "sorbet-runtime"
|
|
|
5
5
|
|
|
6
6
|
require "dependabot/metadata_finders"
|
|
7
7
|
require "dependabot/metadata_finders/base"
|
|
8
|
+
require "dependabot/nix/channel"
|
|
8
9
|
|
|
9
10
|
module Dependabot
|
|
10
11
|
module Nix
|
|
11
12
|
class MetadataFinder < Dependabot::MetadataFinders::Base
|
|
12
13
|
extend T::Sig
|
|
13
14
|
|
|
15
|
+
# Channel tarballs resolve to nixpkgs revisions, so metadata points there.
|
|
16
|
+
NIXPKGS_SOURCE_URL = "https://github.com/NixOS/nixpkgs"
|
|
17
|
+
|
|
14
18
|
private
|
|
15
19
|
|
|
16
20
|
sig { override.returns(T.nilable(Dependabot::Source)) }
|
|
17
21
|
def look_up_source
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
source = dependency.requirements.first&.fetch(:source, nil)
|
|
23
|
+
url = source && (source[:url] || source["url"])
|
|
24
|
+
|
|
25
|
+
return Source.from_url(NIXPKGS_SOURCE_URL) if Channel.channel_url?(url)
|
|
20
26
|
|
|
21
27
|
Source.from_url(url)
|
|
22
28
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/nix/update_checker"
|
|
7
|
+
require "dependabot/nix/channel"
|
|
8
|
+
require "dependabot/nix/ignore_filter"
|
|
9
|
+
require "dependabot/registry_client"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Nix
|
|
13
|
+
class UpdateChecker
|
|
14
|
+
# Finds the latest NixOS channel and its revision: channels come from the
|
|
15
|
+
# channels.nixos.org S3 listing, revisions from each channel's git-revision marker.
|
|
16
|
+
class ChannelVersionFinder
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
CHANNELS_BASE_URL = "https://channels.nixos.org"
|
|
20
|
+
CHANNEL_KEY_PATTERN = %r{<Key>([^<]+)</Key>}
|
|
21
|
+
SHA_PATTERN = /\A[0-9a-f]{40}\z/
|
|
22
|
+
|
|
23
|
+
sig do
|
|
24
|
+
params(
|
|
25
|
+
current_channel: String,
|
|
26
|
+
credentials: T::Array[Dependabot::Credential],
|
|
27
|
+
ignored_versions: T::Array[String],
|
|
28
|
+
extension: String
|
|
29
|
+
).void
|
|
30
|
+
end
|
|
31
|
+
def initialize(current_channel:, credentials:, ignored_versions: [], extension: Channel::DEFAULT_EXTENSION)
|
|
32
|
+
@current_channel = T.let(Channel.new(current_channel), Channel)
|
|
33
|
+
@credentials = credentials
|
|
34
|
+
@ignored_versions = ignored_versions
|
|
35
|
+
@extension = extension
|
|
36
|
+
@available_channels = T.let(nil, T.nilable(T::Array[String]))
|
|
37
|
+
@ignore_filter = T.let(nil, T.nilable(IgnoreFilter))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Newest same-family channel with its revision, or nil (rolling channel,
|
|
41
|
+
# already latest, or revision unresolvable).
|
|
42
|
+
sig { returns(T.nilable(T::Hash[Symbol, String])) }
|
|
43
|
+
def latest_channel
|
|
44
|
+
return unless current_channel.versioned?
|
|
45
|
+
|
|
46
|
+
candidate = newest_candidate
|
|
47
|
+
return unless candidate
|
|
48
|
+
|
|
49
|
+
rev = resolve_revision(candidate.name)
|
|
50
|
+
return unless rev
|
|
51
|
+
|
|
52
|
+
{ channel: candidate.name, url: Channel.url_for(candidate.name, extension: extension), commit_sha: rev }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Current channel's revision (refresh path).
|
|
56
|
+
sig { returns(T.nilable(String)) }
|
|
57
|
+
def current_channel_revision
|
|
58
|
+
resolve_revision(current_channel.name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
sig { returns(Channel) }
|
|
64
|
+
attr_reader :current_channel
|
|
65
|
+
|
|
66
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
|
67
|
+
attr_reader :credentials
|
|
68
|
+
|
|
69
|
+
sig { returns(T::Array[String]) }
|
|
70
|
+
attr_reader :ignored_versions
|
|
71
|
+
|
|
72
|
+
sig { returns(String) }
|
|
73
|
+
attr_reader :extension
|
|
74
|
+
|
|
75
|
+
sig { returns(T.nilable(Channel)) }
|
|
76
|
+
def newest_candidate
|
|
77
|
+
candidates = available_channels
|
|
78
|
+
.map { |name| Channel.new(name) }
|
|
79
|
+
.select { |channel| channel.same_family?(current_channel) }
|
|
80
|
+
.select { |channel| channel.newer_than?(current_channel) }
|
|
81
|
+
.reject { |channel| ignore_filter.ignored?(channel.version_string) }
|
|
82
|
+
|
|
83
|
+
candidates.max_by { |channel| T.must(channel.version) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { returns(T::Array[String]) }
|
|
87
|
+
def available_channels
|
|
88
|
+
@available_channels ||= fetch_available_channels
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { returns(T::Array[String]) }
|
|
92
|
+
def fetch_available_channels
|
|
93
|
+
prefix = current_channel.prefix
|
|
94
|
+
return [] unless prefix
|
|
95
|
+
|
|
96
|
+
url = "#{CHANNELS_BASE_URL}/?delimiter=/&list-type=2&prefix=#{prefix}"
|
|
97
|
+
response = Dependabot::RegistryClient.get(url: url)
|
|
98
|
+
return [] unless response.status == 200
|
|
99
|
+
|
|
100
|
+
response.body.to_s.scan(CHANNEL_KEY_PATTERN).flatten
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
Dependabot.logger.info("Failed to list NixOS channels: #{e.class}: #{e.message}")
|
|
103
|
+
[]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { params(channel: String).returns(T.nilable(String)) }
|
|
107
|
+
def resolve_revision(channel)
|
|
108
|
+
url = "#{CHANNELS_BASE_URL}/#{channel}/git-revision"
|
|
109
|
+
response = Dependabot::RegistryClient.get(url: url)
|
|
110
|
+
return unless response.status == 200
|
|
111
|
+
|
|
112
|
+
rev = response.body.to_s.strip
|
|
113
|
+
rev if rev.match?(SHA_PATTERN)
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
Dependabot.logger.info("Failed to resolve revision for #{channel}: #{e.class}: #{e.message}")
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { returns(IgnoreFilter) }
|
|
120
|
+
def ignore_filter
|
|
121
|
+
@ignore_filter ||= IgnoreFilter.new(ignored_versions)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
5
|
|
|
6
6
|
require "dependabot/nix/update_checker"
|
|
7
|
-
require "dependabot/nix/
|
|
7
|
+
require "dependabot/nix/versioned_name"
|
|
8
|
+
require "dependabot/nix/ignore_filter"
|
|
8
9
|
require "dependabot/git_metadata_fetcher"
|
|
9
10
|
require "dependabot/git_ref"
|
|
10
11
|
|
|
@@ -16,14 +17,6 @@ module Dependabot
|
|
|
16
17
|
class VersionedBranchFinder
|
|
17
18
|
extend T::Sig
|
|
18
19
|
|
|
19
|
-
# Matches branch names with a YY.MM version segment and optional suffix.
|
|
20
|
-
# Captures: prefix (including separator), version, and optional suffix.
|
|
21
|
-
# Examples: "nixos-24.11" => prefix="nixos-", version="24.11", suffix=nil
|
|
22
|
-
# "nixos-24.11-small" => prefix="nixos-", version="24.11", suffix="-small"
|
|
23
|
-
# "release-24.11-aarch64" => prefix="release-", version="24.11", suffix="-aarch64"
|
|
24
|
-
VERSIONED_BRANCH_PATTERN = /\A(.+[.\-_])(\d{2}\.\d{2})(-[a-zA-Z0-9]+)?\z/
|
|
25
|
-
private_constant :VERSIONED_BRANCH_PATTERN
|
|
26
|
-
|
|
27
20
|
sig do
|
|
28
21
|
params(
|
|
29
22
|
current_ref: String,
|
|
@@ -42,22 +35,16 @@ module Dependabot
|
|
|
42
35
|
# Returns true if the current ref looks like a versioned branch.
|
|
43
36
|
sig { returns(T::Boolean) }
|
|
44
37
|
def versioned_branch?
|
|
45
|
-
|
|
38
|
+
current_name.versioned?
|
|
46
39
|
end
|
|
47
40
|
|
|
48
41
|
# Returns the latest versioned branch info or nil if no newer branch exists.
|
|
49
42
|
# Returns { branch: "nixos-25.05", commit_sha: "abc123" } or nil.
|
|
50
43
|
sig { returns(T.nilable(T::Hash[Symbol, String])) }
|
|
51
44
|
def latest_versioned_branch
|
|
52
|
-
|
|
53
|
-
return unless match
|
|
54
|
-
|
|
55
|
-
prefix = match[1]
|
|
56
|
-
current_version = parse_version(T.must(match[2]))
|
|
57
|
-
return unless current_version
|
|
45
|
+
return unless current_name.versioned?
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
find_latest_branch(T.must(prefix), current_version, suffix)
|
|
47
|
+
find_latest_branch
|
|
61
48
|
end
|
|
62
49
|
|
|
63
50
|
private
|
|
@@ -74,25 +61,14 @@ module Dependabot
|
|
|
74
61
|
sig { returns(T::Array[String]) }
|
|
75
62
|
attr_reader :ignored_versions
|
|
76
63
|
|
|
77
|
-
sig { returns(
|
|
78
|
-
def
|
|
79
|
-
@
|
|
80
|
-
VERSIONED_BRANCH_PATTERN.match(current_ref),
|
|
81
|
-
T.nilable(MatchData)
|
|
82
|
-
)
|
|
64
|
+
sig { returns(VersionedName) }
|
|
65
|
+
def current_name
|
|
66
|
+
@current_name ||= T.let(VersionedName.new(current_ref), T.nilable(VersionedName))
|
|
83
67
|
end
|
|
84
68
|
|
|
85
|
-
sig
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
current_version: T::Array[Integer],
|
|
89
|
-
suffix: T.nilable(String)
|
|
90
|
-
).returns(T.nilable(T::Hash[Symbol, String]))
|
|
91
|
-
end
|
|
92
|
-
def find_latest_branch(prefix, current_version, suffix)
|
|
93
|
-
candidates = remote_branches.filter_map do |ref|
|
|
94
|
-
build_candidate(ref, prefix, current_version, suffix)
|
|
95
|
-
end
|
|
69
|
+
sig { returns(T.nilable(T::Hash[Symbol, String])) }
|
|
70
|
+
def find_latest_branch
|
|
71
|
+
candidates = remote_branches.filter_map { |ref| build_candidate(ref) }
|
|
96
72
|
|
|
97
73
|
latest = candidates.max_by { |c| c[:version] }
|
|
98
74
|
return unless latest
|
|
@@ -100,62 +76,19 @@ module Dependabot
|
|
|
100
76
|
{ branch: latest[:branch].to_s, commit_sha: latest[:commit_sha].to_s }
|
|
101
77
|
end
|
|
102
78
|
|
|
103
|
-
sig
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
|
110
|
-
end
|
|
111
|
-
def build_candidate(ref, prefix, current_version, suffix)
|
|
112
|
-
branch_match = VERSIONED_BRANCH_PATTERN.match(ref.name)
|
|
113
|
-
return unless branch_match
|
|
114
|
-
return unless branch_match[1] == prefix
|
|
115
|
-
return unless branch_match[3] == suffix
|
|
116
|
-
|
|
117
|
-
version_str = T.must(branch_match[2])
|
|
118
|
-
version = parse_version(version_str)
|
|
119
|
-
return unless version
|
|
120
|
-
return unless (version <=> current_version) == 1
|
|
121
|
-
return if version_ignored?(version_str)
|
|
122
|
-
|
|
123
|
-
{ branch: ref.name, commit_sha: ref.commit_sha, version: version }
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
sig { params(version_str: String).returns(T::Boolean) }
|
|
127
|
-
def version_ignored?(version_str)
|
|
128
|
-
return false if ignore_requirements.empty?
|
|
79
|
+
sig { params(ref: Dependabot::GitRef).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
80
|
+
def build_candidate(ref)
|
|
81
|
+
candidate = VersionedName.new(ref.name)
|
|
82
|
+
return unless candidate.same_family?(current_name)
|
|
83
|
+
return unless candidate.newer_than?(current_name)
|
|
84
|
+
return if ignore_filter.ignored?(candidate.version_string)
|
|
129
85
|
|
|
130
|
-
|
|
131
|
-
ignore_requirements.any? { |req| req.satisfied_by?(gem_version) }
|
|
86
|
+
{ branch: ref.name, commit_sha: ref.commit_sha, version: candidate.version }
|
|
132
87
|
end
|
|
133
88
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
parts = version_str.split(".")
|
|
138
|
-
return unless parts.length == 2
|
|
139
|
-
|
|
140
|
-
year = Integer(T.must(parts[0]), 10)
|
|
141
|
-
month = Integer(T.must(parts[1]), 10)
|
|
142
|
-
return unless month.between?(1, 12)
|
|
143
|
-
|
|
144
|
-
[year, month]
|
|
145
|
-
rescue ArgumentError
|
|
146
|
-
nil
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
sig { returns(T::Array[Gem::Requirement]) }
|
|
150
|
-
def ignore_requirements
|
|
151
|
-
@ignore_requirements ||= T.let(
|
|
152
|
-
ignored_versions.flat_map do |req|
|
|
153
|
-
Dependabot::Nix::Requirement.requirements_array(req)
|
|
154
|
-
rescue Gem::Requirement::BadRequirementError
|
|
155
|
-
[]
|
|
156
|
-
end,
|
|
157
|
-
T.nilable(T::Array[Gem::Requirement])
|
|
158
|
-
)
|
|
89
|
+
sig { returns(IgnoreFilter) }
|
|
90
|
+
def ignore_filter
|
|
91
|
+
@ignore_filter ||= T.let(IgnoreFilter.new(ignored_versions), T.nilable(IgnoreFilter))
|
|
159
92
|
end
|
|
160
93
|
|
|
161
94
|
sig { returns(T::Array[Dependabot::GitRef]) }
|
|
@@ -7,6 +7,7 @@ require "dependabot/update_checkers"
|
|
|
7
7
|
require "dependabot/update_checkers/base"
|
|
8
8
|
require "dependabot/nix/version"
|
|
9
9
|
require "dependabot/nix/requirement"
|
|
10
|
+
require "dependabot/nix/channel"
|
|
10
11
|
require "dependabot/git_commit_checker"
|
|
11
12
|
|
|
12
13
|
module Dependabot
|
|
@@ -16,6 +17,7 @@ module Dependabot
|
|
|
16
17
|
|
|
17
18
|
require_relative "update_checker/latest_version_finder"
|
|
18
19
|
require_relative "update_checker/versioned_branch_finder"
|
|
20
|
+
require_relative "update_checker/channel_version_finder"
|
|
19
21
|
|
|
20
22
|
sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) }
|
|
21
23
|
def latest_version
|
|
@@ -38,7 +40,9 @@ module Dependabot
|
|
|
38
40
|
|
|
39
41
|
sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
40
42
|
def updated_requirements
|
|
41
|
-
if
|
|
43
|
+
if tarball_channel_input?
|
|
44
|
+
wrap_requirements(updated_requirements_for_channel)
|
|
45
|
+
elsif ref_pinned_to_version_tag?
|
|
42
46
|
wrap_requirements(updated_requirements_for_tag)
|
|
43
47
|
elsif ref_is_versioned_branch?
|
|
44
48
|
wrap_requirements(updated_requirements_for_versioned_branch)
|
|
@@ -61,7 +65,9 @@ module Dependabot
|
|
|
61
65
|
|
|
62
66
|
sig { returns(T.nilable(String)) }
|
|
63
67
|
def fetch_latest_version
|
|
64
|
-
if
|
|
68
|
+
if tarball_channel_input?
|
|
69
|
+
fetch_latest_version_for_channel
|
|
70
|
+
elsif ref_pinned_to_version_tag?
|
|
65
71
|
fetch_latest_version_for_tag
|
|
66
72
|
elsif ref_is_versioned_branch?
|
|
67
73
|
fetch_latest_version_for_versioned_branch || fetch_latest_version_for_commit
|
|
@@ -70,6 +76,75 @@ module Dependabot
|
|
|
70
76
|
end
|
|
71
77
|
end
|
|
72
78
|
|
|
79
|
+
# --- Tarball channel support ---
|
|
80
|
+
|
|
81
|
+
sig { returns(T::Boolean) }
|
|
82
|
+
def tarball_channel_input?
|
|
83
|
+
source = dependency.source_details(allowed_types: ["tarball"])
|
|
84
|
+
return false unless source
|
|
85
|
+
|
|
86
|
+
Channel.channel_url?(source[:url] || source["url"])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig { returns(T.nilable(String)) }
|
|
90
|
+
def fetch_latest_version_for_channel
|
|
91
|
+
latest_channel&.fetch(:commit_sha) || channel_version_finder.current_channel_revision
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
95
|
+
def updated_requirements_for_channel
|
|
96
|
+
result = latest_channel
|
|
97
|
+
return dependency.requirements unless result
|
|
98
|
+
|
|
99
|
+
dependency.requirements.map do |req|
|
|
100
|
+
source = req[:source]
|
|
101
|
+
next req unless source
|
|
102
|
+
|
|
103
|
+
req.merge(source: source.merge(ref: result[:channel], url: result[:url]))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { returns(T.nilable(T::Hash[Symbol, String])) }
|
|
108
|
+
def latest_channel
|
|
109
|
+
@latest_channel ||= T.let(
|
|
110
|
+
channel_version_finder.latest_channel,
|
|
111
|
+
T.nilable(T::Hash[Symbol, String])
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig { returns(ChannelVersionFinder) }
|
|
116
|
+
def channel_version_finder
|
|
117
|
+
@channel_version_finder ||= T.let(
|
|
118
|
+
ChannelVersionFinder.new(
|
|
119
|
+
current_channel: T.must(tarball_channel_name),
|
|
120
|
+
credentials: credentials,
|
|
121
|
+
ignored_versions: ignored_versions,
|
|
122
|
+
extension: tarball_channel_extension
|
|
123
|
+
),
|
|
124
|
+
T.nilable(ChannelVersionFinder)
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { returns(T.nilable(String)) }
|
|
129
|
+
def tarball_channel_name
|
|
130
|
+
source = dependency.source_details(allowed_types: ["tarball"])
|
|
131
|
+
return unless source
|
|
132
|
+
|
|
133
|
+
ref = source[:ref] || source["ref"]
|
|
134
|
+
return ref if ref
|
|
135
|
+
|
|
136
|
+
# Fall back to the URL's channel when the source omits a ref.
|
|
137
|
+
Channel.channel_name_from_url(source[:url] || source["url"])
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Preserve the flake's existing tarball suffix (xz, gz, bz2) on a bump.
|
|
141
|
+
sig { returns(String) }
|
|
142
|
+
def tarball_channel_extension
|
|
143
|
+
source = dependency.source_details(allowed_types: ["tarball"])
|
|
144
|
+
url = source && (source[:url] || source["url"])
|
|
145
|
+
Channel.extension_from_url(url) || Channel::DEFAULT_EXTENSION
|
|
146
|
+
end
|
|
147
|
+
|
|
73
148
|
# --- Tag-pinned ref support ---
|
|
74
149
|
|
|
75
150
|
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module Dependabot
|
|
7
|
+
module Nix
|
|
8
|
+
# Parses a NixOS-style versioned name (e.g. "nixos-26.05", "nixpkgs-24.11-darwin")
|
|
9
|
+
# into prefix, YY.MM version, and suffix, and compares names within a family.
|
|
10
|
+
class VersionedName
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
# prefix + YY.MM version + optional suffix, e.g. "nixpkgs-26.05-darwin".
|
|
14
|
+
VERSIONED_NAME_PATTERN =
|
|
15
|
+
/\A(?<prefix>.+[.\-_])(?<version>\d{2}\.\d{2})(?<suffix>-[a-zA-Z0-9]+)?\z/
|
|
16
|
+
|
|
17
|
+
sig { params(name: String).void }
|
|
18
|
+
def initialize(name)
|
|
19
|
+
@name = name
|
|
20
|
+
@match = T.let(VERSIONED_NAME_PATTERN.match(name), T.nilable(MatchData))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { returns(String) }
|
|
24
|
+
attr_reader :name
|
|
25
|
+
|
|
26
|
+
sig { returns(T::Boolean) }
|
|
27
|
+
def versioned?
|
|
28
|
+
!@match.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { returns(T.nilable(String)) }
|
|
32
|
+
def prefix
|
|
33
|
+
@match&.[](:prefix)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { returns(T.nilable(String)) }
|
|
37
|
+
def suffix
|
|
38
|
+
@match&.[](:suffix)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { returns(T.nilable(String)) }
|
|
42
|
+
def version_string
|
|
43
|
+
@match&.[](:version)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# YY.MM as a comparable [year, month], or nil.
|
|
47
|
+
sig { returns(T.nilable(T::Array[Integer])) }
|
|
48
|
+
def version
|
|
49
|
+
version_str = version_string
|
|
50
|
+
return unless version_str
|
|
51
|
+
|
|
52
|
+
parse_version(version_str)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Same prefix and suffix, so bumps stay within e.g. nixos-* (not nixos-*-small).
|
|
56
|
+
sig { params(other: VersionedName).returns(T::Boolean) }
|
|
57
|
+
def same_family?(other)
|
|
58
|
+
versioned? && other.versioned? &&
|
|
59
|
+
prefix == other.prefix && suffix == other.suffix
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig { params(other: VersionedName).returns(T::Boolean) }
|
|
63
|
+
def newer_than?(other)
|
|
64
|
+
this_version = version
|
|
65
|
+
other_version = other.version
|
|
66
|
+
return false unless this_version && other_version
|
|
67
|
+
|
|
68
|
+
(this_version <=> other_version) == 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
sig { params(version_str: String).returns(T.nilable(T::Array[Integer])) }
|
|
74
|
+
def parse_version(version_str)
|
|
75
|
+
parts = version_str.split(".")
|
|
76
|
+
return unless parts.length == 2
|
|
77
|
+
|
|
78
|
+
year = Integer(T.must(parts[0]), 10)
|
|
79
|
+
month = Integer(T.must(parts[1]), 10)
|
|
80
|
+
return unless month.between?(1, 12)
|
|
81
|
+
|
|
82
|
+
[year, month]
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-nix
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.384.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.384.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.384.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -242,24 +242,28 @@ extensions: []
|
|
|
242
242
|
extra_rdoc_files: []
|
|
243
243
|
files:
|
|
244
244
|
- lib/dependabot/nix.rb
|
|
245
|
+
- lib/dependabot/nix/channel.rb
|
|
245
246
|
- lib/dependabot/nix/file_fetcher.rb
|
|
246
247
|
- lib/dependabot/nix/file_parser.rb
|
|
247
248
|
- lib/dependabot/nix/file_updater.rb
|
|
248
249
|
- lib/dependabot/nix/flake_nix_parser.rb
|
|
250
|
+
- lib/dependabot/nix/ignore_filter.rb
|
|
249
251
|
- lib/dependabot/nix/metadata_finder.rb
|
|
250
252
|
- lib/dependabot/nix/package/package_details_fetcher.rb
|
|
251
253
|
- lib/dependabot/nix/package_manager.rb
|
|
252
254
|
- lib/dependabot/nix/requirement.rb
|
|
253
255
|
- lib/dependabot/nix/update_checker.rb
|
|
256
|
+
- lib/dependabot/nix/update_checker/channel_version_finder.rb
|
|
254
257
|
- lib/dependabot/nix/update_checker/latest_version_finder.rb
|
|
255
258
|
- lib/dependabot/nix/update_checker/versioned_branch_finder.rb
|
|
256
259
|
- lib/dependabot/nix/version.rb
|
|
260
|
+
- lib/dependabot/nix/versioned_name.rb
|
|
257
261
|
homepage: https://github.com/dependabot/dependabot-core
|
|
258
262
|
licenses:
|
|
259
263
|
- MIT
|
|
260
264
|
metadata:
|
|
261
265
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
262
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
266
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.384.0
|
|
263
267
|
rdoc_options: []
|
|
264
268
|
require_paths:
|
|
265
269
|
- lib
|
|
@@ -274,7 +278,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
274
278
|
- !ruby/object:Gem::Version
|
|
275
279
|
version: 3.3.0
|
|
276
280
|
requirements: []
|
|
277
|
-
rubygems_version:
|
|
281
|
+
rubygems_version: 4.0.14
|
|
278
282
|
specification_version: 4
|
|
279
283
|
summary: Provides Dependabot support for Nix
|
|
280
284
|
test_files: []
|