dependabot-github_actions 0.400.0 → 0.401.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 +4 -2
- data/lib/dependabot/github_actions/file_fetcher.rb +77 -6
- data/lib/dependabot/github_actions/file_parser.rb +32 -1
- data/lib/dependabot/github_actions/file_updater.rb +74 -4
- data/lib/dependabot/github_actions/lockfile/cli_engine.rb +46 -24
- data/lib/dependabot/github_actions/lockfile/env.rb +9 -7
- data/lib/dependabot/github_actions/lockfile/errors.rb +1 -1
- data/lib/dependabot/github_actions/lockfile/reader.rb +17 -0
- data/lib/dependabot/github_actions/lockfile/version_gate.rb +3 -3
- 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: f52dcc4ffca3fffd85efa133c46ee31fee800ddca84ed787876441ec34414b47
|
|
4
|
+
data.tar.gz: a623cf219e7f0b21463f6e3345cccce0c30773340695adf102cbf332d018f163
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eabbda950f81fdeef7ec92d5865b67a467beaf70652ca53123a740d1ad1346b4e3a624b7542da3336e3a3a9e3daef68da641930ae034e903baf1620afd08b7b9
|
|
7
|
+
data.tar.gz: bfd97e4e98fd61922cb6b8034dc3160ccbb2d1924174f14431650e5b82d9291473f63f61c17ca13427f78a32f4e389df1f4c699b927a6acfb9f13edee67c7f35
|
|
@@ -23,6 +23,7 @@ module Dependabot
|
|
|
23
23
|
MANIFEST_FILE_YML = "action.yml"
|
|
24
24
|
# The name of the manifest file
|
|
25
25
|
MANIFEST_FILE_YAML = "action.yaml"
|
|
26
|
+
ACTION_FILE_NAMES = T.let([MANIFEST_FILE_YML, MANIFEST_FILE_YAML].freeze, T::Array[String])
|
|
26
27
|
# The pattern to match any .yml or .yaml file
|
|
27
28
|
ANYTHING_YML = "<anything>.yml"
|
|
28
29
|
# The path to the workflow directory
|
|
@@ -34,8 +35,9 @@ module Dependabot
|
|
|
34
35
|
LOCKFILE_NAME = "actions.lock"
|
|
35
36
|
# The repo-relative path to the Actions lockfile
|
|
36
37
|
LOCKFILE_PATH = T.let("#{WORKFLOW_DIRECTORY}/#{LOCKFILE_NAME}".freeze, String)
|
|
37
|
-
#
|
|
38
|
-
|
|
38
|
+
# Lockfile schema versions this ecosystem understands. gh-actions-lock migrates
|
|
39
|
+
# v0.0.2 input and always writes v0.0.3.
|
|
40
|
+
SUPPORTED_LOCKFILE_VERSIONS = %w(v0.0.2 v0.0.3).freeze
|
|
39
41
|
|
|
40
42
|
OWNER_KEY = "owner"
|
|
41
43
|
REPO_KEY = "repo"
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "pathname"
|
|
4
5
|
require "sorbet-runtime"
|
|
5
6
|
|
|
6
7
|
require "dependabot/file_fetchers"
|
|
7
8
|
require "dependabot/file_fetchers/base"
|
|
8
9
|
require "dependabot/experiments"
|
|
9
10
|
require "dependabot/github_actions/constants"
|
|
11
|
+
require "dependabot/github_actions/workflow_file"
|
|
10
12
|
|
|
11
13
|
module Dependabot
|
|
12
14
|
module GithubActions
|
|
@@ -50,7 +52,7 @@ module Dependabot
|
|
|
50
52
|
# and never activates the ecosystem on its own. Relocking also requires
|
|
51
53
|
# every workflow to be materialized, so retain the workflow-only path if
|
|
52
54
|
# an invalidly encoded workflow had to be omitted.
|
|
53
|
-
fetched_files +=
|
|
55
|
+
fetched_files += relock_files if incorrectly_encoded_workflow_files.empty?
|
|
54
56
|
return fetched_files
|
|
55
57
|
end
|
|
56
58
|
|
|
@@ -76,15 +78,84 @@ module Dependabot
|
|
|
76
78
|
|
|
77
79
|
private
|
|
78
80
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
+
# The lockfile plus everything the engine needs to relock it, or nothing when
|
|
82
|
+
# a workflow it would read is invalidly encoded.
|
|
83
|
+
sig { returns(T::Array[DependencyFile]) }
|
|
84
|
+
def relock_files
|
|
85
|
+
lockfile = actions_lockfile
|
|
86
|
+
return [] unless lockfile && repository_workflow_files.all? { |f| f.content&.valid_encoding? }
|
|
87
|
+
|
|
88
|
+
[lockfile] + repository_workflow_files + self_repository_action_files
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Fetches the canonical `.github/workflows/actions.lock`. Composite-action
|
|
92
|
+
# directories cannot own a lockfile, but `$/` workflows reaching them do.
|
|
81
93
|
sig { returns(T.nilable(DependencyFile)) }
|
|
82
94
|
def actions_lockfile
|
|
83
95
|
return unless Dependabot::Experiments.enabled?(:github_actions_lockfile)
|
|
84
|
-
return unless source.hostname == GITHUB_COM
|
|
85
|
-
|
|
96
|
+
return unless source.hostname == GITHUB_COM || source.hostname.end_with?(".ghe.com")
|
|
97
|
+
|
|
98
|
+
fetch_file_if_present(relative_to_directory(LOCKFILE_PATH))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Repository workflows a composite-action job needs to relock the lock. Support
|
|
102
|
+
# files only: the composite job must never bump the workflows' own refs.
|
|
103
|
+
sig { returns(T::Array[DependencyFile]) }
|
|
104
|
+
def repository_workflow_files
|
|
105
|
+
return [] if ["/", "/#{WORKFLOW_DIRECTORY}"].include?(File.join("/", directory))
|
|
106
|
+
|
|
107
|
+
@repository_workflow_files ||= T.let(
|
|
108
|
+
begin
|
|
109
|
+
dir = relative_to_directory(WORKFLOW_DIRECTORY)
|
|
110
|
+
repo_contents(dir: dir, raise_errors: false)
|
|
111
|
+
.select { |f| f.type == "file" && f.name.match?(MANIFEST_FILE_PATTERN) }
|
|
112
|
+
.map { |f| fetch_file_from_host(File.join(dir, f.name)) }
|
|
113
|
+
.each { |f| f.support_file = true }
|
|
114
|
+
end,
|
|
115
|
+
T.nilable(T::Array[DependencyFile])
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { params(repo_path: String).returns(String) }
|
|
120
|
+
def relative_to_directory(repo_path)
|
|
121
|
+
Pathname.new(File.join("/", repo_path)).relative_path_from(File.join("/", directory)).to_s
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# `$/` composites can live anywhere in the repo and can call each other, so
|
|
125
|
+
# relocking needs every action definition, at its repo-relative path.
|
|
126
|
+
sig { returns(T::Array[DependencyFile]) }
|
|
127
|
+
def self_repository_action_files
|
|
128
|
+
fetched = correctly_encoded_workflow_files + repository_workflow_files
|
|
129
|
+
return [] unless fetched.any? { |f| self_repository_uses?(f) }
|
|
130
|
+
|
|
131
|
+
# One recursive tree call. A truncated tree (>100k entries) may miss composites,
|
|
132
|
+
# which the engine then reports as an EngineError.
|
|
133
|
+
repo_action_paths
|
|
134
|
+
.reject { |path| fetched.any? { |f| f.path == path } }
|
|
135
|
+
.map do |path|
|
|
136
|
+
file = fetch_file_from_host(relative_to_directory(path))
|
|
137
|
+
file.support_file = true
|
|
138
|
+
file
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
sig { params(file: DependencyFile).returns(T::Boolean) }
|
|
143
|
+
def self_repository_uses?(file)
|
|
144
|
+
content = file.content.to_s
|
|
145
|
+
return false unless content.include?("$/")
|
|
146
|
+
|
|
147
|
+
WorkflowFile.new(content).uses_declarations.any? { |declaration| declaration.value.start_with?("$/") }
|
|
148
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
|
|
149
|
+
false
|
|
150
|
+
end
|
|
86
151
|
|
|
87
|
-
|
|
152
|
+
sig { returns(T::Array[String]) }
|
|
153
|
+
def repo_action_paths
|
|
154
|
+
tree = github_client.tree(repo, T.must(commit), recursive: true)
|
|
155
|
+
T.let(tree[:tree], T::Array[Sawyer::Resource])
|
|
156
|
+
.select { |entry| T.let(entry[:type], String) == "blob" }
|
|
157
|
+
.map { |entry| T.let("/#{entry[:path]}", String) }
|
|
158
|
+
.select { |path| ACTION_FILE_NAMES.include?(File.basename(path)) }
|
|
88
159
|
end
|
|
89
160
|
|
|
90
161
|
sig { returns(T::Array[DependencyFile]) }
|
|
@@ -9,6 +9,7 @@ require "dependabot/errors"
|
|
|
9
9
|
require "dependabot/file_parsers"
|
|
10
10
|
require "dependabot/file_parsers/base"
|
|
11
11
|
require "dependabot/github_actions/constants"
|
|
12
|
+
require "dependabot/github_actions/lockfile/reader"
|
|
12
13
|
require "dependabot/github_actions/version"
|
|
13
14
|
require "dependabot/github_actions/package_manager"
|
|
14
15
|
require "dependabot/github_actions/workflow_file"
|
|
@@ -137,6 +138,9 @@ module Dependabot
|
|
|
137
138
|
).returns(Dependabot::Dependency)
|
|
138
139
|
end
|
|
139
140
|
def build_github_dependency(file, string, metadata)
|
|
141
|
+
locked = locked_hostname(string)
|
|
142
|
+
return github_dependency(file, string, locked, metadata) if locked
|
|
143
|
+
|
|
140
144
|
unless source&.hostname == GITHUB_COM
|
|
141
145
|
dep = github_dependency(file, string, T.must(source).hostname, metadata)
|
|
142
146
|
git_checker = Dependabot::GitCommitChecker.new(dependency: dep, credentials: credentials)
|
|
@@ -193,9 +197,36 @@ module Dependabot
|
|
|
193
197
|
)
|
|
194
198
|
end
|
|
195
199
|
|
|
200
|
+
# A lockfile pin already names its host, so trust it over probing the tenant:
|
|
201
|
+
# a tenant blip must not silently rebind an in-tenant action to github.com.
|
|
202
|
+
sig { params(string: String).returns(T.nilable(String)) }
|
|
203
|
+
def locked_hostname(string)
|
|
204
|
+
reader = lockfile_reader
|
|
205
|
+
return unless reader
|
|
206
|
+
|
|
207
|
+
details = T.must(string.match(GITHUB_REPO_REFERENCE)).named_captures
|
|
208
|
+
action_ref = "#{details.fetch(OWNER_KEY)}/#{details.fetch(REPO_KEY)}@#{details.fetch(REF_KEY)}"
|
|
209
|
+
reader.pinned_hostname(action_ref, home: source&.hostname || GITHUB_COM)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# A malformed lock must not block parsing; the updater gates it at relock.
|
|
213
|
+
sig { returns(T.nilable(Lockfile::Reader)) }
|
|
214
|
+
def lockfile_reader
|
|
215
|
+
return @lockfile_reader if defined?(@lockfile_reader)
|
|
216
|
+
|
|
217
|
+
@lockfile_reader = T.let(
|
|
218
|
+
begin
|
|
219
|
+
Lockfile::Reader.from_files(dependency_files)
|
|
220
|
+
rescue Dependabot::DependencyFileNotParseable
|
|
221
|
+
nil
|
|
222
|
+
end,
|
|
223
|
+
T.nilable(Lockfile::Reader)
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
|
|
196
227
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
197
228
|
def workflow_files
|
|
198
|
-
dependency_files.reject { |file| file.path.delete_prefix("/") == LOCKFILE_PATH }
|
|
229
|
+
dependency_files.reject { |file| file.support_file? || file.path.delete_prefix("/") == LOCKFILE_PATH }
|
|
199
230
|
end
|
|
200
231
|
|
|
201
232
|
sig { override.void }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "pathname"
|
|
4
5
|
require "sorbet-runtime"
|
|
5
6
|
|
|
6
7
|
require "dependabot/errors"
|
|
@@ -63,16 +64,18 @@ module Dependabot
|
|
|
63
64
|
.returns(T::Array[Dependabot::DependencyFile])
|
|
64
65
|
end
|
|
65
66
|
def relocked_files(updated_workflow_files)
|
|
66
|
-
changed_repository_workflows = changed_workflow_files.
|
|
67
|
+
changed_repository_workflows, changed_actions = changed_workflow_files.partition do |file|
|
|
67
68
|
File.dirname(repo_relative_path(file)) == WORKFLOW_DIRECTORY
|
|
68
69
|
end
|
|
69
|
-
|
|
70
|
+
# A bumped composite changes the lock entries of every workflow reaching it.
|
|
71
|
+
affected_workflows = changed_repository_workflows | workflows_reaching(changed_actions)
|
|
72
|
+
return [] if affected_workflows.empty?
|
|
70
73
|
|
|
71
74
|
lock = lockfile
|
|
72
75
|
reader = lockfile_reader
|
|
73
76
|
return [] unless lock && reader
|
|
74
77
|
|
|
75
|
-
changed_onboarded =
|
|
78
|
+
changed_onboarded = affected_workflows.select { |f| reader.onboarded?(repo_relative_path(f)) }
|
|
76
79
|
return [] if changed_onboarded.empty?
|
|
77
80
|
|
|
78
81
|
# Gate only once the lock is authoritative for a workflow we're changing, so an
|
|
@@ -82,15 +85,76 @@ module Dependabot
|
|
|
82
85
|
|
|
83
86
|
# Materialize the full onboarded closure so the lock remains intact, but fix
|
|
84
87
|
# only changed workflows so unrelated refs are not touched.
|
|
85
|
-
content = Lockfile::CliEngine.new(credentials).relock(
|
|
88
|
+
content = Lockfile::CliEngine.new(credentials, hostname: source_hostname).relock(
|
|
86
89
|
workflow_files: rewritten_onboarded_workflow_files(reader, updated_workflow_files),
|
|
87
90
|
lockfile: lock,
|
|
91
|
+
action_files: rewritten_action_files(updated_workflow_files),
|
|
88
92
|
workflow_paths: changed_onboarded.map { |file| repo_relative_path(file) }
|
|
89
93
|
)
|
|
90
94
|
|
|
91
95
|
[updated_file(file: lock, content: content)]
|
|
92
96
|
end
|
|
93
97
|
|
|
98
|
+
# Every action definition, with a bumped composite's rewritten content.
|
|
99
|
+
sig do
|
|
100
|
+
params(updated_workflow_files: T::Array[Dependabot::DependencyFile])
|
|
101
|
+
.returns(T::Array[Dependabot::DependencyFile])
|
|
102
|
+
end
|
|
103
|
+
def rewritten_action_files(updated_workflow_files)
|
|
104
|
+
updated_by_path = updated_workflow_files.to_h { |file| [repo_relative_path(file), file] }
|
|
105
|
+
dependency_files
|
|
106
|
+
.select { |file| ACTION_FILE_NAMES.include?(File.basename(file.name)) }
|
|
107
|
+
.map { |file| updated_by_path.fetch(repo_relative_path(file), file) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Repository workflows whose step-level `$/` chain (through nested composites)
|
|
111
|
+
# reaches a changed action. Only `$/` matters: gh-actions-lock refuses
|
|
112
|
+
# `./` refs in locked workflows.
|
|
113
|
+
sig do
|
|
114
|
+
params(changed_actions: T::Array[Dependabot::DependencyFile])
|
|
115
|
+
.returns(T::Array[Dependabot::DependencyFile])
|
|
116
|
+
end
|
|
117
|
+
def workflows_reaching(changed_actions)
|
|
118
|
+
return [] if changed_actions.empty?
|
|
119
|
+
|
|
120
|
+
targets = changed_actions.map { |file| repo_relative_path(file) }
|
|
121
|
+
dependency_files.select do |file|
|
|
122
|
+
File.dirname(repo_relative_path(file)) == WORKFLOW_DIRECTORY && !lockfile?(file) &&
|
|
123
|
+
reaches?(file, targets, Set.new)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
sig do
|
|
128
|
+
params(file: Dependabot::DependencyFile, targets: T::Array[String], seen: T::Set[String])
|
|
129
|
+
.returns(T::Boolean)
|
|
130
|
+
end
|
|
131
|
+
def reaches?(file, targets, seen)
|
|
132
|
+
self_repository_files(file).any? do |target|
|
|
133
|
+
path = repo_relative_path(target)
|
|
134
|
+
seen.add?(path) && (targets.include?(path) || reaches?(target, targets, seen))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Action definitions a workflow or composite references via step-level `$/`.
|
|
139
|
+
# Job-level `$/` reusable workflows own their lock entries, so they are not followed.
|
|
140
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Array[Dependabot::DependencyFile]) }
|
|
141
|
+
def self_repository_files(file)
|
|
142
|
+
content = file.content.to_s
|
|
143
|
+
return [] unless content.include?("$/")
|
|
144
|
+
|
|
145
|
+
WorkflowFile.new(content).uses_declarations.filter_map do |declaration|
|
|
146
|
+
next unless declaration.steps_sequence && declaration.value.start_with?("$/")
|
|
147
|
+
|
|
148
|
+
target = Pathname.new("./#{declaration.value.delete_prefix('$/')}").cleanpath.to_s
|
|
149
|
+
dependency_files.find do |candidate|
|
|
150
|
+
path = repo_relative_path(candidate)
|
|
151
|
+
File.dirname(path) == target && ACTION_FILE_NAMES.include?(File.basename(path))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
|
|
155
|
+
[]
|
|
156
|
+
end
|
|
157
|
+
|
|
94
158
|
# The onboarded closure as the engine should see it: every workflow the lock
|
|
95
159
|
# tracks, with bumped refs applied to changed ones and the rest left verbatim.
|
|
96
160
|
sig do
|
|
@@ -114,6 +178,12 @@ module Dependabot
|
|
|
114
178
|
.select { |f| reader.onboarded?(repo_relative_path(f)) }
|
|
115
179
|
end
|
|
116
180
|
|
|
181
|
+
# The repository's home host, plumbed from the job source by the updater.
|
|
182
|
+
sig { returns(String) }
|
|
183
|
+
def source_hostname
|
|
184
|
+
T.cast(options.fetch(:source_hostname, GITHUB_COM), String)
|
|
185
|
+
end
|
|
186
|
+
|
|
117
187
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
118
188
|
def lockfile
|
|
119
189
|
dependency_files.find { |f| lockfile?(f) }
|
|
@@ -25,11 +25,22 @@ module Dependabot
|
|
|
25
25
|
|
|
26
26
|
JsonObject = T.type_alias { T::Hash[String, Object] }
|
|
27
27
|
FIXED_FINDING_CATEGORIES = %w(onboarding-required ref-changed stale).freeze
|
|
28
|
-
UNRESOLVABLE_CATEGORIES = %w(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
UNRESOLVABLE_CATEGORIES = %w(
|
|
29
|
+
unreachable-pin repo-replaced repo-unavailable repo-renamed not-pinned
|
|
30
|
+
local-action invalid-self-repository-ref misleading-sha
|
|
31
|
+
).freeze
|
|
32
|
+
|
|
33
|
+
# Host-identity failures from the engine: the lock is bound to a host it can't
|
|
34
|
+
# be verified on. User-actionable (restore/regenerate), never retryable. The
|
|
35
|
+
# engine wraps every lookup error, so only a 404 counts; 5xx/429/403/timeouts stay EngineError.
|
|
36
|
+
HOST_IDENTITY_ERROR = /verifying repository identity .*: HTTP 404\b|repository IDs|not the selected host/
|
|
37
|
+
|
|
38
|
+
# `hostname` is the repository's home host. The engine binds every pin with an
|
|
39
|
+
# omitted lockfile `hostname` to it, so it must never be guessed.
|
|
40
|
+
sig { params(credentials: T::Array[Dependabot::Credential], hostname: String).void }
|
|
41
|
+
def initialize(credentials, hostname: GITHUB_COM)
|
|
32
42
|
@credentials = credentials
|
|
43
|
+
@hostname = hostname
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
# Binary baked into the ecosystem image; falls back to PATH for local dev.
|
|
@@ -46,19 +57,26 @@ module Dependabot
|
|
|
46
57
|
params(
|
|
47
58
|
workflow_files: T::Array[Dependabot::DependencyFile],
|
|
48
59
|
lockfile: Dependabot::DependencyFile,
|
|
49
|
-
workflow_paths: T::Array[String]
|
|
60
|
+
workflow_paths: T::Array[String],
|
|
61
|
+
action_files: T::Array[Dependabot::DependencyFile]
|
|
50
62
|
).returns(String)
|
|
51
63
|
end
|
|
52
|
-
def relock(
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
def relock(
|
|
65
|
+
workflow_files:,
|
|
66
|
+
lockfile:,
|
|
67
|
+
workflow_paths: workflow_files.map { |file| repo_path(file) },
|
|
68
|
+
action_files: []
|
|
69
|
+
)
|
|
70
|
+
in_repo(workflow_files + action_files, lockfile) do |dir|
|
|
71
|
+
args = %w(--no-onboard --no-narrow --no-interactive --json=findings) +
|
|
72
|
+
["--hostname", hostname] + workflow_paths
|
|
55
73
|
json, exit_status = run(dir, args)
|
|
56
74
|
|
|
57
75
|
skipped = onboarding_skips(json, lockfile)
|
|
58
76
|
log_skips(skipped)
|
|
59
77
|
|
|
60
78
|
# `findings` is the PRE-fix diagnosis; at exit 0 fix-mode already resolved
|
|
61
|
-
# them.
|
|
79
|
+
# them. Writes are all-or-nothing, so exit 1 means nothing was written.
|
|
62
80
|
raise_on_findings(json) if exit_status == 1
|
|
63
81
|
|
|
64
82
|
File.read(File.join(dir, LOCKFILE_PATH))
|
|
@@ -70,6 +88,9 @@ module Dependabot
|
|
|
70
88
|
sig { returns(T::Array[Dependabot::Credential]) }
|
|
71
89
|
attr_reader :credentials
|
|
72
90
|
|
|
91
|
+
sig { returns(String) }
|
|
92
|
+
attr_reader :hostname
|
|
93
|
+
|
|
73
94
|
sig do
|
|
74
95
|
type_parameters(:T)
|
|
75
96
|
.params(
|
|
@@ -104,6 +125,7 @@ module Dependabot
|
|
|
104
125
|
sig { params(dir: String, args: T::Array[String]).returns([JsonObject, Integer]) }
|
|
105
126
|
def run(dir, args)
|
|
106
127
|
stdout, stderr, exit_status = invoke(dir, args)
|
|
128
|
+
raise DependencyFileNotResolvable, stderr.strip if exit_status > 1 && stderr.match?(HOST_IDENTITY_ERROR)
|
|
107
129
|
raise EngineError, "gh-actions-lock failed (exit #{exit_status}): #{stderr.strip}" if exit_status > 1
|
|
108
130
|
|
|
109
131
|
json = case (parsed = JSON.parse(stdout))
|
|
@@ -120,7 +142,9 @@ module Dependabot
|
|
|
120
142
|
# [stdout, stderr, exit_status].
|
|
121
143
|
sig { params(dir: String, args: T::Array[String]).returns([String, String, Integer]) }
|
|
122
144
|
def invoke(dir, args)
|
|
123
|
-
|
|
145
|
+
# The temp dir has no .git, so name the root that `$/` paths resolve against.
|
|
146
|
+
env = Env.build(credentials, hostname).merge("GH_ACTIONS_LOCK_REPO_ROOT" => dir)
|
|
147
|
+
env_cmd = [env, self.class.binary_path, *args, { chdir: dir }]
|
|
124
148
|
stdout, stderr, process = CommandHelpers.capture3_with_timeout(env_cmd)
|
|
125
149
|
|
|
126
150
|
# A failed spawn comes back as a nil status, not an exception.
|
|
@@ -133,23 +157,21 @@ module Dependabot
|
|
|
133
157
|
[stdout || "", stderr || "", process.exitstatus]
|
|
134
158
|
end
|
|
135
159
|
|
|
160
|
+
# Exit 1 wrote nothing. Any user-actionable finding wins; anything else retries.
|
|
136
161
|
sig { params(json: JsonObject).void }
|
|
137
162
|
def raise_on_findings(json)
|
|
138
|
-
T.cast(Array(json["findings"]), T::Array[JsonObject])
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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"
|
|
163
|
+
errors = T.cast(Array(json["findings"]), T::Array[JsonObject])
|
|
164
|
+
.select { |finding| finding["severity"].nil? || finding["severity"] == "error" }
|
|
165
|
+
unresolvable = errors.find { |finding| UNRESOLVABLE_CATEGORIES.include?(finding["category"].to_s) }
|
|
166
|
+
if unresolvable
|
|
167
|
+
raise UnresolvableDependency.new(
|
|
168
|
+
(unresolvable["dependency"] || "unknown").to_s,
|
|
169
|
+
(unresolvable["detail"] || unresolvable["category"]).to_s
|
|
170
|
+
)
|
|
152
171
|
end
|
|
172
|
+
|
|
173
|
+
categories = errors.map { |finding| finding["category"].to_s }.uniq - FIXED_FINDING_CATEGORIES
|
|
174
|
+
raise EngineError, "gh-actions-lock exited 1 without an actionable finding: #{categories.inspect}"
|
|
153
175
|
end
|
|
154
176
|
|
|
155
177
|
# Partitions onboarding-required findings into genuine skips vs. a lock the
|
|
@@ -11,7 +11,9 @@ module Dependabot
|
|
|
11
11
|
module Lockfile
|
|
12
12
|
# Builds the subprocess environment for the gh-actions-lock engine. Hosted
|
|
13
13
|
# Dependabot is tokenless behind a MITM proxy that overwrites the auth header,
|
|
14
|
-
# while proxyless local runs hold the real
|
|
14
|
+
# while proxyless local runs hold the real home-host token in `credentials`.
|
|
15
|
+
# Only a home-host `git_source` token is ever used; the engine reads other
|
|
16
|
+
# hosts (dotcom fallback) anonymously.
|
|
15
17
|
module Env
|
|
16
18
|
extend T::Sig
|
|
17
19
|
|
|
@@ -20,13 +22,13 @@ module Dependabot
|
|
|
20
22
|
DUMMY_TOKEN = "x-access-token"
|
|
21
23
|
|
|
22
24
|
sig do
|
|
23
|
-
params(credentials: T::Array[Dependabot::Credential])
|
|
25
|
+
params(credentials: T::Array[Dependabot::Credential], hostname: String)
|
|
24
26
|
.returns(T::Hash[String, String])
|
|
25
27
|
end
|
|
26
|
-
def self.build(credentials)
|
|
28
|
+
def self.build(credentials, hostname = GITHUB_COM)
|
|
27
29
|
env = {}
|
|
28
30
|
|
|
29
|
-
github_credential =
|
|
31
|
+
github_credential = home_credential(credentials, hostname)
|
|
30
32
|
env["GH_TOKEN"] = github_credential&.fetch("password", nil) || DUMMY_TOKEN
|
|
31
33
|
env["GH_ACTIONS_LOCK_DEPENDABOT_PROXY"] = "1" unless github_credential
|
|
32
34
|
|
|
@@ -36,12 +38,12 @@ module Dependabot
|
|
|
36
38
|
# Mirrors SharedHelpers.configure_git_to_use_https_with_credentials: prefer a
|
|
37
39
|
# deliberately-added token over an app installation token ("v1." prefix).
|
|
38
40
|
sig do
|
|
39
|
-
params(credentials: T::Array[Dependabot::Credential])
|
|
41
|
+
params(credentials: T::Array[Dependabot::Credential], hostname: String)
|
|
40
42
|
.returns(T.nilable(Dependabot::Credential))
|
|
41
43
|
end
|
|
42
|
-
def self.
|
|
44
|
+
def self.home_credential(credentials, hostname)
|
|
43
45
|
candidates = credentials.select do |c|
|
|
44
|
-
c["type"] == "git_source" && c["host"] ==
|
|
46
|
+
c["type"] == "git_source" && c["host"] == hostname && c["password"]
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
candidates.find { |c| !c["password"]&.start_with?("v1.") } || candidates.first
|
|
@@ -23,7 +23,7 @@ module Dependabot
|
|
|
23
23
|
super(
|
|
24
24
|
LOCKFILE_PATH,
|
|
25
25
|
"Unsupported actions.lock version #{found.inspect}; " \
|
|
26
|
-
"this version of Dependabot supports #{supported
|
|
26
|
+
"this version of Dependabot supports #{supported}. " \
|
|
27
27
|
"Upgrade Dependabot or regenerate the lockfile with a compatible gh-actions-lock version."
|
|
28
28
|
)
|
|
29
29
|
end
|
|
@@ -67,6 +67,23 @@ module Dependabot
|
|
|
67
67
|
Array(workflows[path]).include?(action_ref)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# The host a pinned `owner/repo@ref` is bound to. An omitted `hostname` means
|
|
71
|
+
# `home` in v0.0.3 and github.com in older schemas. Nil when unpinned or when
|
|
72
|
+
# bound to a host other than `home` or github.com.
|
|
73
|
+
sig { params(action_ref: String, home: String).returns(T.nilable(String)) }
|
|
74
|
+
def pinned_hostname(action_ref, home:)
|
|
75
|
+
nwo, _, ref = action_ref.rpartition("@")
|
|
76
|
+
entry = dependencies.find do |key, _|
|
|
77
|
+
key_nwo, _, key_ref = key.to_s.rpartition("@")
|
|
78
|
+
key_ref == ref && key_nwo.casecmp?(nwo)
|
|
79
|
+
end&.last
|
|
80
|
+
return unless entry.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
hostname = entry["hostname"].to_s
|
|
83
|
+
hostname = version == "v0.0.3" ? home : GITHUB_COM if hostname.empty?
|
|
84
|
+
hostname if [home, GITHUB_COM].include?(hostname)
|
|
85
|
+
end
|
|
86
|
+
|
|
70
87
|
# Asserts every `dependencies` entry carries {REQUIRED_DEPENDENCY_KEYS}. A
|
|
71
88
|
# missing key makes the engine silently treat the whole lockfile as empty.
|
|
72
89
|
# Deferred to the relock gate (not the constructor) so a malformed lock
|
|
@@ -10,7 +10,7 @@ module Dependabot
|
|
|
10
10
|
module GithubActions
|
|
11
11
|
module Lockfile
|
|
12
12
|
# Pre-1.0 lockfile schema revisions may be breaking, so only the explicitly
|
|
13
|
-
# supported
|
|
13
|
+
# supported versions are safe to read and rewrite.
|
|
14
14
|
module VersionGate
|
|
15
15
|
extend T::Sig
|
|
16
16
|
|
|
@@ -18,12 +18,12 @@ module Dependabot
|
|
|
18
18
|
def self.assert_supported!(found)
|
|
19
19
|
return if compatible?(found)
|
|
20
20
|
|
|
21
|
-
raise UnsupportedLockfileVersion.new(found,
|
|
21
|
+
raise UnsupportedLockfileVersion.new(found, SUPPORTED_LOCKFILE_VERSIONS.join(", "))
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
sig { params(found: String).returns(T::Boolean) }
|
|
25
25
|
def self.compatible?(found)
|
|
26
|
-
found
|
|
26
|
+
SUPPORTED_LOCKFILE_VERSIONS.include?(found)
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
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.401.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.401.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.401.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -278,7 +278,7 @@ licenses:
|
|
|
278
278
|
- MIT
|
|
279
279
|
metadata:
|
|
280
280
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
281
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
281
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.401.0
|
|
282
282
|
rdoc_options: []
|
|
283
283
|
require_paths:
|
|
284
284
|
- lib
|