dependabot-npm_and_yarn 0.391.0 → 0.392.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/npm_and_yarn/file_updater/npm_lockfile_updater.rb +70 -8
- data/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb +242 -18
- data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +74 -19
- data/lib/dependabot/npm_and_yarn/file_updater.rb +34 -3
- data/lib/dependabot/npm_and_yarn/helpers.rb +151 -9
- data/lib/dependabot/npm_and_yarn/native_helpers.rb +50 -28
- data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +4 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f8ae1a24dcec46ab349429b68c517aba99fa9308f3e020aa53db169c4f1614c
|
|
4
|
+
data.tar.gz: 15b8a2c1f6dc838d1b4638b5771e59237c3995720cc2b9a2543e09d1adf9477a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ace7c85c19a0ee524f22b71afbc4f0fe8c40d93751ccae53517f652fcec920cb73bdc13d08a22958002d6ac70abfa8da2f4ac7454da7df53a28202b0cc9fe6e0
|
|
7
|
+
data.tar.gz: d3c9658cce6589964732bff6236b0dfc40821de0cf523c28598d7f93d34e5defa3e2b1fc020b3a59bceb2d5ef8be5c45fc58a82ef2b3180f8ce34805ffa4bc26
|
|
@@ -30,16 +30,25 @@ module Dependabot
|
|
|
30
30
|
dependencies: T::Array[Dependabot::Dependency],
|
|
31
31
|
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
32
32
|
credentials: T::Array[Credential],
|
|
33
|
-
security_updates_only: T::Boolean
|
|
33
|
+
security_updates_only: T::Boolean,
|
|
34
|
+
release_age_days: T.nilable(Integer)
|
|
34
35
|
)
|
|
35
36
|
.void
|
|
36
37
|
end
|
|
37
|
-
def initialize(
|
|
38
|
+
def initialize(
|
|
39
|
+
lockfile:,
|
|
40
|
+
dependencies:,
|
|
41
|
+
dependency_files:,
|
|
42
|
+
credentials:,
|
|
43
|
+
security_updates_only: false,
|
|
44
|
+
release_age_days: nil
|
|
45
|
+
)
|
|
38
46
|
@lockfile = lockfile
|
|
39
47
|
@dependencies = dependencies
|
|
40
48
|
@dependency_files = dependency_files
|
|
41
49
|
@credentials = credentials
|
|
42
50
|
@security_updates_only = security_updates_only
|
|
51
|
+
@release_age_days = release_age_days
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
sig { returns(Dependabot::DependencyFile) }
|
|
@@ -343,7 +352,7 @@ module Dependabot
|
|
|
343
352
|
|
|
344
353
|
NativeHelpers.run_npm8_subdependency_update_command(
|
|
345
354
|
dependency_names,
|
|
346
|
-
|
|
355
|
+
min_release_age_arg: effective_min_release_age_arg
|
|
347
356
|
)
|
|
348
357
|
|
|
349
358
|
updated_content = File.read(lockfile_basename)
|
|
@@ -354,7 +363,7 @@ module Dependabot
|
|
|
354
363
|
# npm audit fix exits non-zero when vulnerabilities remain, so we
|
|
355
364
|
# rescue and use whatever lockfile changes it managed to make.
|
|
356
365
|
begin
|
|
357
|
-
NativeHelpers.run_npm_audit_fix_command(
|
|
366
|
+
NativeHelpers.run_npm_audit_fix_command(min_release_age_arg: effective_min_release_age_arg)
|
|
358
367
|
sub_dependencies.each { |dep| dep.metadata[:audit_fix_used] = true }
|
|
359
368
|
rescue SharedHelpers::HelperSubprocessFailed
|
|
360
369
|
Dependabot.logger.info("npm audit fix failed or partially fixed — continuing with any changes made")
|
|
@@ -404,9 +413,8 @@ module Dependabot
|
|
|
404
413
|
]
|
|
405
414
|
|
|
406
415
|
command_args << "--save-optional" if has_optional_dependencies
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
command_args << "--min-release-age=0" if security_updates_only?
|
|
416
|
+
min_release_age_arg = effective_min_release_age_arg
|
|
417
|
+
command_args << min_release_age_arg if min_release_age_arg
|
|
410
418
|
|
|
411
419
|
command = command_args.join(" ")
|
|
412
420
|
|
|
@@ -419,13 +427,67 @@ module Dependabot
|
|
|
419
427
|
]
|
|
420
428
|
|
|
421
429
|
fingerprint_args << "--save-optional" if has_optional_dependencies
|
|
422
|
-
|
|
430
|
+
# The cooldown day value varies per job, so keep it out of the fingerprint.
|
|
431
|
+
fingerprint_args << fingerprint_min_release_age_arg(min_release_age_arg) if min_release_age_arg
|
|
423
432
|
|
|
424
433
|
fingerprint = fingerprint_args.join(" ")
|
|
425
434
|
|
|
426
435
|
Helpers.run_npm_command(command, fingerprint: fingerprint)
|
|
427
436
|
end
|
|
428
437
|
|
|
438
|
+
# Returns the `--min-release-age` argument to pass to npm (`install`,
|
|
439
|
+
# `update`, or `audit fix`), or nil when none applies. Security updates pass
|
|
440
|
+
# `=0` so a release-age gate never blocks a fix (this intentionally
|
|
441
|
+
# overrides any `.npmrc` gate). Regular updates pass the dependabot.yml
|
|
442
|
+
# cooldown floor (in days) so npm holds transitive dependencies back to
|
|
443
|
+
# versions at least that old. Requires npm >= 11.10 (the updater image
|
|
444
|
+
# ships a supporting version).
|
|
445
|
+
#
|
|
446
|
+
# When the repo also sets an explicit `min-release-age` in `.npmrc`, the
|
|
447
|
+
# longest release-age wins: the cooldown floor is only injected on the CLI
|
|
448
|
+
# when it exceeds the user's configured value, otherwise the user's (equal
|
|
449
|
+
# or longer) gate is left untouched so neither policy is silently weakened.
|
|
450
|
+
sig { returns(T.nilable(String)) }
|
|
451
|
+
def effective_min_release_age_arg
|
|
452
|
+
return nil unless npm_supports_min_release_age?
|
|
453
|
+
|
|
454
|
+
return "--min-release-age=0" if security_updates_only?
|
|
455
|
+
|
|
456
|
+
effective = Helpers.higher_release_age_gate(@release_age_days, npmrc_min_release_age)
|
|
457
|
+
return nil unless effective
|
|
458
|
+
|
|
459
|
+
"--min-release-age=#{effective}"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Whether the npm that will run supports `--min-release-age` (npm 11.10+).
|
|
463
|
+
# npm runs through Corepack, so a repo pinned to an older npm via
|
|
464
|
+
# `packageManager` would reject the flag; gate it out for those. Memoized so
|
|
465
|
+
# the version subprocess runs at most once per update.
|
|
466
|
+
sig { returns(T::Boolean) }
|
|
467
|
+
def npm_supports_min_release_age?
|
|
468
|
+
@npm_supports_min_release_age = T.let(@npm_supports_min_release_age, T.nilable(T::Boolean))
|
|
469
|
+
return @npm_supports_min_release_age unless @npm_supports_min_release_age.nil?
|
|
470
|
+
|
|
471
|
+
@npm_supports_min_release_age = Helpers.npm_supports_min_release_age?
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
sig { params(arg: String).returns(String) }
|
|
475
|
+
def fingerprint_min_release_age_arg(arg)
|
|
476
|
+
arg == "--min-release-age=0" ? arg : "--min-release-age=<days>"
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# The `min-release-age` (in days) configured across the repo's `.npmrc`
|
|
480
|
+
# files, or nil when unset. A value we cannot parse as a bare integer is
|
|
481
|
+
# reported as Float::INFINITY so an explicit-but-non-numeric user gate is
|
|
482
|
+
# never overridden by the cooldown floor.
|
|
483
|
+
sig { returns(T.nilable(T.any(Integer, Float))) }
|
|
484
|
+
def npmrc_min_release_age
|
|
485
|
+
Helpers.max_configured_release_age(
|
|
486
|
+
dependency_files,
|
|
487
|
+
[Helpers::ReleaseAgeGateSetting.new(filename: ".npmrc", key: "min-release-age", separator: "=")]
|
|
488
|
+
)
|
|
489
|
+
end
|
|
490
|
+
|
|
429
491
|
sig { params(dependency: Dependabot::Dependency).returns(String) }
|
|
430
492
|
def npm_install_args(dependency)
|
|
431
493
|
git_requirement = dependency.requirements.find { |req| req.source_string("type") == "git" }
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "dependabot/npm_and_yarn/helpers"
|
|
5
5
|
require "dependabot/npm_and_yarn/package/registry_finder"
|
|
6
6
|
require "dependabot/npm_and_yarn/registry_parser"
|
|
7
|
+
require "dependabot/npm_and_yarn/version"
|
|
7
8
|
require "dependabot/shared_helpers"
|
|
8
9
|
|
|
9
10
|
module Dependabot
|
|
@@ -22,7 +23,8 @@ module Dependabot
|
|
|
22
23
|
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
23
24
|
repo_contents_path: T.nilable(String),
|
|
24
25
|
credentials: T::Array[Dependabot::Credential],
|
|
25
|
-
security_updates_only: T::Boolean
|
|
26
|
+
security_updates_only: T::Boolean,
|
|
27
|
+
release_age_days: T.nilable(Integer)
|
|
26
28
|
).void
|
|
27
29
|
end
|
|
28
30
|
def initialize(
|
|
@@ -30,13 +32,15 @@ module Dependabot
|
|
|
30
32
|
dependency_files:,
|
|
31
33
|
repo_contents_path:,
|
|
32
34
|
credentials:,
|
|
33
|
-
security_updates_only: false
|
|
35
|
+
security_updates_only: false,
|
|
36
|
+
release_age_days: nil
|
|
34
37
|
)
|
|
35
38
|
@dependencies = dependencies
|
|
36
39
|
@dependency_files = dependency_files
|
|
37
40
|
@repo_contents_path = repo_contents_path
|
|
38
41
|
@credentials = credentials
|
|
39
42
|
@security_updates_only = security_updates_only
|
|
43
|
+
@release_age_days = release_age_days
|
|
40
44
|
@error_handler = T.let(
|
|
41
45
|
PnpmErrorHandler.new(
|
|
42
46
|
dependencies: dependencies,
|
|
@@ -92,6 +96,18 @@ module Dependabot
|
|
|
92
96
|
|
|
93
97
|
IRRESOLVABLE_PACKAGE = "ERR_PNPM_NO_MATCHING_VERSION"
|
|
94
98
|
INVALID_REQUIREMENT = "ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER"
|
|
99
|
+
|
|
100
|
+
# pnpm introduced the `minimumReleaseAge` release-age gate in 10.16 and the
|
|
101
|
+
# `minimumReleaseAgeStrict` toggle in 11.0; older versions ignore them.
|
|
102
|
+
PNPM_MINIMUM_RELEASE_AGE_VERSION = "10.16"
|
|
103
|
+
PNPM_MINIMUM_RELEASE_AGE_STRICT_VERSION = "11.0"
|
|
104
|
+
# pnpm 10.x ignores minimumReleaseAge when shared-workspace-lockfile is
|
|
105
|
+
# disabled (pnpm/pnpm#10008); the fix ships in pnpm 11.
|
|
106
|
+
PNPM_WORKSPACE_RELEASE_AGE_FIX_VERSION = "11.0"
|
|
107
|
+
# pnpm 11 stopped reading non-registry settings (e.g. minimum-release-age)
|
|
108
|
+
# from .npmrc, so a .npmrc release-age gate is only effective on pnpm 10.x.
|
|
109
|
+
PNPM_NPMRC_RELEASE_AGE_DROPPED_VERSION = "11.0"
|
|
110
|
+
|
|
95
111
|
UNREACHABLE_GIT = %r{Command failed with exit code 128: git ls-remote (?<url>.*github\.com/[^/]+/[^ ]+)}
|
|
96
112
|
UNREACHABLE_GIT_V8 = %r{ERR_PNPM_FETCH_404[ [^:print]]+GET (?<url>https://codeload\.github\.com/[^/]+/[^/]+)/}
|
|
97
113
|
FORBIDDEN_PACKAGE = /ERR_PNPM_FETCH_403[ [^:print]]+GET (?<dependency_url>.*): Forbidden - 403/
|
|
@@ -207,35 +223,242 @@ module Dependabot
|
|
|
207
223
|
|
|
208
224
|
cmd = "update #{dependency_updates} --lockfile-only --no-save -r"
|
|
209
225
|
fingerprint = "update <dependency_updates> --lockfile-only --no-save -r"
|
|
210
|
-
|
|
211
|
-
# Override any minimumReleaseAge set in pnpm-workspace.yaml: security fixes must not be
|
|
212
|
-
# blocked by a release-age gate the user configured for regular updates.
|
|
213
|
-
cmd += " --config.minimumReleaseAge=0 --config.minimumReleaseAgeStrict=false"
|
|
214
|
-
fingerprint += " --config.minimumReleaseAge=0 --config.minimumReleaseAgeStrict=false"
|
|
215
|
-
end
|
|
216
|
-
Helpers.run_pnpm_command(cmd, fingerprint: fingerprint)
|
|
226
|
+
run_pnpm_command_with_release_age_gate(cmd, fingerprint)
|
|
217
227
|
end
|
|
218
228
|
|
|
219
229
|
sig { returns(T.nilable(String)) }
|
|
220
230
|
def run_pnpm_install
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
231
|
+
# `install --lockfile-only` has no dynamic content, so it needs no
|
|
232
|
+
# fingerprint of its own. Passing nil lets the release-age gate handle
|
|
233
|
+
# fingerprinting only when it appends the (dynamic) minimumReleaseAge.
|
|
234
|
+
run_pnpm_command_with_release_age_gate("install --lockfile-only")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# pnpm's `minimumReleaseAge` needs the registry `time` field, which is
|
|
238
|
+
# absent from the abbreviated metadata pnpm uses for some packages during
|
|
239
|
+
# `pnpm update`, raising ERR_PNPM_MISSING_TIME. When that happens for a
|
|
240
|
+
# cooldown-derived gate we retry without it so the update still succeeds,
|
|
241
|
+
# rather than failing the whole job. Security bypass (`=0`) never triggers
|
|
242
|
+
# this because it disables the age lookup entirely.
|
|
243
|
+
sig { params(cmd: String, fingerprint: T.nilable(String)).returns(T.nilable(String)) }
|
|
244
|
+
def run_pnpm_command_with_release_age_gate(cmd, fingerprint = nil)
|
|
245
|
+
gate = release_age_gate_config
|
|
246
|
+
return execute_pnpm_command(cmd, fingerprint) unless gate
|
|
247
|
+
|
|
248
|
+
gate_fingerprint = security_updates_only? ? gate : fingerprint_minimum_release_age_config
|
|
249
|
+
gated_fingerprint = "#{fingerprint || cmd} #{gate_fingerprint}"
|
|
250
|
+
begin
|
|
251
|
+
execute_pnpm_command("#{cmd} #{gate}", gated_fingerprint)
|
|
252
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
253
|
+
raise if security_updates_only? || !e.message.include?("ERR_PNPM_MISSING_TIME")
|
|
254
|
+
|
|
255
|
+
Dependabot.logger.warn(
|
|
256
|
+
"pnpm could not apply the cooldown release-age gate because the registry metadata " \
|
|
257
|
+
"is missing the \"time\" field (ERR_PNPM_MISSING_TIME); retrying without the " \
|
|
258
|
+
"transitive cooldown gate so the update is not blocked."
|
|
259
|
+
)
|
|
260
|
+
execute_pnpm_command(cmd, fingerprint)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
sig { params(cmd: String, fingerprint: T.nilable(String)).returns(T.nilable(String)) }
|
|
265
|
+
def execute_pnpm_command(cmd, fingerprint)
|
|
266
|
+
if fingerprint
|
|
267
|
+
Helpers.run_pnpm_command(cmd, fingerprint: fingerprint)
|
|
268
|
+
else
|
|
269
|
+
Helpers.run_pnpm_command(cmd)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Returns the pnpm `--config.minimumReleaseAge` arguments to apply, or nil.
|
|
274
|
+
# Security updates disable the gate (`=0`); regular updates apply the
|
|
275
|
+
# dependabot.yml cooldown floor (in minutes). `minimumReleaseAge` was added
|
|
276
|
+
# in pnpm 10.16, so older pnpm silently ignores it — rather than give a
|
|
277
|
+
# false guarantee we skip the gate (and warn) when the running pnpm is too
|
|
278
|
+
# old to enforce it.
|
|
279
|
+
sig { returns(T.nilable(String)) }
|
|
280
|
+
def release_age_gate_config
|
|
281
|
+
if !security_updates_only? && @release_age_days&.positive? && !pnpm_supports_minimum_release_age?
|
|
282
|
+
Dependabot.logger.warn(
|
|
283
|
+
"pnpm #{pnpm_version || '(unknown version)'} does not support minimumReleaseAge " \
|
|
284
|
+
"(added in pnpm 10.16); the release-age cooldown cannot be enforced for transitive " \
|
|
285
|
+
"dependencies on this pnpm version."
|
|
286
|
+
)
|
|
287
|
+
return nil
|
|
226
288
|
end
|
|
227
|
-
|
|
289
|
+
|
|
290
|
+
minutes = effective_release_age_minutes
|
|
291
|
+
return nil if minutes.nil?
|
|
292
|
+
|
|
293
|
+
# Security updates pass minimumReleaseAge=0 unconditionally: older pnpm
|
|
294
|
+
# ignores it, and a transient version-probe failure must not leave a native
|
|
295
|
+
# gate active and block the fix.
|
|
296
|
+
return minimum_release_age_gate_args(minutes) if security_updates_only?
|
|
297
|
+
|
|
298
|
+
minimum_release_age_gate_args(minutes)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# The pnpm `minimumReleaseAge` value (in minutes) to enforce for this
|
|
302
|
+
# update, independent of the pnpm version: 0 to bypass the gate for
|
|
303
|
+
# security fixes (which must not be blocked by a release-age gate the user
|
|
304
|
+
# configured for regular updates), the dependabot.yml cooldown floor for
|
|
305
|
+
# regular updates, or nil when no gate applies.
|
|
306
|
+
#
|
|
307
|
+
# When the repo also sets an explicit `minimumReleaseAge` in
|
|
308
|
+
# pnpm-workspace.yaml (or `.npmrc`), the longest release-age wins: the
|
|
309
|
+
# cooldown floor is only injected when it exceeds the user's configured
|
|
310
|
+
# value, otherwise the user's (equal or longer) gate is left untouched so
|
|
311
|
+
# neither policy is silently weakened (dependabot/dependabot-core#13165).
|
|
312
|
+
sig { returns(T.nilable(Integer)) }
|
|
313
|
+
def effective_release_age_minutes
|
|
314
|
+
return 0 if security_updates_only?
|
|
315
|
+
|
|
316
|
+
cooldown_minutes = @release_age_days && (@release_age_days * Helpers::MINUTES_PER_DAY)
|
|
317
|
+
Helpers.higher_release_age_gate(cooldown_minutes, pnpm_configured_minimum_release_age)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Builds the pnpm `--config.minimumReleaseAge` args for `minutes`, adding
|
|
321
|
+
# the strict toggle only when appropriate (see `disable_strict_release_age?`).
|
|
322
|
+
sig { params(minutes: Integer).returns(String) }
|
|
323
|
+
def minimum_release_age_gate_args(minutes)
|
|
324
|
+
args = "--config.minimumReleaseAge=#{minutes}"
|
|
325
|
+
args += " --config.minimumReleaseAgeStrict=false" if disable_strict_release_age?
|
|
326
|
+
args
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# pnpm defaults `minimumReleaseAgeStrict` to *on* when `minimumReleaseAge`
|
|
330
|
+
# is set via the CLI, which fails resolution when no version satisfies the
|
|
331
|
+
# window and is incompatible with the `--no-save` update command. We
|
|
332
|
+
# disable strict for Dependabot's CLI override on pnpm >= 11.0, where the
|
|
333
|
+
# toggle exists. Equal-or-longer native gates remain untouched because no
|
|
334
|
+
# CLI override is added for them.
|
|
335
|
+
sig { returns(T::Boolean) }
|
|
336
|
+
def disable_strict_release_age?
|
|
337
|
+
pnpm_supports_minimum_release_age_strict?
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
sig { returns(String) }
|
|
341
|
+
def fingerprint_minimum_release_age_config
|
|
342
|
+
args = "--config.minimumReleaseAge=<minutes>"
|
|
343
|
+
args += " --config.minimumReleaseAgeStrict=false" if disable_strict_release_age?
|
|
344
|
+
args
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# The concrete pnpm version that will run, memoized (including a nil result)
|
|
348
|
+
# so the version subprocess runs at most once per update.
|
|
349
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
350
|
+
def pnpm_version
|
|
351
|
+
return @pnpm_version if defined?(@pnpm_version)
|
|
352
|
+
|
|
353
|
+
@pnpm_version = T.let(Helpers.pnpm_version, T.nilable(Dependabot::Version))
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
sig { returns(T::Boolean) }
|
|
357
|
+
def pnpm_supports_minimum_release_age?
|
|
358
|
+
version = pnpm_version
|
|
359
|
+
return false if version.nil? || version < Version.new(PNPM_MINIMUM_RELEASE_AGE_VERSION)
|
|
360
|
+
|
|
361
|
+
if pnpm_shared_workspace_lockfile_disabled? &&
|
|
362
|
+
version < Version.new(PNPM_WORKSPACE_RELEASE_AGE_FIX_VERSION)
|
|
363
|
+
Dependabot.logger.warn(
|
|
364
|
+
"pnpm #{version} ignores minimumReleaseAge when shared-workspace-lockfile is disabled " \
|
|
365
|
+
"(pnpm/pnpm#10008); the release-age cooldown cannot be enforced. Upgrade to pnpm 11+."
|
|
366
|
+
)
|
|
367
|
+
return false
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
true
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
sig { returns(T::Boolean) }
|
|
374
|
+
def pnpm_supports_minimum_release_age_strict?
|
|
375
|
+
version = pnpm_version
|
|
376
|
+
!version.nil? && version >= Version.new(PNPM_MINIMUM_RELEASE_AGE_STRICT_VERSION)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# pnpm 10.x ignores `minimumReleaseAge` when `shared-workspace-lockfile` is
|
|
380
|
+
# disabled (pnpm/pnpm#10008), so the cooldown cannot be enforced there.
|
|
381
|
+
sig { returns(T::Boolean) }
|
|
382
|
+
def pnpm_shared_workspace_lockfile_disabled?
|
|
383
|
+
dependency_files.any? do |file|
|
|
384
|
+
case File.basename(file.name)
|
|
385
|
+
when "pnpm-workspace.yaml"
|
|
386
|
+
yaml_boolean_setting(file.content.to_s, "shared-workspace-lockfile", ":") == false
|
|
387
|
+
when ".npmrc"
|
|
388
|
+
yaml_boolean_setting(file.content.to_s, "shared-workspace-lockfile", "=") == false
|
|
389
|
+
else
|
|
390
|
+
false
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Reads a YAML/INI boolean `key`, returning true/false, or nil when absent or
|
|
396
|
+
# non-boolean. Handles optionally quoted keys/values (`"key": True`), boolean
|
|
397
|
+
# casing, and trailing comments so a valid native setting is never misread.
|
|
398
|
+
# The last occurrence wins, matching how pnpm/INI resolve a repeated key.
|
|
399
|
+
sig { params(content: String, key: String, separator: String).returns(T.nilable(T::Boolean)) }
|
|
400
|
+
def yaml_boolean_setting(content, key, separator)
|
|
401
|
+
quoted_key = /["']?#{Regexp.escape(key)}["']?/
|
|
402
|
+
presence = /^\s*#{quoted_key}\s*#{Regexp.escape(separator)}/
|
|
403
|
+
last_line = content.lines.reverse_each.find { |line| line.match?(presence) }
|
|
404
|
+
return unless last_line
|
|
405
|
+
|
|
406
|
+
# npmrc/INI treats both `#` and `;` as comment delimiters; YAML uses `#`.
|
|
407
|
+
comment_chars = separator == "=" ? "#;" : "#"
|
|
408
|
+
match = last_line.match(
|
|
409
|
+
/^\s*#{quoted_key}\s*#{Regexp.escape(separator)}\s*["']?(\w+)["']?\s*(?:[#{comment_chars}].*)?$/
|
|
410
|
+
)
|
|
411
|
+
return unless match
|
|
412
|
+
|
|
413
|
+
case T.must(match[1]).downcase
|
|
414
|
+
when "true" then true
|
|
415
|
+
when "false" then false
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# The largest `minimumReleaseAge` (in minutes) the repo configures for pnpm,
|
|
420
|
+
# read from pnpm-workspace.yaml (`minimumReleaseAge:`) or `.npmrc`
|
|
421
|
+
# (`minimum-release-age=`), or nil when unset. A value we cannot parse as a
|
|
422
|
+
# bare integer is reported as Float::INFINITY so an explicit-but-non-numeric
|
|
423
|
+
# user gate is never overridden by the cooldown floor.
|
|
424
|
+
sig { returns(T.nilable(T.any(Integer, Float))) }
|
|
425
|
+
def pnpm_configured_minimum_release_age
|
|
426
|
+
settings = [
|
|
427
|
+
Helpers::ReleaseAgeGateSetting.new(
|
|
428
|
+
filename: "pnpm-workspace.yaml", key: "minimumReleaseAge", separator: ":"
|
|
429
|
+
)
|
|
430
|
+
]
|
|
431
|
+
# pnpm 11+ ignores non-registry settings in .npmrc, so a .npmrc
|
|
432
|
+
# `minimum-release-age` is only an effective user gate on pnpm 10.x.
|
|
433
|
+
# Counting it on pnpm 11 would wrongly suppress the cooldown CLI override
|
|
434
|
+
# while pnpm resolves with no age gate.
|
|
435
|
+
if pnpm_reads_npmrc_release_age?
|
|
436
|
+
settings << Helpers::ReleaseAgeGateSetting.new(
|
|
437
|
+
filename: ".npmrc", key: "minimum-release-age", separator: "="
|
|
438
|
+
)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
Helpers.max_configured_release_age(dependency_files, settings)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
sig { returns(T::Boolean) }
|
|
445
|
+
def pnpm_reads_npmrc_release_age?
|
|
446
|
+
version = pnpm_version
|
|
447
|
+
!version.nil? && version < Version.new(PNPM_NPMRC_RELEASE_AGE_DROPPED_VERSION)
|
|
228
448
|
end
|
|
229
449
|
|
|
230
450
|
# Tries `pnpm update --depth Infinity <dep>` for each dependency as a
|
|
231
451
|
# first-tier fallback when the regular update is a no-op (typically
|
|
232
452
|
# transitive deps not listed in any package.json), without relying on
|
|
233
|
-
# audit fixes that may modify manifests on older pnpm versions.
|
|
453
|
+
# audit fixes that may modify manifests on older pnpm versions. It is
|
|
454
|
+
# routed through the release-age gate so the fallback cannot bypass the
|
|
455
|
+
# transitive dependency cooldown.
|
|
234
456
|
sig { void }
|
|
235
457
|
def run_pnpm_deep_update_fallback
|
|
236
458
|
recursive = workspace_files.any?
|
|
237
459
|
dependencies.each do |dep|
|
|
238
|
-
NativeHelpers.
|
|
460
|
+
cmd, fingerprint = NativeHelpers.pnpm_deep_update_command(dep.name, recursive: recursive)
|
|
461
|
+
run_pnpm_command_with_release_age_gate(cmd, fingerprint)
|
|
239
462
|
dep.metadata[:deep_update_used] = true
|
|
240
463
|
end
|
|
241
464
|
rescue SharedHelpers::HelperSubprocessFailed
|
|
@@ -253,7 +476,8 @@ module Dependabot
|
|
|
253
476
|
package_json_snapshots = Dir.glob("**/package.json").to_h { |f| [f, File.read(f)] }
|
|
254
477
|
|
|
255
478
|
begin
|
|
256
|
-
NativeHelpers.
|
|
479
|
+
cmd, fingerprint = NativeHelpers.pnpm_audit_fix_command
|
|
480
|
+
run_pnpm_command_with_release_age_gate(cmd, fingerprint)
|
|
257
481
|
run_pnpm_install
|
|
258
482
|
|
|
259
483
|
manifest_changed = package_json_snapshots.any? { |f, c| File.read(f) != c }
|
|
@@ -31,7 +31,8 @@ module Dependabot
|
|
|
31
31
|
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
32
32
|
repo_contents_path: T.nilable(String),
|
|
33
33
|
credentials: T::Array[Dependabot::Credential],
|
|
34
|
-
security_updates_only: T::Boolean
|
|
34
|
+
security_updates_only: T::Boolean,
|
|
35
|
+
release_age_days: T.nilable(Integer)
|
|
35
36
|
)
|
|
36
37
|
.void
|
|
37
38
|
end
|
|
@@ -40,13 +41,15 @@ module Dependabot
|
|
|
40
41
|
dependency_files:,
|
|
41
42
|
repo_contents_path:,
|
|
42
43
|
credentials:,
|
|
43
|
-
security_updates_only: false
|
|
44
|
+
security_updates_only: false,
|
|
45
|
+
release_age_days: nil
|
|
44
46
|
)
|
|
45
47
|
@dependencies = dependencies
|
|
46
48
|
@dependency_files = dependency_files
|
|
47
49
|
@repo_contents_path = repo_contents_path
|
|
48
50
|
@credentials = credentials
|
|
49
51
|
@security_updates_only = security_updates_only
|
|
52
|
+
@release_age_days = release_age_days
|
|
50
53
|
@error_handler = T.let(
|
|
51
54
|
YarnErrorHandler.new(
|
|
52
55
|
dependencies: dependencies,
|
|
@@ -345,40 +348,92 @@ module Dependabot
|
|
|
345
348
|
]
|
|
346
349
|
|
|
347
350
|
original_content = File.read(yarn_lock.name)
|
|
348
|
-
Helpers.run_yarn_commands(*commands)
|
|
351
|
+
Helpers.run_yarn_commands(*commands, env: yarn_time_gate_env)
|
|
349
352
|
|
|
350
353
|
updated_content = File.read(yarn_lock.name)
|
|
351
354
|
if updated_content == original_content && Dependabot::Experiments.enabled?(:enable_audit_fix_fallback)
|
|
352
|
-
|
|
353
|
-
NativeHelpers.run_yarn_audit_fix_command
|
|
354
|
-
dep.metadata[:audit_fix_used] = true
|
|
355
|
-
rescue SharedHelpers::HelperSubprocessFailed
|
|
356
|
-
Dependabot.logger.info(
|
|
357
|
-
"yarn npm audit --fix failed or partially fixed — continuing with any changes made"
|
|
358
|
-
)
|
|
359
|
-
end
|
|
355
|
+
run_yarn_audit_fix_fallback(dep)
|
|
360
356
|
updated_content = File.read(yarn_lock.name)
|
|
361
357
|
end
|
|
362
358
|
|
|
363
359
|
{ yarn_lock.name => updated_content }
|
|
364
360
|
end
|
|
365
361
|
|
|
362
|
+
# Fallback for transitive deps where add/dedupe/remove is a no-op. Threads
|
|
363
|
+
# the release-age gate env so the audit-fix resolve honours the same cooldown
|
|
364
|
+
# (and security `=0` bypass) as the primary commands.
|
|
365
|
+
sig { params(dep: Dependabot::Dependency).void }
|
|
366
|
+
def run_yarn_audit_fix_fallback(dep)
|
|
367
|
+
NativeHelpers.run_yarn_audit_fix_command(env: yarn_time_gate_env)
|
|
368
|
+
dep.metadata[:audit_fix_used] = true
|
|
369
|
+
rescue SharedHelpers::HelperSubprocessFailed
|
|
370
|
+
Dependabot.logger.info(
|
|
371
|
+
"yarn npm audit --fix failed or partially fixed — continuing with any changes made"
|
|
372
|
+
)
|
|
373
|
+
end
|
|
374
|
+
|
|
366
375
|
sig { returns(T::Boolean) }
|
|
367
376
|
def security_updates_only?
|
|
368
377
|
@security_updates_only
|
|
369
378
|
end
|
|
370
379
|
|
|
371
|
-
# Returns an env hash that
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
#
|
|
380
|
+
# Returns an env hash that sets Yarn Berry's YARN_NPM_MINIMAL_AGE_GATE.
|
|
381
|
+
# Security updates set it to 0 so a release-age gate never blocks a fix
|
|
382
|
+
# (this intentionally overrides any `.yarnrc.yml` gate). Regular updates set
|
|
383
|
+
# it to the dependabot.yml cooldown floor (in minutes) so yarn holds
|
|
384
|
+
# transitive dependencies back to versions at least that old. For regular
|
|
385
|
+
# updates an equal-or-longer `npmMinimalAgeGate` in .yarnrc.yml takes
|
|
386
|
+
# precedence, so it is left untouched.
|
|
387
|
+
#
|
|
388
|
+
# npmMinimalAgeGate was introduced in Yarn 4.10.0. Setting the env var on
|
|
389
|
+
# older Yarn Berry releases (or Yarn classic) raises "Unrecognized or legacy
|
|
390
|
+
# configuration settings found: npmMinimalAgeGate" and aborts the install,
|
|
391
|
+
# so we gate on a version check.
|
|
392
|
+
#
|
|
393
|
+
# Memoized (including a nil result): the value is deterministic for the
|
|
394
|
+
# updater's lifetime and this is invoked from several yarn command sites,
|
|
395
|
+
# so we avoid running `yarn --version` (and emitting duplicate log lines)
|
|
396
|
+
# more than once per update.
|
|
376
397
|
sig { returns(T.nilable(T::Hash[String, String])) }
|
|
377
398
|
def yarn_time_gate_env
|
|
378
|
-
return
|
|
379
|
-
return nil unless Helpers.yarn_berry_supports_minimal_age_gate?
|
|
399
|
+
return @yarn_time_gate_env if defined?(@yarn_time_gate_env)
|
|
380
400
|
|
|
381
|
-
|
|
401
|
+
gate_value = yarn_minimal_age_gate_value
|
|
402
|
+
env = if gate_value && Helpers.yarn_berry_supports_minimal_age_gate?
|
|
403
|
+
{ "YARN_NPM_MINIMAL_AGE_GATE" => gate_value }
|
|
404
|
+
end
|
|
405
|
+
@yarn_time_gate_env = T.let(env, T.nilable(T::Hash[String, String]))
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Returns the YARN_NPM_MINIMAL_AGE_GATE value (in minutes) for a regular
|
|
409
|
+
# update, or nil when none applies. Security updates pass "0" so a
|
|
410
|
+
# release-age gate never blocks a fix (this intentionally overrides any
|
|
411
|
+
# `.yarnrc.yml` gate).
|
|
412
|
+
#
|
|
413
|
+
# When the repo also sets an explicit `npmMinimalAgeGate` in `.yarnrc.yml`,
|
|
414
|
+
# the longest release-age wins: the cooldown floor is only injected via the
|
|
415
|
+
# env var when it exceeds the user's configured value, otherwise the user's
|
|
416
|
+
# (equal or longer) gate is left untouched so neither policy is silently
|
|
417
|
+
# weakened.
|
|
418
|
+
sig { returns(T.nilable(String)) }
|
|
419
|
+
def yarn_minimal_age_gate_value
|
|
420
|
+
return "0" if security_updates_only?
|
|
421
|
+
|
|
422
|
+
cooldown_minutes = @release_age_days && (@release_age_days * Helpers::MINUTES_PER_DAY)
|
|
423
|
+
effective = Helpers.higher_release_age_gate(cooldown_minutes, yarnrc_minimal_age_gate)
|
|
424
|
+
effective&.to_s
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# The `npmMinimalAgeGate` (in minutes) configured across the repo's
|
|
428
|
+
# `.yarnrc.yml` files, or nil when unset. A value we cannot parse as a bare
|
|
429
|
+
# integer is reported as Float::INFINITY so an explicit-but-non-numeric user
|
|
430
|
+
# gate is never overridden by the cooldown floor.
|
|
431
|
+
sig { returns(T.nilable(T.any(Integer, Float))) }
|
|
432
|
+
def yarnrc_minimal_age_gate
|
|
433
|
+
Helpers.max_configured_release_age(
|
|
434
|
+
dependency_files,
|
|
435
|
+
[Helpers::ReleaseAgeGateSetting.new(filename: ".yarnrc.yml", key: "npmMinimalAgeGate", separator: ":")]
|
|
436
|
+
)
|
|
382
437
|
end
|
|
383
438
|
|
|
384
439
|
sig { returns(String) }
|
|
@@ -6,6 +6,7 @@ require "dependabot/file_updaters/base"
|
|
|
6
6
|
require "dependabot/file_updaters/vendor_updater"
|
|
7
7
|
require "dependabot/file_updaters/artifact_updater"
|
|
8
8
|
require "dependabot/errors"
|
|
9
|
+
require "dependabot/package/release_cooldown_options"
|
|
9
10
|
require "dependabot/npm_and_yarn/dependency_files_filterer"
|
|
10
11
|
require "dependabot/npm_and_yarn/sub_dependency_files_filterer"
|
|
11
12
|
require "sorbet-runtime"
|
|
@@ -435,6 +436,33 @@ module Dependabot
|
|
|
435
436
|
)
|
|
436
437
|
end
|
|
437
438
|
|
|
439
|
+
# The number of days from the dependabot.yml `cooldown` config to apply as
|
|
440
|
+
# a release-age floor for *transitive* dependencies. npm, pnpm and yarn each
|
|
441
|
+
# enforce this natively at install time, which is the only point at which
|
|
442
|
+
# Dependabot can constrain the versions the package manager resolves for the
|
|
443
|
+
# transitive tree. Returns nil for security updates (which must never be
|
|
444
|
+
# blocked by a release-age gate) or when no positive cooldown is configured.
|
|
445
|
+
# `default_days` is used because the native gates are a single global value
|
|
446
|
+
# and cannot express per-semver-type days or include/exclude patterns.
|
|
447
|
+
#
|
|
448
|
+
# The gate is also skipped when none of the dependencies being updated are
|
|
449
|
+
# subject to the cooldown (per its `include`/`exclude` patterns), so an
|
|
450
|
+
# update the user explicitly opted out of cooldown for is not gated.
|
|
451
|
+
sig { returns(T.nilable(Integer)) }
|
|
452
|
+
def cooldown_release_age_days
|
|
453
|
+
return nil if options.fetch(:security_updates_only, false)
|
|
454
|
+
|
|
455
|
+
cooldown = T.cast(
|
|
456
|
+
options[:update_cooldown],
|
|
457
|
+
T.nilable(Dependabot::Package::ReleaseCooldownOptions)
|
|
458
|
+
)
|
|
459
|
+
return nil if cooldown.nil?
|
|
460
|
+
return nil unless dependencies.any? { |dep| cooldown.included?(dep.name) }
|
|
461
|
+
|
|
462
|
+
days = cooldown.default_days
|
|
463
|
+
days.positive? ? days : nil
|
|
464
|
+
end
|
|
465
|
+
|
|
438
466
|
sig { returns(Dependabot::NpmAndYarn::FileUpdater::YarnLockfileUpdater) }
|
|
439
467
|
def yarn_lockfile_updater
|
|
440
468
|
@yarn_lockfile_updater ||= T.let(
|
|
@@ -443,7 +471,8 @@ module Dependabot
|
|
|
443
471
|
dependency_files: dependency_files,
|
|
444
472
|
repo_contents_path: repo_contents_path,
|
|
445
473
|
credentials: credentials,
|
|
446
|
-
security_updates_only: options.fetch(:security_updates_only, false) ? true : false
|
|
474
|
+
security_updates_only: options.fetch(:security_updates_only, false) ? true : false,
|
|
475
|
+
release_age_days: cooldown_release_age_days
|
|
447
476
|
),
|
|
448
477
|
T.nilable(Dependabot::NpmAndYarn::FileUpdater::YarnLockfileUpdater)
|
|
449
478
|
)
|
|
@@ -457,7 +486,8 @@ module Dependabot
|
|
|
457
486
|
dependency_files: dependency_files,
|
|
458
487
|
repo_contents_path: repo_contents_path,
|
|
459
488
|
credentials: credentials,
|
|
460
|
-
security_updates_only: options.fetch(:security_updates_only, false) ? true : false
|
|
489
|
+
security_updates_only: options.fetch(:security_updates_only, false) ? true : false,
|
|
490
|
+
release_age_days: cooldown_release_age_days
|
|
461
491
|
),
|
|
462
492
|
T.nilable(Dependabot::NpmAndYarn::FileUpdater::PnpmLockfileUpdater)
|
|
463
493
|
)
|
|
@@ -474,7 +504,8 @@ module Dependabot
|
|
|
474
504
|
dependencies: dependencies,
|
|
475
505
|
dependency_files: dependency_files,
|
|
476
506
|
credentials: credentials,
|
|
477
|
-
security_updates_only: options.fetch(:security_updates_only, false) ? true : false
|
|
507
|
+
security_updates_only: options.fetch(:security_updates_only, false) ? true : false,
|
|
508
|
+
release_age_days: cooldown_release_age_days
|
|
478
509
|
)
|
|
479
510
|
end
|
|
480
511
|
|
|
@@ -50,6 +50,9 @@ module Dependabot
|
|
|
50
50
|
NPM_V6 = 6
|
|
51
51
|
NPM_DEFAULT_VERSION = NPM_V11
|
|
52
52
|
|
|
53
|
+
# Minimum npm version that supports the `--min-release-age` CLI flag.
|
|
54
|
+
NPM_MINIMUM_RELEASE_AGE_VERSION = "11.10.0"
|
|
55
|
+
|
|
53
56
|
# PNPM Version Constants
|
|
54
57
|
PNPM_V10 = 10
|
|
55
58
|
PNPM_V9 = 9
|
|
@@ -70,6 +73,91 @@ module Dependabot
|
|
|
70
73
|
SUPPORTED_COREPACK_PACKAGE_MANAGERS = %w(npm yarn pnpm).freeze
|
|
71
74
|
COREPACK_SIGNATURE_METADATA_ERROR = "No compatible signature found in package metadata"
|
|
72
75
|
|
|
76
|
+
# pnpm's `minimumReleaseAge` and yarn's `npmMinimalAgeGate` are expressed in
|
|
77
|
+
# minutes, whereas dependabot.yml cooldown and npm's `min-release-age` use
|
|
78
|
+
# days. Used to convert a cooldown floor (days) into the minutes those gates
|
|
79
|
+
# expect.
|
|
80
|
+
MINUTES_PER_DAY = 1440
|
|
81
|
+
|
|
82
|
+
# Resolves the release-age gate value a lockfile updater should enforce for a
|
|
83
|
+
# regular update, applying "highest precedence wins" between the Dependabot
|
|
84
|
+
# cooldown floor and any explicit native gate the user configured (both given
|
|
85
|
+
# in the same package-manager unit). Returns the value to pass on the CLI/env
|
|
86
|
+
# (overriding the user's config) when the cooldown is longer, or nil to leave
|
|
87
|
+
# the user's own (equal-or-longer) gate — or the absence of one — untouched.
|
|
88
|
+
# A non-numeric user gate should be passed as Float::INFINITY so it is never
|
|
89
|
+
# overridden. Security updates handle their own `=0` bypass and must not use
|
|
90
|
+
# this method.
|
|
91
|
+
sig do
|
|
92
|
+
params(cooldown: T.nilable(Integer), user_gate: T.nilable(T.any(Integer, Float))).returns(T.nilable(Integer))
|
|
93
|
+
end
|
|
94
|
+
def self.higher_release_age_gate(cooldown, user_gate)
|
|
95
|
+
return nil if cooldown.nil? || !cooldown.positive?
|
|
96
|
+
return nil if user_gate && cooldown <= user_gate
|
|
97
|
+
|
|
98
|
+
cooldown
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Describes where a native release-age gate can be configured: the file it
|
|
102
|
+
# may appear in, the setting key, and the separator between key and value
|
|
103
|
+
# ("=" for npmrc/ini-style files, ":" for YAML). Passed to
|
|
104
|
+
# `max_configured_release_age` so each ecosystem parses its user-configured
|
|
105
|
+
# gate through the same logic.
|
|
106
|
+
class ReleaseAgeGateSetting < T::Struct
|
|
107
|
+
const :filename, String
|
|
108
|
+
const :key, String
|
|
109
|
+
const :separator, String
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The largest explicitly-configured native release-age gate across the repo's
|
|
113
|
+
# dependency files, or nil when none is set. Each file whose basename matches
|
|
114
|
+
# one of `settings` is parsed for a bare integer value; a present-but-non-
|
|
115
|
+
# numeric value is reported as Float::INFINITY so an explicit user gate is
|
|
116
|
+
# never overridden by the cooldown floor (see `higher_release_age_gate`).
|
|
117
|
+
sig do
|
|
118
|
+
params(
|
|
119
|
+
dependency_files: T::Array[DependencyFile],
|
|
120
|
+
settings: T::Array[ReleaseAgeGateSetting]
|
|
121
|
+
).returns(T.nilable(T.any(Integer, Float)))
|
|
122
|
+
end
|
|
123
|
+
def self.max_configured_release_age(dependency_files, settings)
|
|
124
|
+
values = dependency_files.filter_map do |file|
|
|
125
|
+
setting = settings.find { |candidate| candidate.filename == File.basename(file.name) }
|
|
126
|
+
next unless setting
|
|
127
|
+
|
|
128
|
+
configured_release_age(file.content.to_s, setting)
|
|
129
|
+
end
|
|
130
|
+
values.max
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Resolves the effective release-age value a single file sets for `setting`,
|
|
134
|
+
# or nil when the key is absent. npmrc/INI and YAML both let the *last*
|
|
135
|
+
# occurrence of a key win, so we read the last matching line rather than the
|
|
136
|
+
# first (e.g. `min-release-age=30` then `min-release-age=3` resolves to 3). A
|
|
137
|
+
# present-but-non-numeric value is reported as Float::INFINITY.
|
|
138
|
+
sig { params(content: String, setting: ReleaseAgeGateSetting).returns(T.nilable(T.any(Integer, Float))) }
|
|
139
|
+
def self.configured_release_age(content, setting)
|
|
140
|
+
key = Regexp.escape(setting.key)
|
|
141
|
+
separator = Regexp.escape(setting.separator)
|
|
142
|
+
# Match an optionally quoted key (`"minimumReleaseAge":`) so a valid quoted
|
|
143
|
+
# YAML key is not treated as absent.
|
|
144
|
+
quoted_key = /["']?#{key}["']?/
|
|
145
|
+
presence = /^\s*#{quoted_key}\s*#{separator}/
|
|
146
|
+
# Allow an optionally quoted value and an optional trailing comment
|
|
147
|
+
# (e.g. `minimumReleaseAge: "4320" # 3 days`). npmrc/INI treats both `#` and
|
|
148
|
+
# `;` as comment delimiters; YAML uses `#` only. Otherwise a quoted or
|
|
149
|
+
# commented value is treated as non-numeric (Float::INFINITY).
|
|
150
|
+
comment_chars = setting.separator == "=" ? "#;" : "#"
|
|
151
|
+
value = /^\s*#{quoted_key}\s*#{separator}\s*["']?(\d+)["']?\s*(?:[#{comment_chars}].*)?$/
|
|
152
|
+
|
|
153
|
+
last_line = content.lines.reverse_each.find { |line| line.match?(presence) }
|
|
154
|
+
return unless last_line
|
|
155
|
+
|
|
156
|
+
match = last_line.match(value)
|
|
157
|
+
match ? T.must(match[1]).to_i : Float::INFINITY
|
|
158
|
+
end
|
|
159
|
+
private_class_method :configured_release_age
|
|
160
|
+
|
|
73
161
|
sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) }
|
|
74
162
|
def self.npm_version_numeric(lockfile)
|
|
75
163
|
detected_npm_version = detect_npm_version(lockfile)
|
|
@@ -141,6 +229,62 @@ module Dependabot
|
|
|
141
229
|
PNPM_FALLBACK_VERSION
|
|
142
230
|
end
|
|
143
231
|
|
|
232
|
+
# The concrete pnpm version that will run for this update — resolved via
|
|
233
|
+
# Corepack when the corepack experiment is enabled (honouring the repo's
|
|
234
|
+
# `packageManager` pin), otherwise the pnpm on PATH. Returns nil when the
|
|
235
|
+
# version can't be determined. Used to gate version-specific config such as
|
|
236
|
+
# `minimumReleaseAge` (added in pnpm 10.16) and `minimumReleaseAgeStrict`
|
|
237
|
+
# (added in pnpm 11.0), which older pnpm versions silently ignore.
|
|
238
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
239
|
+
def self.pnpm_version
|
|
240
|
+
raw = if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
|
|
241
|
+
package_manager_version(PNPMPackageManager::NAME, env: merge_corepack_env(nil))
|
|
242
|
+
else
|
|
243
|
+
local_package_manager_version(PNPMPackageManager::NAME)
|
|
244
|
+
end
|
|
245
|
+
Version.new(raw)
|
|
246
|
+
rescue StandardError => e
|
|
247
|
+
Dependabot.logger.warn("Could not determine pnpm version to gate release-age settings: #{e.message}")
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# The concrete npm version that will run — via Corepack when the corepack
|
|
252
|
+
# experiment is enabled (honouring the repo's `packageManager` pin, which can
|
|
253
|
+
# select npm 7-11), otherwise the npm on PATH. Returns nil when it can't be
|
|
254
|
+
# determined. Used to gate `--min-release-age`, added in npm 11.10.
|
|
255
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
256
|
+
def self.npm_version
|
|
257
|
+
raw = if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
|
|
258
|
+
package_manager_version(NpmPackageManager::NAME, env: merge_corepack_env(nil))
|
|
259
|
+
else
|
|
260
|
+
local_package_manager_version(NpmPackageManager::NAME)
|
|
261
|
+
end
|
|
262
|
+
Version.new(raw)
|
|
263
|
+
rescue StandardError => e
|
|
264
|
+
Dependabot.logger.warn("Could not determine npm version to gate release-age settings: #{e.message}")
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# True when the running npm supports `--min-release-age` (npm 11.10+). Because
|
|
269
|
+
# npm runs through Corepack, a repo pinned to an older npm via `packageManager`
|
|
270
|
+
# would reject the flag, so the cooldown gate must be skipped for it.
|
|
271
|
+
sig { returns(T::Boolean) }
|
|
272
|
+
def self.npm_supports_min_release_age?
|
|
273
|
+
version = npm_version
|
|
274
|
+
return false if version.nil?
|
|
275
|
+
|
|
276
|
+
supported = version >= Version.new(NPM_MINIMUM_RELEASE_AGE_VERSION)
|
|
277
|
+
if supported
|
|
278
|
+
Dependabot.logger.info("npm #{version} supports --min-release-age.")
|
|
279
|
+
else
|
|
280
|
+
Dependabot.logger.info(
|
|
281
|
+
"npm #{version} does not support --min-release-age (requires 11.10.0+); the release-age " \
|
|
282
|
+
"cooldown gate will not be applied to transitive dependencies."
|
|
283
|
+
)
|
|
284
|
+
end
|
|
285
|
+
supported
|
|
286
|
+
end
|
|
287
|
+
|
|
144
288
|
sig { params(key: String, default_value: String).returns(T.untyped) }
|
|
145
289
|
def self.fetch_yarnrc_yml_value(key, default_value)
|
|
146
290
|
if File.exist?(".yarnrc.yml") && (yarnrc = YAML.load_file(".yarnrc.yml"))
|
|
@@ -254,10 +398,7 @@ module Dependabot
|
|
|
254
398
|
version = Version.new(run_single_yarn_command("--version"))
|
|
255
399
|
supported = version >= Version.new("4.10.0")
|
|
256
400
|
if supported
|
|
257
|
-
Dependabot.logger.info(
|
|
258
|
-
"Yarn #{version} supports npmMinimalAgeGate. " \
|
|
259
|
-
"Setting YARN_NPM_MINIMAL_AGE_GATE=0 to bypass the release-age gate for security updates."
|
|
260
|
-
)
|
|
401
|
+
Dependabot.logger.info("Yarn #{version} supports npmMinimalAgeGate.")
|
|
261
402
|
else
|
|
262
403
|
Dependabot.logger.info(
|
|
263
404
|
"Yarn #{version} does not support npmMinimalAgeGate (requires 4.10.0+). " \
|
|
@@ -268,8 +409,9 @@ module Dependabot
|
|
|
268
409
|
rescue StandardError => e
|
|
269
410
|
Dependabot.logger.warn(
|
|
270
411
|
"Could not determine Yarn version to check npmMinimalAgeGate support: #{e.message}. " \
|
|
271
|
-
"Assuming unsupported (returning false). YARN_NPM_MINIMAL_AGE_GATE will not be set, " \
|
|
272
|
-
"
|
|
412
|
+
"Assuming unsupported (returning false). YARN_NPM_MINIMAL_AGE_GATE will not be set, so the " \
|
|
413
|
+
"security-update `=0` bypass cannot be applied — any release-age gate configured in " \
|
|
414
|
+
".yarnrc.yml (npmMinimalAgeGate) or enforced by the registry may still block security updates."
|
|
273
415
|
)
|
|
274
416
|
false
|
|
275
417
|
end
|
|
@@ -301,11 +443,11 @@ module Dependabot
|
|
|
301
443
|
# set to false. Yarn commands should _not_ be ran outside of this helper
|
|
302
444
|
# to ensure that postinstall scripts are never executed, as they could
|
|
303
445
|
# contain malicious code.
|
|
304
|
-
sig { params(commands: T::Array[String]).void }
|
|
305
|
-
def self.run_yarn_commands(*commands)
|
|
446
|
+
sig { params(commands: T::Array[String], env: T.nilable(T::Hash[String, String])).void }
|
|
447
|
+
def self.run_yarn_commands(*commands, env: nil)
|
|
306
448
|
setup_yarn_berry
|
|
307
449
|
commands.each do |cmd, fingerprint|
|
|
308
|
-
run_single_yarn_command(cmd, fingerprint: fingerprint) if cmd
|
|
450
|
+
run_single_yarn_command(cmd, fingerprint: fingerprint, env: env) if cmd
|
|
309
451
|
end
|
|
310
452
|
end
|
|
311
453
|
|
|
@@ -24,8 +24,10 @@ module Dependabot
|
|
|
24
24
|
File.join(__dir__, "../../../helpers")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
sig
|
|
28
|
-
|
|
27
|
+
sig do
|
|
28
|
+
params(dependency_names: T::Array[String], min_release_age_arg: T.nilable(String)).returns(String)
|
|
29
|
+
end
|
|
30
|
+
def self.run_npm8_subdependency_update_command(dependency_names, min_release_age_arg: nil)
|
|
29
31
|
# NOTE: npm options
|
|
30
32
|
# - `--force` ignores checks for platform (os, cpu) and engines
|
|
31
33
|
# - `--ignore-scripts` disables prepare and prepack scripts which are run
|
|
@@ -37,9 +39,10 @@ module Dependabot
|
|
|
37
39
|
"--ignore-scripts",
|
|
38
40
|
"--package-lock-only"
|
|
39
41
|
]
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
|
|
42
|
+
# Apply the effective release-age gate: `=0` bypasses any `.npmrc` gate for
|
|
43
|
+
# security fixes, a positive value enforces the dependabot.yml cooldown
|
|
44
|
+
# floor on transitive updates. nil leaves npm's own resolution untouched.
|
|
45
|
+
command_args << min_release_age_arg if min_release_age_arg
|
|
43
46
|
command = command_args.join(" ")
|
|
44
47
|
|
|
45
48
|
fingerprint_args = [
|
|
@@ -49,41 +52,51 @@ module Dependabot
|
|
|
49
52
|
"--ignore-scripts",
|
|
50
53
|
"--package-lock-only"
|
|
51
54
|
]
|
|
52
|
-
fingerprint_args <<
|
|
55
|
+
fingerprint_args << fingerprint_min_release_age_arg(min_release_age_arg) if min_release_age_arg
|
|
53
56
|
fingerprint = fingerprint_args.join(" ")
|
|
54
57
|
|
|
55
58
|
Helpers.run_npm_command(command, fingerprint: fingerprint)
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
sig { params(
|
|
59
|
-
def self.run_npm_audit_fix_command(
|
|
61
|
+
sig { params(min_release_age_arg: T.nilable(String)).returns(String) }
|
|
62
|
+
def self.run_npm_audit_fix_command(min_release_age_arg: nil)
|
|
60
63
|
# Fallback for transitive dependencies in workspace repos where
|
|
61
64
|
# `npm update` is a no-op because the package isn't in package.json.
|
|
62
65
|
# `npm audit fix` updates all fixable vulnerabilities in the lockfile.
|
|
63
66
|
# `--force` ignores checks for platform (os, cpu) and engines,
|
|
64
67
|
# matching the flags used by run_npm8_subdependency_update_command.
|
|
65
68
|
command = "audit fix --force --package-lock-only --ignore-scripts"
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
fingerprint
|
|
69
|
+
# Apply the effective release-age gate (see run_npm8_subdependency_update_command).
|
|
70
|
+
command += " #{min_release_age_arg}" if min_release_age_arg
|
|
71
|
+
fingerprint = "audit fix --force --package-lock-only --ignore-scripts"
|
|
72
|
+
fingerprint += " #{fingerprint_min_release_age_arg(min_release_age_arg)}" if min_release_age_arg
|
|
70
73
|
|
|
71
74
|
Helpers.run_npm_command(command, fingerprint: fingerprint)
|
|
72
75
|
end
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
# Masks the varying cooldown day count out of the telemetry fingerprint while
|
|
78
|
+
# keeping the security `=0` bypass distinguishable (mirrors the npm lockfile
|
|
79
|
+
# updater's `fingerprint_min_release_age_arg`).
|
|
80
|
+
sig { params(arg: String).returns(String) }
|
|
81
|
+
def self.fingerprint_min_release_age_arg(arg)
|
|
82
|
+
arg == "--min-release-age=0" ? arg : "--min-release-age=<days>"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { returns([String, String]) }
|
|
86
|
+
def self.pnpm_audit_fix_command
|
|
76
87
|
# Fallback for transitive dependencies where `pnpm update` is a no-op.
|
|
77
88
|
# pnpm 11's update fix method updates vulnerable packages in the lockfile.
|
|
78
89
|
# Older supported versions only accept `--fix`, which may add manifest overrides.
|
|
79
90
|
version_output = Helpers.run_pnpm_command("-v", fingerprint: "-v")
|
|
80
91
|
fix_option = pnpm_major_version(version_output) >= 11 ? "--fix=update" : "--fix"
|
|
81
92
|
command = "audit #{fix_option}"
|
|
93
|
+
[command, command]
|
|
94
|
+
end
|
|
82
95
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
96
|
+
sig { returns(String) }
|
|
97
|
+
def self.run_pnpm_audit_fix_command
|
|
98
|
+
command, fingerprint = pnpm_audit_fix_command
|
|
99
|
+
Helpers.run_pnpm_command(command, fingerprint: fingerprint)
|
|
87
100
|
end
|
|
88
101
|
|
|
89
102
|
sig { params(output: String).returns(Integer) }
|
|
@@ -97,27 +110,36 @@ module Dependabot
|
|
|
97
110
|
end
|
|
98
111
|
private_class_method :pnpm_major_version
|
|
99
112
|
|
|
100
|
-
sig { params(dependency_name: String, recursive: T::Boolean).returns(String) }
|
|
101
|
-
def self.
|
|
113
|
+
sig { params(dependency_name: String, recursive: T::Boolean).returns([String, String]) }
|
|
114
|
+
def self.pnpm_deep_update_command(dependency_name, recursive: false)
|
|
102
115
|
# `pnpm update --depth Infinity <dep>` traverses the full dependency
|
|
103
116
|
# graph, allowing transitive dependencies to be updated in the lockfile
|
|
104
117
|
# without relying on audit fixes that may modify manifests on older pnpm versions.
|
|
105
118
|
# `-r --include-workspace-root` is required for workspace repos so the
|
|
106
119
|
# update is applied across all packages.
|
|
107
120
|
flags = recursive ? "-r --include-workspace-root " : ""
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
121
|
+
cmd = "#{flags}update #{dependency_name} --depth Infinity --lockfile-only"
|
|
122
|
+
fingerprint = "#{flags}update <dependency_name> --depth Infinity --lockfile-only"
|
|
123
|
+
[cmd, fingerprint]
|
|
112
124
|
end
|
|
113
125
|
|
|
114
|
-
sig { returns(String) }
|
|
115
|
-
def self.
|
|
126
|
+
sig { params(dependency_name: String, recursive: T::Boolean).returns(String) }
|
|
127
|
+
def self.run_pnpm_deep_update_command(dependency_name, recursive: false)
|
|
128
|
+
cmd, fingerprint = pnpm_deep_update_command(dependency_name, recursive: recursive)
|
|
129
|
+
Helpers.run_pnpm_command(cmd, fingerprint: fingerprint)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
sig { params(env: T.nilable(T::Hash[String, String])).returns(String) }
|
|
133
|
+
def self.run_yarn_audit_fix_command(env: nil)
|
|
116
134
|
# Fallback for transitive dependencies where `yarn up -R` is a no-op.
|
|
117
|
-
# `yarn npm audit --fix` updates vulnerable deps in the lockfile.
|
|
135
|
+
# `yarn npm audit --fix` updates vulnerable deps in the lockfile. The
|
|
136
|
+
# release-age gate env is threaded through so this lockfile-resolving
|
|
137
|
+
# command honours the same cooldown (and security `=0` bypass) as the
|
|
138
|
+
# primary add/dedupe/remove commands.
|
|
118
139
|
Helpers.run_yarn_command(
|
|
119
140
|
"npm audit --fix --mode update-lockfile",
|
|
120
|
-
fingerprint: "npm audit --fix --mode update-lockfile"
|
|
141
|
+
fingerprint: "npm audit --fix --mode update-lockfile",
|
|
142
|
+
env: env
|
|
121
143
|
)
|
|
122
144
|
end
|
|
123
145
|
end
|
|
@@ -329,13 +329,15 @@ module Dependabot
|
|
|
329
329
|
|
|
330
330
|
NativeHelpers.run_npm8_subdependency_update_command(
|
|
331
331
|
[dependency.name],
|
|
332
|
-
|
|
332
|
+
min_release_age_arg: security_updates_only? ? "--min-release-age=0" : nil
|
|
333
333
|
)
|
|
334
334
|
|
|
335
335
|
updated_content = File.read(lockfile_name)
|
|
336
336
|
if updated_content == original_content && Dependabot::Experiments.enabled?(:enable_audit_fix_fallback)
|
|
337
337
|
begin
|
|
338
|
-
NativeHelpers.run_npm_audit_fix_command(
|
|
338
|
+
NativeHelpers.run_npm_audit_fix_command(
|
|
339
|
+
min_release_age_arg: security_updates_only? ? "--min-release-age=0" : nil
|
|
340
|
+
)
|
|
339
341
|
dependency.metadata[:audit_fix_used] = true
|
|
340
342
|
rescue SharedHelpers::HelperSubprocessFailed
|
|
341
343
|
Dependabot.logger.info("npm audit fix failed or partially fixed — continuing with any changes made")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-npm_and_yarn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.392.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.392.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.392.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -388,7 +388,7 @@ licenses:
|
|
|
388
388
|
- MIT
|
|
389
389
|
metadata:
|
|
390
390
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
391
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
391
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.392.0
|
|
392
392
|
rdoc_options: []
|
|
393
393
|
require_paths:
|
|
394
394
|
- lib
|