dependabot-github_actions 0.389.0 → 0.390.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/github_actions/constants.rb +7 -0
- data/lib/dependabot/github_actions/file_fetcher.rb +20 -1
- data/lib/dependabot/github_actions/file_parser.rb +1 -3
- data/lib/dependabot/github_actions/file_updater.rb +100 -11
- data/lib/dependabot/github_actions/helpers.rb +7 -3
- data/lib/dependabot/github_actions/lockfile/cli_engine.rb +225 -0
- data/lib/dependabot/github_actions/lockfile/env.rb +52 -0
- data/lib/dependabot/github_actions/lockfile/errors.rb +52 -0
- data/lib/dependabot/github_actions/lockfile/reader.rb +129 -0
- data/lib/dependabot/github_actions/lockfile/version_gate.rb +31 -0
- data/lib/dependabot/github_actions/lockfile.rb +23 -0
- data/lib/dependabot/github_actions/package/package_details_fetcher.rb +11 -4
- data/lib/dependabot/github_actions/update_checker/latest_version_finder.rb +19 -5
- data/lib/dependabot/github_actions/update_checker.rb +137 -24
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 915f664971e3665b0f6ee89557dc064b19fac2a49600250d2762a286fca20da7
|
|
4
|
+
data.tar.gz: a02dc5897d62ee777fac539862d8e0cd728695d43f31ad531e1dc8c5a65db287
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1d0fe90498dbae54af7ee1105a98c20a56128633d1ab72e11530e890726130f009fb581d616bda816985aa39a6dd1f7605ca94999e10e05a468d8f1ffecf752
|
|
7
|
+
data.tar.gz: 76a7272475cb1ffcf7964f74f08ac3cfa80efe4612932f8d520b456c3fead93bfa195770420e494d7e1265b2bf0d2237e6b85e9143ed52bf02d52ed985db5ecb
|
|
@@ -33,6 +33,13 @@ module Dependabot
|
|
|
33
33
|
# The path to the config .yml file
|
|
34
34
|
CONFIG_YMLS = T.let("#{WORKFLOW_DIRECTORY}/#{ANYTHING_YML}".freeze, String)
|
|
35
35
|
|
|
36
|
+
# The basename of the per-repo Actions lockfile generated by gh-actions-lock
|
|
37
|
+
LOCKFILE_NAME = T.let("actions.lock", String)
|
|
38
|
+
# The repo-relative path to the Actions lockfile
|
|
39
|
+
LOCKFILE_PATH = T.let("#{WORKFLOW_DIRECTORY}/#{LOCKFILE_NAME}".freeze, String)
|
|
40
|
+
# The only lockfile schema version this ecosystem understands.
|
|
41
|
+
SUPPORTED_LOCKFILE_VERSION = T.let("v0.0.2", String)
|
|
42
|
+
|
|
36
43
|
OWNER_KEY = T.let("owner", String)
|
|
37
44
|
REPO_KEY = T.let("repo", String)
|
|
38
45
|
PATH_KEY = T.let("path", String)
|
|
@@ -5,6 +5,7 @@ require "sorbet-runtime"
|
|
|
5
5
|
|
|
6
6
|
require "dependabot/file_fetchers"
|
|
7
7
|
require "dependabot/file_fetchers/base"
|
|
8
|
+
require "dependabot/experiments"
|
|
8
9
|
require "dependabot/github_actions/constants"
|
|
9
10
|
|
|
10
11
|
module Dependabot
|
|
@@ -44,7 +45,14 @@ module Dependabot
|
|
|
44
45
|
fetched_files = []
|
|
45
46
|
fetched_files += correctly_encoded_workflow_files
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
if fetched_files.any?
|
|
49
|
+
# The lockfile is additive: it is only fetched alongside workflow files
|
|
50
|
+
# and never activates the ecosystem on its own. Relocking also requires
|
|
51
|
+
# every workflow to be materialized, so retain the workflow-only path if
|
|
52
|
+
# an invalidly encoded workflow had to be omitted.
|
|
53
|
+
fetched_files += [actions_lockfile].compact if incorrectly_encoded_workflow_files.empty?
|
|
54
|
+
return fetched_files
|
|
55
|
+
end
|
|
48
56
|
|
|
49
57
|
if incorrectly_encoded_workflow_files.none?
|
|
50
58
|
expected_paths =
|
|
@@ -68,6 +76,17 @@ module Dependabot
|
|
|
68
76
|
|
|
69
77
|
private
|
|
70
78
|
|
|
79
|
+
# Fetches the canonical `.github/workflows/actions.lock` when the update covers
|
|
80
|
+
# repository workflows. Composite-action directories cannot own a lockfile.
|
|
81
|
+
sig { returns(T.nilable(DependencyFile)) }
|
|
82
|
+
def actions_lockfile
|
|
83
|
+
return unless Dependabot::Experiments.enabled?(:github_actions_lockfile)
|
|
84
|
+
return unless source.hostname == GITHUB_COM
|
|
85
|
+
return fetch_file_if_present(LOCKFILE_PATH) if directory == "/"
|
|
86
|
+
|
|
87
|
+
fetch_file_if_present(LOCKFILE_NAME) if directory.delete_prefix("/") == WORKFLOW_DIRECTORY
|
|
88
|
+
end
|
|
89
|
+
|
|
71
90
|
sig { returns(T::Array[DependencyFile]) }
|
|
72
91
|
def workflow_files
|
|
73
92
|
return @workflow_files unless @workflow_files.empty?
|
|
@@ -171,9 +171,7 @@ module Dependabot
|
|
|
171
171
|
|
|
172
172
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
173
173
|
def workflow_files
|
|
174
|
-
|
|
175
|
-
# filter here
|
|
176
|
-
dependency_files
|
|
174
|
+
dependency_files.reject { |file| file.path.delete_prefix("/") == LOCKFILE_PATH }
|
|
177
175
|
end
|
|
178
176
|
|
|
179
177
|
sig { override.void }
|
|
@@ -7,6 +7,7 @@ require "dependabot/errors"
|
|
|
7
7
|
require "dependabot/file_updaters"
|
|
8
8
|
require "dependabot/file_updaters/base"
|
|
9
9
|
require "dependabot/github_actions/constants"
|
|
10
|
+
require "dependabot/github_actions/lockfile"
|
|
10
11
|
|
|
11
12
|
module Dependabot
|
|
12
13
|
module GithubActions
|
|
@@ -15,18 +16,10 @@ module Dependabot
|
|
|
15
16
|
|
|
16
17
|
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
|
|
17
18
|
def updated_dependency_files
|
|
18
|
-
updated_files =
|
|
19
|
-
|
|
20
|
-
dependency_files.each do |file|
|
|
21
|
-
next unless requirement_changed?(file, dependency)
|
|
22
|
-
|
|
23
|
-
updated_files <<
|
|
24
|
-
updated_file(
|
|
25
|
-
file: file,
|
|
26
|
-
content: updated_workflow_file_content(file)
|
|
27
|
-
)
|
|
19
|
+
updated_files = changed_workflow_files.map do |file|
|
|
20
|
+
updated_file(file: file, content: updated_workflow_file_content(file))
|
|
28
21
|
end
|
|
29
|
-
|
|
22
|
+
updated_files.concat(relocked_files(updated_files))
|
|
30
23
|
updated_files.reject! { |f| dependency_files.include?(f) }
|
|
31
24
|
raise "No files changed!" if updated_files.none?
|
|
32
25
|
|
|
@@ -49,6 +42,102 @@ module Dependabot
|
|
|
49
42
|
raise "No workflow files!"
|
|
50
43
|
end
|
|
51
44
|
|
|
45
|
+
# Workflow files (everything except the lockfile) whose requirement changed.
|
|
46
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
47
|
+
def changed_workflow_files
|
|
48
|
+
dependency_files
|
|
49
|
+
.reject { |f| lockfile?(f) }
|
|
50
|
+
.select { |f| requirement_changed?(f, dependency) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# When the repo has an `actions.lock` authoritative for one or more changed
|
|
54
|
+
# workflows, regenerate it through the gh-actions-lock engine. Lock keys and
|
|
55
|
+
# onboarding comparisons are repo-relative paths, independent of the Dependabot
|
|
56
|
+
# `directory`. Workflows absent from the lock (and lockless repos) never reach
|
|
57
|
+
# here, preserving today's regex-only behavior.
|
|
58
|
+
sig do
|
|
59
|
+
params(updated_workflow_files: T::Array[Dependabot::DependencyFile])
|
|
60
|
+
.returns(T::Array[Dependabot::DependencyFile])
|
|
61
|
+
end
|
|
62
|
+
def relocked_files(updated_workflow_files)
|
|
63
|
+
changed_repository_workflows = changed_workflow_files.select do |file|
|
|
64
|
+
File.dirname(repo_relative_path(file)) == WORKFLOW_DIRECTORY
|
|
65
|
+
end
|
|
66
|
+
return [] if changed_repository_workflows.empty?
|
|
67
|
+
|
|
68
|
+
lock = lockfile
|
|
69
|
+
reader = lockfile_reader
|
|
70
|
+
return [] unless lock && reader
|
|
71
|
+
|
|
72
|
+
changed_onboarded = changed_repository_workflows.select { |f| reader.onboarded?(repo_relative_path(f)) }
|
|
73
|
+
return [] if changed_onboarded.empty?
|
|
74
|
+
|
|
75
|
+
# Gate only once the lock is authoritative for a workflow we're changing, so an
|
|
76
|
+
# incompatible/malformed lock over untouched workflows never blocks a legacy update.
|
|
77
|
+
Lockfile::VersionGate.assert_supported!(reader.version)
|
|
78
|
+
reader.validate_dependency_entries!
|
|
79
|
+
|
|
80
|
+
# Materialize the full onboarded closure so the lock remains intact, but fix
|
|
81
|
+
# only changed workflows so unrelated refs are not touched.
|
|
82
|
+
content = Lockfile::CliEngine.new(credentials).relock(
|
|
83
|
+
workflow_files: rewritten_onboarded_workflow_files(reader, updated_workflow_files),
|
|
84
|
+
lockfile: lock,
|
|
85
|
+
workflow_paths: changed_onboarded.map { |file| repo_relative_path(file) }
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
[updated_file(file: lock, content: content)]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The onboarded closure as the engine should see it: every workflow the lock
|
|
92
|
+
# tracks, with bumped refs applied to changed ones and the rest left verbatim.
|
|
93
|
+
sig do
|
|
94
|
+
params(
|
|
95
|
+
reader: Lockfile::Reader,
|
|
96
|
+
updated_workflow_files: T::Array[Dependabot::DependencyFile]
|
|
97
|
+
).returns(T::Array[Dependabot::DependencyFile])
|
|
98
|
+
end
|
|
99
|
+
def rewritten_onboarded_workflow_files(reader, updated_workflow_files)
|
|
100
|
+
updated_by_path = updated_workflow_files.to_h { |file| [repo_relative_path(file), file] }
|
|
101
|
+
onboarded_workflow_files(reader).map do |file|
|
|
102
|
+
updated_by_path.fetch(repo_relative_path(file), file)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# All workflow files the lock is authoritative for (changed or not).
|
|
107
|
+
sig { params(reader: Lockfile::Reader).returns(T::Array[Dependabot::DependencyFile]) }
|
|
108
|
+
def onboarded_workflow_files(reader)
|
|
109
|
+
dependency_files
|
|
110
|
+
.reject { |f| lockfile?(f) }
|
|
111
|
+
.select { |f| reader.onboarded?(repo_relative_path(f)) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
115
|
+
def lockfile
|
|
116
|
+
dependency_files.find { |f| lockfile?(f) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { returns(T.nilable(Lockfile::Reader)) }
|
|
120
|
+
def lockfile_reader
|
|
121
|
+
return @lockfile_reader if defined?(@lockfile_reader)
|
|
122
|
+
|
|
123
|
+
@lockfile_reader = T.let(
|
|
124
|
+
Lockfile::Reader.from_files(dependency_files),
|
|
125
|
+
T.nilable(Lockfile::Reader)
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
|
|
130
|
+
def lockfile?(file)
|
|
131
|
+
repo_relative_path(file) == LOCKFILE_PATH
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Repo-relative path (no leading slash), independent of the configured
|
|
135
|
+
# Dependabot directory. This is the canonical form lock keys use.
|
|
136
|
+
sig { params(file: Dependabot::DependencyFile).returns(String) }
|
|
137
|
+
def repo_relative_path(file)
|
|
138
|
+
file.path.delete_prefix("/")
|
|
139
|
+
end
|
|
140
|
+
|
|
52
141
|
# rubocop:disable Metrics/AbcSize
|
|
53
142
|
sig { params(file: Dependabot::DependencyFile).returns(String) }
|
|
54
143
|
def updated_workflow_file_content(file)
|
|
@@ -26,7 +26,8 @@ module Dependabot
|
|
|
26
26
|
ignored_versions: T::Array[String],
|
|
27
27
|
raise_on_ignored: T::Boolean,
|
|
28
28
|
consider_version_branches_pinned: T::Boolean,
|
|
29
|
-
dependency_source_details: T.nilable(T::Hash[Symbol, String])
|
|
29
|
+
dependency_source_details: T.nilable(T::Hash[Symbol, String]),
|
|
30
|
+
git_metadata_fetcher: T.nilable(Dependabot::GitMetadataFetcher)
|
|
30
31
|
)
|
|
31
32
|
.void
|
|
32
33
|
end
|
|
@@ -36,7 +37,8 @@ module Dependabot
|
|
|
36
37
|
ignored_versions: [],
|
|
37
38
|
raise_on_ignored: false,
|
|
38
39
|
consider_version_branches_pinned: false,
|
|
39
|
-
dependency_source_details: nil
|
|
40
|
+
dependency_source_details: nil,
|
|
41
|
+
git_metadata_fetcher: nil
|
|
40
42
|
)
|
|
41
43
|
@dependency = dependency
|
|
42
44
|
@credentials = credentials
|
|
@@ -44,6 +46,7 @@ module Dependabot
|
|
|
44
46
|
@raise_on_ignored = raise_on_ignored
|
|
45
47
|
@consider_version_branches_pinned = consider_version_branches_pinned
|
|
46
48
|
@dependency_source_details = dependency_source_details
|
|
49
|
+
@git_metadata_fetcher = git_metadata_fetcher
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
sig { returns(Dependabot::Dependency) }
|
|
@@ -79,7 +82,8 @@ module Dependabot
|
|
|
79
82
|
ignored_versions: ignored_versions,
|
|
80
83
|
raise_on_ignored: raise_on_ignored,
|
|
81
84
|
consider_version_branches_pinned: true,
|
|
82
|
-
dependency_source_details: source
|
|
85
|
+
dependency_source_details: source,
|
|
86
|
+
git_metadata_fetcher: @git_metadata_fetcher
|
|
83
87
|
)
|
|
84
88
|
end
|
|
85
89
|
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "sorbet-runtime"
|
|
7
|
+
|
|
8
|
+
require "dependabot/shared_helpers"
|
|
9
|
+
require "dependabot/command_helpers"
|
|
10
|
+
require "dependabot/credential"
|
|
11
|
+
require "dependabot/dependency_file"
|
|
12
|
+
require "dependabot/errors"
|
|
13
|
+
require "dependabot/logger"
|
|
14
|
+
require "dependabot/github_actions/constants"
|
|
15
|
+
require "dependabot/github_actions/lockfile/env"
|
|
16
|
+
require "dependabot/github_actions/lockfile/errors"
|
|
17
|
+
require "dependabot/github_actions/lockfile/reader"
|
|
18
|
+
|
|
19
|
+
module Dependabot
|
|
20
|
+
module GithubActions
|
|
21
|
+
module Lockfile
|
|
22
|
+
# Shells out to gh-actions-lock to regenerate actions.lock.
|
|
23
|
+
class CliEngine
|
|
24
|
+
extend T::Sig
|
|
25
|
+
|
|
26
|
+
JsonObject = T.type_alias { T::Hash[String, Object] }
|
|
27
|
+
FIXED_FINDING_CATEGORIES = T.let(%w(onboarding-required ref-changed stale).freeze, T::Array[String])
|
|
28
|
+
UNRESOLVABLE_CATEGORIES = T.let(%w(impostor-commit lockfile-forgery).freeze, T::Array[String])
|
|
29
|
+
|
|
30
|
+
sig { params(credentials: T::Array[Dependabot::Credential]).void }
|
|
31
|
+
def initialize(credentials)
|
|
32
|
+
@credentials = credentials
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Binary baked into the ecosystem image; falls back to PATH for local dev.
|
|
36
|
+
sig { returns(String) }
|
|
37
|
+
def self.binary_path
|
|
38
|
+
base = ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", nil)
|
|
39
|
+
base ? File.join(base, "github_actions", "bin", "gh-actions-lock") : "gh-actions-lock"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Re-pins workflows already tracked in the lockfile. `--no-onboard` refuses new
|
|
43
|
+
# workflows/actions (surfaced as onboarding-required skips); `--no-narrow`
|
|
44
|
+
# keeps the exact ref Dependabot wrote.
|
|
45
|
+
sig do
|
|
46
|
+
params(
|
|
47
|
+
workflow_files: T::Array[Dependabot::DependencyFile],
|
|
48
|
+
lockfile: Dependabot::DependencyFile,
|
|
49
|
+
workflow_paths: T::Array[String]
|
|
50
|
+
).returns(String)
|
|
51
|
+
end
|
|
52
|
+
def relock(workflow_files:, lockfile:, workflow_paths: workflow_files.map { |file| repo_path(file) })
|
|
53
|
+
in_repo(workflow_files, lockfile) do |dir|
|
|
54
|
+
args = %w(--no-onboard --no-narrow --no-interactive --json=findings) + workflow_paths
|
|
55
|
+
json, exit_status = run(dir, args)
|
|
56
|
+
|
|
57
|
+
skipped = onboarding_skips(json, lockfile)
|
|
58
|
+
log_skips(skipped)
|
|
59
|
+
|
|
60
|
+
# `findings` is the PRE-fix diagnosis; at exit 0 fix-mode already resolved
|
|
61
|
+
# them. Only exit 1 can carry a survivor (impostor/forgery or a skip).
|
|
62
|
+
raise_on_findings(json) if exit_status == 1
|
|
63
|
+
|
|
64
|
+
File.read(File.join(dir, LOCKFILE_PATH))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
|
71
|
+
attr_reader :credentials
|
|
72
|
+
|
|
73
|
+
sig do
|
|
74
|
+
type_parameters(:T)
|
|
75
|
+
.params(
|
|
76
|
+
workflow_files: T::Array[Dependabot::DependencyFile],
|
|
77
|
+
lockfile: T.nilable(Dependabot::DependencyFile),
|
|
78
|
+
blk: T.proc.params(dir: String).returns(T.type_parameter(:T))
|
|
79
|
+
)
|
|
80
|
+
.returns(T.type_parameter(:T))
|
|
81
|
+
end
|
|
82
|
+
def in_repo(workflow_files, lockfile, &blk)
|
|
83
|
+
SharedHelpers.in_a_temporary_directory do |dir|
|
|
84
|
+
(workflow_files + [lockfile].compact).each do |file|
|
|
85
|
+
path = File.join(dir, repo_path(file))
|
|
86
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
87
|
+
File.write(path, file.content)
|
|
88
|
+
end
|
|
89
|
+
blk.call(dir.to_s) # rubocop:disable Performance/RedundantBlockCall
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Repo-relative path (no leading slash) so the temp repo always mirrors the
|
|
94
|
+
# real repository layout regardless of the Dependabot directory config.
|
|
95
|
+
sig { params(file: Dependabot::DependencyFile).returns(String) }
|
|
96
|
+
def repo_path(file)
|
|
97
|
+
file.path.delete_prefix("/")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Invokes the binary and returns [parsed JSON, exit code]. Exit is tri-state:
|
|
101
|
+
# 0 = valid, 1 = blocking findings (still well-formed JSON on stdout, parse it),
|
|
102
|
+
# 2+ = tool failure (no usable JSON). JSON and exit are not interchangeable in
|
|
103
|
+
# fix-mode: findings/valid are PRE-fix, exit is POST-fix, so callers gate on exit.
|
|
104
|
+
sig { params(dir: String, args: T::Array[String]).returns([JsonObject, Integer]) }
|
|
105
|
+
def run(dir, args)
|
|
106
|
+
stdout, stderr, exit_status = invoke(dir, args)
|
|
107
|
+
raise EngineError, "gh-actions-lock failed (exit #{exit_status}): #{stderr.strip}" if exit_status > 1
|
|
108
|
+
|
|
109
|
+
json = case (parsed = JSON.parse(stdout))
|
|
110
|
+
when Hash then parsed
|
|
111
|
+
else raise EngineError, "gh-actions-lock emitted non-object JSON"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
[json, exit_status]
|
|
115
|
+
rescue JSON::ParserError => e
|
|
116
|
+
raise EngineError, "gh-actions-lock emitted unparseable JSON: #{e.message}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Runs the binary with no shell (argv array, no escaping) and returns
|
|
120
|
+
# [stdout, stderr, exit_status].
|
|
121
|
+
sig { params(dir: String, args: T::Array[String]).returns([String, String, Integer]) }
|
|
122
|
+
def invoke(dir, args)
|
|
123
|
+
env_cmd = [Env.build(credentials), self.class.binary_path, *args, { chdir: dir }]
|
|
124
|
+
stdout, stderr, process = CommandHelpers.capture3_with_timeout(env_cmd)
|
|
125
|
+
|
|
126
|
+
# A failed spawn comes back as a nil status, not an exception.
|
|
127
|
+
if process.nil?
|
|
128
|
+
raise EngineError,
|
|
129
|
+
"gh-actions-lock failed to start (#{self.class.binary_path}): #{stderr.to_s.strip}"
|
|
130
|
+
end
|
|
131
|
+
raise EngineError, "gh-actions-lock terminated by signal #{process.termsig}" if process.termsig
|
|
132
|
+
|
|
133
|
+
[stdout || "", stderr || "", process.exitstatus]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sig { params(json: JsonObject).void }
|
|
137
|
+
def raise_on_findings(json)
|
|
138
|
+
T.cast(Array(json["findings"]), T::Array[JsonObject]).each do |finding|
|
|
139
|
+
next unless finding["severity"].nil? || finding["severity"] == "error"
|
|
140
|
+
|
|
141
|
+
category = finding["category"].to_s
|
|
142
|
+
next if FIXED_FINDING_CATEGORIES.include?(category)
|
|
143
|
+
|
|
144
|
+
if UNRESOLVABLE_CATEGORIES.include?(category)
|
|
145
|
+
raise UnresolvableDependency.new(
|
|
146
|
+
(finding["dependency"] || "unknown").to_s,
|
|
147
|
+
(finding["detail"] || category).to_s
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
raise EngineError, "gh-actions-lock left an unhandled #{category.inspect} finding"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Partitions onboarding-required findings into genuine skips vs. a lock the
|
|
156
|
+
# engine could not read. The discriminator is the action, not the path: a
|
|
157
|
+
# finding is contradictory iff the lock already pins its `dependency` under its
|
|
158
|
+
# `workflow`. A finding with no `dependency` falls back to the path check.
|
|
159
|
+
sig do
|
|
160
|
+
params(json: JsonObject, lockfile: Dependabot::DependencyFile)
|
|
161
|
+
.returns(T::Array[String])
|
|
162
|
+
end
|
|
163
|
+
def onboarding_skips(json, lockfile)
|
|
164
|
+
findings = T.cast(Array(json["findings"]), T::Array[JsonObject])
|
|
165
|
+
onboarding = findings.select { |finding| finding["category"] == "onboarding-required" }
|
|
166
|
+
return [] if onboarding.empty?
|
|
167
|
+
|
|
168
|
+
reader = Reader.from_files([lockfile])
|
|
169
|
+
contradictory, strays = onboarding.partition { |finding| lock_contradicts?(reader, finding) }
|
|
170
|
+
|
|
171
|
+
raise_lockfile_unrecognized(contradictory) if contradictory.any?
|
|
172
|
+
|
|
173
|
+
strays.map { |finding| workflow_for(finding) }.uniq
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# True when an onboarding-required finding contradicts a lock we can read.
|
|
177
|
+
sig { params(reader: T.nilable(Reader), finding: JsonObject).returns(T::Boolean) }
|
|
178
|
+
def lock_contradicts?(reader, finding)
|
|
179
|
+
return false unless reader
|
|
180
|
+
|
|
181
|
+
action = finding["dependency"].to_s
|
|
182
|
+
workflow = workflow_for(finding)
|
|
183
|
+
return reader.pins_action?(workflow, action) unless action.empty?
|
|
184
|
+
|
|
185
|
+
reader.onboarded?(workflow)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
sig { params(finding: JsonObject).returns(String) }
|
|
189
|
+
def workflow_for(finding)
|
|
190
|
+
(finding["workflow"] || finding["dependency"] || "unknown").to_s
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The engine reported lock-tracked workflows as un-onboarded: it could not read
|
|
194
|
+
# the lock and treated it as empty. Fail with a lockfile error, not a crash.
|
|
195
|
+
sig { params(findings: T::Array[JsonObject]).void }
|
|
196
|
+
def raise_lockfile_unrecognized(findings)
|
|
197
|
+
paths = findings.map { |finding| workflow_for(finding) }
|
|
198
|
+
slice_keys = %w(workflow dependency category severity detail)
|
|
199
|
+
Dependabot.logger.debug(
|
|
200
|
+
"gh-actions-lock reported lockfile-tracked workflow(s) as un-onboarded " \
|
|
201
|
+
"(lock unreadable): #{findings.map { |f| f.slice(*slice_keys) }}"
|
|
202
|
+
)
|
|
203
|
+
raise Dependabot::DependencyFileNotParseable.new(
|
|
204
|
+
LOCKFILE_PATH,
|
|
205
|
+
"gh-actions-lock could not read #{LOCKFILE_PATH} as covering #{paths.join(', ')}, even " \
|
|
206
|
+
"though the lockfile tracks #{paths.length == 1 ? 'that workflow' : 'those workflows'}. " \
|
|
207
|
+
"The lockfile is likely malformed or unreadable by the engine: every dependencies entry " \
|
|
208
|
+
"must include #{Reader::REQUIRED_DEPENDENCY_KEYS.join(', ')}, or the engine silently " \
|
|
209
|
+
"treats the whole lockfile as empty. No changes were made."
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
sig { params(skipped: T::Array[String]).void }
|
|
214
|
+
def log_skips(skipped)
|
|
215
|
+
return if skipped.empty?
|
|
216
|
+
|
|
217
|
+
Dependabot.logger.info(
|
|
218
|
+
"gh-actions-lock skipped onboarding in #{skipped.size} workflow(s) (no-onboard is " \
|
|
219
|
+
"update's default; skipped actions remain untracked, not failed): #{skipped.join(', ')}"
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/credential"
|
|
7
|
+
require "dependabot/github_actions/constants"
|
|
8
|
+
|
|
9
|
+
module Dependabot
|
|
10
|
+
module GithubActions
|
|
11
|
+
module Lockfile
|
|
12
|
+
# Builds the subprocess environment for the gh-actions-lock engine. Hosted
|
|
13
|
+
# Dependabot is tokenless behind a MITM proxy that overwrites the auth header,
|
|
14
|
+
# while proxyless local runs hold the real github.com token in `credentials`.
|
|
15
|
+
module Env
|
|
16
|
+
extend T::Sig
|
|
17
|
+
|
|
18
|
+
# Placeholder satisfies go-gh's "token must be present" check in hosted mode
|
|
19
|
+
# where the proxy supplies real auth. Mirrors git's installation-token username.
|
|
20
|
+
DUMMY_TOKEN = T.let("x-access-token", String)
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
params(credentials: T::Array[Dependabot::Credential])
|
|
24
|
+
.returns(T::Hash[String, String])
|
|
25
|
+
end
|
|
26
|
+
def self.build(credentials)
|
|
27
|
+
env = {}
|
|
28
|
+
|
|
29
|
+
github_credential = github_dot_com_credential(credentials)
|
|
30
|
+
env["GH_TOKEN"] = github_credential&.fetch("password", nil) || DUMMY_TOKEN
|
|
31
|
+
env["GH_ACTIONS_LOCK_DEPENDABOT_PROXY"] = "1" unless github_credential
|
|
32
|
+
|
|
33
|
+
env
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Mirrors SharedHelpers.configure_git_to_use_https_with_credentials: prefer a
|
|
37
|
+
# deliberately-added token over an app installation token ("v1." prefix).
|
|
38
|
+
sig do
|
|
39
|
+
params(credentials: T::Array[Dependabot::Credential])
|
|
40
|
+
.returns(T.nilable(Dependabot::Credential))
|
|
41
|
+
end
|
|
42
|
+
def self.github_dot_com_credential(credentials)
|
|
43
|
+
candidates = credentials.select do |c|
|
|
44
|
+
c["type"] == "git_source" && c["host"] == GITHUB_COM && c["password"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
candidates.find { |c| !c["password"]&.start_with?("v1.") } || candidates.first
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/errors"
|
|
6
|
+
require "dependabot/github_actions/constants"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module GithubActions
|
|
10
|
+
module Lockfile
|
|
11
|
+
# Lockfile schema version this ecosystem does not understand. Hard error so we
|
|
12
|
+
# never half-write an incompatible lockfile.
|
|
13
|
+
class UnsupportedLockfileVersion < Dependabot::DependencyFileNotParseable
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
sig { returns(String) }
|
|
17
|
+
attr_reader :found, :supported
|
|
18
|
+
|
|
19
|
+
sig { params(found: String, supported: String).void }
|
|
20
|
+
def initialize(found, supported)
|
|
21
|
+
@found = found
|
|
22
|
+
@supported = supported
|
|
23
|
+
super(
|
|
24
|
+
LOCKFILE_PATH,
|
|
25
|
+
"Unsupported actions.lock version #{found.inspect}; " \
|
|
26
|
+
"this version of Dependabot supports #{supported.inspect}. " \
|
|
27
|
+
"Upgrade Dependabot or regenerate the lockfile with a compatible gh-actions-lock version."
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Engine could not resolve a dependency (often a transitive action the job
|
|
33
|
+
# token cannot reach). We refuse to emit a partial lockfile.
|
|
34
|
+
class UnresolvableDependency < Dependabot::DependencyFileNotResolvable
|
|
35
|
+
extend T::Sig
|
|
36
|
+
|
|
37
|
+
sig { returns(String) }
|
|
38
|
+
attr_reader :dependency, :detail
|
|
39
|
+
|
|
40
|
+
sig { params(dependency: String, detail: String).void }
|
|
41
|
+
def initialize(dependency, detail)
|
|
42
|
+
@dependency = dependency
|
|
43
|
+
@detail = detail
|
|
44
|
+
super("Could not resolve #{dependency}: #{detail}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Engine itself failed (binary missing, tool failure, unparseable JSON).
|
|
49
|
+
class EngineError < Dependabot::DependabotError; end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
require "dependabot/errors"
|
|
8
|
+
require "dependabot/dependency_file"
|
|
9
|
+
require "dependabot/github_actions/constants"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module GithubActions
|
|
13
|
+
module Lockfile
|
|
14
|
+
# Read-only parser for `.github/workflows/actions.lock`. Authoritative only for
|
|
15
|
+
# the workflow paths in its `workflows:` section, so onboarding is decided per
|
|
16
|
+
# path via {#onboarded?}, never repo-wide. Never writes: lockfile generation
|
|
17
|
+
# belongs to the gh-actions-lock engine.
|
|
18
|
+
class Reader
|
|
19
|
+
extend T::Sig
|
|
20
|
+
|
|
21
|
+
ParsedObject = T.type_alias { T::Hash[String, Object] }
|
|
22
|
+
|
|
23
|
+
# Every commit must carry an explicit hash-algorithm prefix.
|
|
24
|
+
ALGO_PREFIXES = T.let(%w(sha1- sha256-).freeze, T::Array[String])
|
|
25
|
+
|
|
26
|
+
# Keys gh-actions-lock requires on every `dependencies` entry. A missing key
|
|
27
|
+
# makes the engine silently discard the whole lockfile in memory and report
|
|
28
|
+
# every workflow as un-onboarded.
|
|
29
|
+
REQUIRED_DEPENDENCY_KEYS = T.let(%w(ref commit owner_id repo_id).freeze, T::Array[String])
|
|
30
|
+
|
|
31
|
+
sig { params(content: String).void }
|
|
32
|
+
def initialize(content)
|
|
33
|
+
@content = T.let(content, String)
|
|
34
|
+
@data = T.let(parse, ParsedObject)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns a Reader for the lockfile in a DependencyFile array, or nil when the
|
|
38
|
+
# repo has no lockfile (whole repo is on the legacy path).
|
|
39
|
+
sig { params(files: T::Array[Dependabot::DependencyFile]).returns(T.nilable(Reader)) }
|
|
40
|
+
def self.from_files(files)
|
|
41
|
+
file = files.find { |f| f.path.delete_prefix("/") == LOCKFILE_PATH }
|
|
42
|
+
return nil unless file
|
|
43
|
+
|
|
44
|
+
content = file.content
|
|
45
|
+
return nil if content.nil? || content.strip.empty?
|
|
46
|
+
|
|
47
|
+
new(content)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { returns(String) }
|
|
51
|
+
def version
|
|
52
|
+
@data["version"].to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
56
|
+
def onboarded?(path)
|
|
57
|
+
workflows.key?(path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# True when the lockfile already pins `action_ref` (an `owner/repo@ref`) under
|
|
61
|
+
# the given workflow path. Discriminates a legitimate new-action skip from a
|
|
62
|
+
# contradictory "lock read as empty" finding.
|
|
63
|
+
sig { params(path: String, action_ref: String).returns(T::Boolean) }
|
|
64
|
+
def pins_action?(path, action_ref)
|
|
65
|
+
return false if action_ref.empty?
|
|
66
|
+
|
|
67
|
+
Array(workflows[path]).include?(action_ref)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Asserts every `dependencies` entry carries {REQUIRED_DEPENDENCY_KEYS}. A
|
|
71
|
+
# missing key makes the engine silently treat the whole lockfile as empty.
|
|
72
|
+
# Deferred to the relock gate (not the constructor) so a malformed lock
|
|
73
|
+
# covering only untouched workflows never blocks a legacy regex update.
|
|
74
|
+
sig { void }
|
|
75
|
+
def validate_dependency_entries!
|
|
76
|
+
dependencies.each do |key, entry|
|
|
77
|
+
raise parse_error("dependency entry #{key.inspect} is not a mapping") unless entry.is_a?(Hash)
|
|
78
|
+
|
|
79
|
+
missing = REQUIRED_DEPENDENCY_KEYS.reject { |field| entry.key?(field) }
|
|
80
|
+
unless missing.empty?
|
|
81
|
+
raise parse_error(
|
|
82
|
+
"dependency entry #{key.inspect} is missing required field(s) #{missing.join(', ')}; " \
|
|
83
|
+
"gh-actions-lock requires #{REQUIRED_DEPENDENCY_KEYS.join(', ')} on every entry or it " \
|
|
84
|
+
"silently treats the whole lockfile as empty"
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
commit = entry["commit"].to_s
|
|
89
|
+
next if ALGO_PREFIXES.any? { |prefix| commit.start_with?(prefix) }
|
|
90
|
+
|
|
91
|
+
raise parse_error(
|
|
92
|
+
"dependency entry #{key.inspect} has commit #{commit.inspect} without a hash-algorithm prefix " \
|
|
93
|
+
"(expected one of #{ALGO_PREFIXES.join(', ')})"
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
sig { returns(ParsedObject) }
|
|
101
|
+
def workflows
|
|
102
|
+
wf = @data["workflows"]
|
|
103
|
+
wf.is_a?(Hash) ? wf : {}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { returns(ParsedObject) }
|
|
107
|
+
def dependencies
|
|
108
|
+
deps = @data["dependencies"]
|
|
109
|
+
deps.is_a?(Hash) ? deps : {}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig { returns(ParsedObject) }
|
|
113
|
+
def parse
|
|
114
|
+
parsed = YAML.safe_load(@content)
|
|
115
|
+
raise parse_error("lockfile is not a mapping") unless parsed.is_a?(Hash)
|
|
116
|
+
|
|
117
|
+
parsed
|
|
118
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias => e
|
|
119
|
+
raise parse_error(e.message)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
sig { params(message: String).returns(Dependabot::DependencyFileNotParseable) }
|
|
123
|
+
def parse_error(message)
|
|
124
|
+
Dependabot::DependencyFileNotParseable.new(LOCKFILE_PATH, message)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/github_actions/constants"
|
|
7
|
+
require "dependabot/github_actions/lockfile/errors"
|
|
8
|
+
|
|
9
|
+
module Dependabot
|
|
10
|
+
module GithubActions
|
|
11
|
+
module Lockfile
|
|
12
|
+
# Pre-1.0 lockfile schema revisions may be breaking, so only the explicitly
|
|
13
|
+
# supported version is safe to read and rewrite.
|
|
14
|
+
module VersionGate
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
sig { params(found: String).void }
|
|
18
|
+
def self.assert_supported!(found)
|
|
19
|
+
return if compatible?(found)
|
|
20
|
+
|
|
21
|
+
raise UnsupportedLockfileVersion.new(found, SUPPORTED_LOCKFILE_VERSION)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(found: String).returns(T::Boolean) }
|
|
25
|
+
def self.compatible?(found)
|
|
26
|
+
found == SUPPORTED_LOCKFILE_VERSION
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module Dependabot
|
|
7
|
+
module GithubActions
|
|
8
|
+
# Integration with the gh-actions-lock lockfile (`.github/workflows/actions.lock`),
|
|
9
|
+
# which is generated and owned by the upstream `gh-actions-lock` CLI. This namespace
|
|
10
|
+
# is the boundary: {Lockfile::Reader} parses an existing lock (read-only),
|
|
11
|
+
# {Lockfile::CliEngine} invokes the resolver/rewriter, {Lockfile::Env} builds
|
|
12
|
+
# the subprocess env, and {Lockfile::VersionGate} rejects unsupported schemas.
|
|
13
|
+
module Lockfile
|
|
14
|
+
extend T::Sig
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require "dependabot/github_actions/lockfile/errors"
|
|
20
|
+
require "dependabot/github_actions/lockfile/reader"
|
|
21
|
+
require "dependabot/github_actions/lockfile/version_gate"
|
|
22
|
+
require "dependabot/github_actions/lockfile/env"
|
|
23
|
+
require "dependabot/github_actions/lockfile/cli_engine"
|
|
@@ -31,7 +31,8 @@ module Dependabot
|
|
|
31
31
|
credentials: T::Array[Dependabot::Credential],
|
|
32
32
|
ignored_versions: T::Array[String],
|
|
33
33
|
raise_on_ignored: T::Boolean,
|
|
34
|
-
security_advisories: T::Array[Dependabot::SecurityAdvisory]
|
|
34
|
+
security_advisories: T::Array[Dependabot::SecurityAdvisory],
|
|
35
|
+
git_metadata_fetcher: T.nilable(Dependabot::GitMetadataFetcher)
|
|
35
36
|
).void
|
|
36
37
|
end
|
|
37
38
|
def initialize(
|
|
@@ -39,13 +40,15 @@ module Dependabot
|
|
|
39
40
|
credentials:,
|
|
40
41
|
ignored_versions: [],
|
|
41
42
|
raise_on_ignored: false,
|
|
42
|
-
security_advisories: []
|
|
43
|
+
security_advisories: [],
|
|
44
|
+
git_metadata_fetcher: nil
|
|
43
45
|
)
|
|
44
46
|
@dependency = dependency
|
|
45
47
|
@credentials = credentials
|
|
46
48
|
@raise_on_ignored = raise_on_ignored
|
|
47
49
|
@ignored_versions = ignored_versions
|
|
48
50
|
@security_advisories = security_advisories
|
|
51
|
+
@git_metadata_fetcher = git_metadata_fetcher
|
|
49
52
|
|
|
50
53
|
@git_helper = T.let(git_helper, Dependabot::GithubActions::Helpers::Githelper)
|
|
51
54
|
end
|
|
@@ -123,7 +126,10 @@ module Dependabot
|
|
|
123
126
|
ref = git_commit_checker.local_ref_for_latest_version_matching_existing_precision
|
|
124
127
|
return ref if ref && ref.fetch(:version) > current_version
|
|
125
128
|
|
|
126
|
-
git_commit_checker.local_ref_for_latest_version_lower_precision
|
|
129
|
+
lower_precision_ref = git_commit_checker.local_ref_for_latest_version_lower_precision
|
|
130
|
+
return ref if ref&.fetch(:version) == current_version
|
|
131
|
+
|
|
132
|
+
lower_precision_ref
|
|
127
133
|
end,
|
|
128
134
|
T.nilable(T::Hash[Symbol, T.untyped])
|
|
129
135
|
)
|
|
@@ -270,7 +276,8 @@ module Dependabot
|
|
|
270
276
|
ignored_versions: ignored_versions,
|
|
271
277
|
raise_on_ignored: raise_on_ignored,
|
|
272
278
|
consider_version_branches_pinned: false,
|
|
273
|
-
dependency_source_details: nil
|
|
279
|
+
dependency_source_details: nil,
|
|
280
|
+
git_metadata_fetcher: @git_metadata_fetcher
|
|
274
281
|
)
|
|
275
282
|
end
|
|
276
283
|
end
|
|
@@ -32,7 +32,8 @@ module Dependabot
|
|
|
32
32
|
security_advisories: T::Array[Dependabot::SecurityAdvisory],
|
|
33
33
|
raise_on_ignored: T::Boolean,
|
|
34
34
|
options: T::Hash[Symbol, T.untyped],
|
|
35
|
-
cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions)
|
|
35
|
+
cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions),
|
|
36
|
+
git_metadata_fetcher: T.nilable(Dependabot::GitMetadataFetcher)
|
|
36
37
|
).void
|
|
37
38
|
end
|
|
38
39
|
def initialize(
|
|
@@ -43,7 +44,8 @@ module Dependabot
|
|
|
43
44
|
security_advisories:,
|
|
44
45
|
raise_on_ignored:,
|
|
45
46
|
options: {},
|
|
46
|
-
cooldown_options: nil
|
|
47
|
+
cooldown_options: nil,
|
|
48
|
+
git_metadata_fetcher: nil
|
|
47
49
|
)
|
|
48
50
|
@dependency = dependency
|
|
49
51
|
@dependency_files = dependency_files
|
|
@@ -53,9 +55,19 @@ module Dependabot
|
|
|
53
55
|
@raise_on_ignored = raise_on_ignored
|
|
54
56
|
@options = options
|
|
55
57
|
@cooldown_options = cooldown_options
|
|
58
|
+
@git_metadata_fetcher = git_metadata_fetcher
|
|
56
59
|
|
|
57
60
|
@git_helper = T.let(git_helper, Dependabot::GithubActions::Helpers::Githelper)
|
|
58
|
-
super
|
|
61
|
+
super(
|
|
62
|
+
dependency: dependency,
|
|
63
|
+
dependency_files: dependency_files,
|
|
64
|
+
credentials: credentials,
|
|
65
|
+
ignored_versions: ignored_versions,
|
|
66
|
+
security_advisories: security_advisories,
|
|
67
|
+
raise_on_ignored: raise_on_ignored,
|
|
68
|
+
options: options,
|
|
69
|
+
cooldown_options: cooldown_options
|
|
70
|
+
)
|
|
59
71
|
end
|
|
60
72
|
|
|
61
73
|
sig { returns(Dependabot::Dependency) }
|
|
@@ -150,7 +162,8 @@ module Dependabot
|
|
|
150
162
|
credentials: credentials,
|
|
151
163
|
ignored_versions: ignored_versions,
|
|
152
164
|
raise_on_ignored: raise_on_ignored,
|
|
153
|
-
security_advisories: security_advisories
|
|
165
|
+
security_advisories: security_advisories,
|
|
166
|
+
git_metadata_fetcher: @git_metadata_fetcher
|
|
154
167
|
),
|
|
155
168
|
T.nilable(Dependabot::GithubActions::Package::PackageDetailsFetcher)
|
|
156
169
|
)
|
|
@@ -363,7 +376,8 @@ module Dependabot
|
|
|
363
376
|
ignored_versions: ignored_versions,
|
|
364
377
|
raise_on_ignored: raise_on_ignored,
|
|
365
378
|
consider_version_branches_pinned: false,
|
|
366
|
-
dependency_source_details: nil
|
|
379
|
+
dependency_source_details: nil,
|
|
380
|
+
git_metadata_fetcher: @git_metadata_fetcher
|
|
367
381
|
)
|
|
368
382
|
end
|
|
369
383
|
end
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
5
|
|
|
6
6
|
require "dependabot/errors"
|
|
7
|
+
require "dependabot/git_metadata_fetcher"
|
|
7
8
|
require "dependabot/github_actions/constants"
|
|
8
9
|
require "dependabot/github_actions/containing_branch_finder"
|
|
10
|
+
require "dependabot/github_actions/lockfile/reader"
|
|
9
11
|
require "dependabot/github_actions/requirement"
|
|
10
12
|
require "dependabot/github_actions/version"
|
|
11
13
|
require "dependabot/update_checkers"
|
|
@@ -17,6 +19,8 @@ module Dependabot
|
|
|
17
19
|
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
|
18
20
|
extend T::Sig
|
|
19
21
|
|
|
22
|
+
GitSource = T.type_alias { T::Hash[Symbol, String] }
|
|
23
|
+
|
|
20
24
|
require_relative "update_checker/latest_version_finder"
|
|
21
25
|
|
|
22
26
|
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
@@ -53,11 +57,16 @@ module Dependabot
|
|
|
53
57
|
)
|
|
54
58
|
end
|
|
55
59
|
|
|
60
|
+
sig { override.returns(T::Boolean) }
|
|
61
|
+
def up_to_date?
|
|
62
|
+
super && !onboarded_requirements_changed?
|
|
63
|
+
end
|
|
64
|
+
|
|
56
65
|
sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
57
66
|
def updated_requirements
|
|
58
67
|
updated_reqs = dependency.requirements.map do |req|
|
|
59
|
-
source = req
|
|
60
|
-
updated = updated_ref(source)
|
|
68
|
+
source = T.cast(req.source, GitSource)
|
|
69
|
+
updated = updated_ref(source, onboarded: onboarded_requirement?(req))
|
|
61
70
|
next req unless updated
|
|
62
71
|
|
|
63
72
|
current = source[:ref]
|
|
@@ -65,8 +74,8 @@ module Dependabot
|
|
|
65
74
|
# Maintain a short git hash only if it matches the latest
|
|
66
75
|
if req[:type] == "git" &&
|
|
67
76
|
git_commit_checker.ref_looks_like_commit_sha?(updated) &&
|
|
68
|
-
git_commit_checker.ref_looks_like_commit_sha?(current) &&
|
|
69
|
-
updated.start_with?(current)
|
|
77
|
+
git_commit_checker.ref_looks_like_commit_sha?(T.must(current)) &&
|
|
78
|
+
updated.start_with?(T.must(current))
|
|
70
79
|
next req
|
|
71
80
|
end
|
|
72
81
|
|
|
@@ -78,23 +87,118 @@ module Dependabot
|
|
|
78
87
|
|
|
79
88
|
private
|
|
80
89
|
|
|
90
|
+
sig { params(requirements_to_unlock: T.nilable(Symbol)).returns(T::Boolean) }
|
|
91
|
+
def numeric_version_can_update?(requirements_to_unlock:)
|
|
92
|
+
return true if super
|
|
93
|
+
return false unless requirements_to_unlock == :own
|
|
94
|
+
|
|
95
|
+
onboarded_requirements_changed?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
sig { returns(T::Boolean) }
|
|
99
|
+
def onboarded_requirements_changed?
|
|
100
|
+
dependency.requirements.zip(updated_requirements).any? do |current, updated|
|
|
101
|
+
onboarded_requirement?(current) && current != updated
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A requirement is "onboarded" when the repo carries an `actions.lock` that is
|
|
106
|
+
# authoritative for the requirement's workflow. Only onboarded requirements get
|
|
107
|
+
# per-source precision selection; everything else flows through the combined
|
|
108
|
+
# finder exactly as before, so non-onboarded repos see byte-identical behavior.
|
|
109
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(T::Boolean) }
|
|
110
|
+
def onboarded_requirement?(req)
|
|
111
|
+
file = dependency_files.find { |f| f.name == req.file }
|
|
112
|
+
return false unless file
|
|
113
|
+
return false unless File.dirname(file.path.delete_prefix("/")) == WORKFLOW_DIRECTORY
|
|
114
|
+
|
|
115
|
+
reader = lockfile_reader
|
|
116
|
+
return false unless reader
|
|
117
|
+
|
|
118
|
+
reader.onboarded?(file.path.delete_prefix("/"))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
sig { returns(T.nilable(Dependabot::GithubActions::Lockfile::Reader)) }
|
|
122
|
+
def lockfile_reader
|
|
123
|
+
return @lockfile_reader if defined?(@lockfile_reader)
|
|
124
|
+
|
|
125
|
+
@lockfile_reader = T.let(
|
|
126
|
+
Dependabot::GithubActions::Lockfile::Reader.from_files(dependency_files),
|
|
127
|
+
T.nilable(Dependabot::GithubActions::Lockfile::Reader)
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
81
131
|
sig { returns(T.nilable(Dependabot::GithubActions::UpdateChecker::LatestVersionFinder)) }
|
|
82
132
|
def latest_version_finder
|
|
83
133
|
@latest_version_finder ||=
|
|
84
134
|
T.let(
|
|
85
|
-
|
|
86
|
-
dependency: dependency,
|
|
87
|
-
credentials: credentials,
|
|
88
|
-
dependency_files: dependency_files,
|
|
89
|
-
security_advisories: security_advisories,
|
|
90
|
-
ignored_versions: ignored_versions,
|
|
91
|
-
raise_on_ignored: raise_on_ignored,
|
|
92
|
-
cooldown_options: update_cooldown
|
|
93
|
-
),
|
|
135
|
+
build_latest_version_finder(dependency),
|
|
94
136
|
T.nilable(Dependabot::GithubActions::UpdateChecker::LatestVersionFinder)
|
|
95
137
|
)
|
|
96
138
|
end
|
|
97
139
|
|
|
140
|
+
# A finder scoped to a single requirement's source ref, so version selection
|
|
141
|
+
# precision-matches THAT ref (e.g. `v4.3.1` → latest 3-segment tag) instead of
|
|
142
|
+
# the combined dependency version (the lower of all refs, which flattens every
|
|
143
|
+
# requirement to the coarsest precision). Finders remain scoped per ref, but share
|
|
144
|
+
# one repository metadata fetcher. Falls back to the combined finder for sources
|
|
145
|
+
# whose ref is not a version (SHA / branch), where precision has no meaning.
|
|
146
|
+
sig { params(source: T.nilable(GitSource)).returns(LatestVersionFinder) }
|
|
147
|
+
def latest_version_finder_for(source)
|
|
148
|
+
ref = source&.fetch(:ref, nil)
|
|
149
|
+
return T.must(latest_version_finder) unless ref && version_class.correct?(ref)
|
|
150
|
+
|
|
151
|
+
@latest_version_finder_for ||= T.let({}, T.nilable(T::Hash[String, LatestVersionFinder]))
|
|
152
|
+
@latest_version_finder_for[ref] ||= build_latest_version_finder(per_source_dependency(source, ref))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
sig { params(dep: Dependabot::Dependency).returns(LatestVersionFinder) }
|
|
156
|
+
def build_latest_version_finder(dep)
|
|
157
|
+
LatestVersionFinder.new(
|
|
158
|
+
dependency: dep,
|
|
159
|
+
credentials: credentials,
|
|
160
|
+
dependency_files: dependency_files,
|
|
161
|
+
security_advisories: security_advisories,
|
|
162
|
+
ignored_versions: ignored_versions,
|
|
163
|
+
raise_on_ignored: raise_on_ignored,
|
|
164
|
+
cooldown_options: update_cooldown,
|
|
165
|
+
git_metadata_fetcher: git_metadata_fetcher
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
sig { returns(Dependabot::GitMetadataFetcher) }
|
|
170
|
+
def git_metadata_fetcher
|
|
171
|
+
@git_metadata_fetcher ||= T.let(
|
|
172
|
+
Dependabot::GitMetadataFetcher.new(
|
|
173
|
+
url: T.must(
|
|
174
|
+
dependency.requirements.filter_map do |requirement|
|
|
175
|
+
T.cast(requirement.source, T.nilable(GitSource))&.fetch(:url, nil)
|
|
176
|
+
end.first
|
|
177
|
+
),
|
|
178
|
+
credentials: credentials
|
|
179
|
+
),
|
|
180
|
+
T.nilable(Dependabot::GitMetadataFetcher)
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# A synthetic single-requirement dependency whose version mirrors the precision
|
|
185
|
+
# of `source[:ref]`. The downstream precision machinery keys entirely off
|
|
186
|
+
# `dependency.version`, so this is what makes per-source precision selection work
|
|
187
|
+
# without touching the shared combined dependency reported up to the rest of the
|
|
188
|
+
# update.
|
|
189
|
+
sig do
|
|
190
|
+
params(source: GitSource, ref: String)
|
|
191
|
+
.returns(Dependabot::Dependency)
|
|
192
|
+
end
|
|
193
|
+
def per_source_dependency(source, ref)
|
|
194
|
+
Dependabot::Dependency.new(
|
|
195
|
+
name: dependency.name,
|
|
196
|
+
version: Dependabot::GithubActions::Version.remove_leading_v(ref).to_s,
|
|
197
|
+
requirements: [{ requirement: nil, groups: [], source: source, file: nil, metadata: {} }],
|
|
198
|
+
package_manager: dependency.package_manager
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
98
202
|
sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
|
|
99
203
|
def active_advisories
|
|
100
204
|
security_advisories.select do |advisory|
|
|
@@ -143,12 +247,17 @@ module Dependabot
|
|
|
143
247
|
)
|
|
144
248
|
end
|
|
145
249
|
|
|
146
|
-
sig
|
|
147
|
-
|
|
250
|
+
sig do
|
|
251
|
+
params(source: T.nilable(GitSource), onboarded: T::Boolean)
|
|
252
|
+
.returns(T.nilable(String))
|
|
253
|
+
end
|
|
254
|
+
def updated_ref(source, onboarded: false)
|
|
148
255
|
# TODO: Support Docker sources
|
|
149
256
|
return unless git_commit_checker.git_dependency?
|
|
150
257
|
|
|
151
|
-
|
|
258
|
+
finder = onboarded ? latest_version_finder_for(source) : T.must(latest_version_finder)
|
|
259
|
+
|
|
260
|
+
if vulnerable? && (new_tag = finder.lowest_security_fix_release)
|
|
152
261
|
return new_tag.fetch(:tag)
|
|
153
262
|
end
|
|
154
263
|
|
|
@@ -156,13 +265,13 @@ module Dependabot
|
|
|
156
265
|
|
|
157
266
|
# Return the git tag if updating a pinned version
|
|
158
267
|
if source_git_commit_checker.pinned_ref_looks_like_version? &&
|
|
159
|
-
(new_tag =
|
|
268
|
+
(new_tag = finder.latest_version_tag_respecting_cooldown)
|
|
160
269
|
return new_tag.fetch(:tag)
|
|
161
270
|
end
|
|
162
271
|
|
|
163
272
|
# Return the pinned git commit if one is available
|
|
164
273
|
if source_git_commit_checker.pinned_ref_looks_like_commit_sha? &&
|
|
165
|
-
(new_commit_sha = latest_commit_sha(source_git_commit_checker))
|
|
274
|
+
(new_commit_sha = latest_commit_sha(source_git_commit_checker, finder))
|
|
166
275
|
return new_commit_sha
|
|
167
276
|
end
|
|
168
277
|
|
|
@@ -170,17 +279,20 @@ module Dependabot
|
|
|
170
279
|
nil
|
|
171
280
|
end
|
|
172
281
|
|
|
173
|
-
sig
|
|
174
|
-
|
|
175
|
-
|
|
282
|
+
sig do
|
|
283
|
+
params(source_checker: Dependabot::GitCommitChecker, finder: LatestVersionFinder)
|
|
284
|
+
.returns(T.nilable(String))
|
|
285
|
+
end
|
|
286
|
+
def latest_commit_sha(source_checker, finder)
|
|
287
|
+
latest_tag = finder.latest_version_tag
|
|
176
288
|
return unless latest_tag
|
|
177
289
|
|
|
178
290
|
if source_checker.local_tag_for_pinned_sha
|
|
179
|
-
new_tag =
|
|
291
|
+
new_tag = finder.latest_version_tag_respecting_cooldown
|
|
180
292
|
new_tag&.fetch(:commit_sha)
|
|
181
293
|
else
|
|
182
294
|
# Keep SHA rewrites aligned with the checker decision (including cooldown filtering).
|
|
183
|
-
latest =
|
|
295
|
+
latest = finder.latest_release_version
|
|
184
296
|
latest.is_a?(String) ? latest : latest_commit_for_pinned_ref
|
|
185
297
|
end
|
|
186
298
|
end
|
|
@@ -198,7 +310,8 @@ module Dependabot
|
|
|
198
310
|
ignored_versions: ignored_versions,
|
|
199
311
|
raise_on_ignored: raise_on_ignored,
|
|
200
312
|
consider_version_branches_pinned: false,
|
|
201
|
-
dependency_source_details: nil
|
|
313
|
+
dependency_source_details: nil,
|
|
314
|
+
git_metadata_fetcher: git_metadata_fetcher
|
|
202
315
|
)
|
|
203
316
|
end
|
|
204
317
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-github_actions
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.390.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.390.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.390.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -248,6 +248,12 @@ files:
|
|
|
248
248
|
- lib/dependabot/github_actions/file_parser.rb
|
|
249
249
|
- lib/dependabot/github_actions/file_updater.rb
|
|
250
250
|
- lib/dependabot/github_actions/helpers.rb
|
|
251
|
+
- lib/dependabot/github_actions/lockfile.rb
|
|
252
|
+
- lib/dependabot/github_actions/lockfile/cli_engine.rb
|
|
253
|
+
- lib/dependabot/github_actions/lockfile/env.rb
|
|
254
|
+
- lib/dependabot/github_actions/lockfile/errors.rb
|
|
255
|
+
- lib/dependabot/github_actions/lockfile/reader.rb
|
|
256
|
+
- lib/dependabot/github_actions/lockfile/version_gate.rb
|
|
251
257
|
- lib/dependabot/github_actions/metadata_finder.rb
|
|
252
258
|
- lib/dependabot/github_actions/package/package_details_fetcher.rb
|
|
253
259
|
- lib/dependabot/github_actions/package_manager.rb
|
|
@@ -260,7 +266,7 @@ licenses:
|
|
|
260
266
|
- MIT
|
|
261
267
|
metadata:
|
|
262
268
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
263
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
269
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.390.0
|
|
264
270
|
rdoc_options: []
|
|
265
271
|
require_paths:
|
|
266
272
|
- lib
|