dependabot-github_actions 0.400.0 → 0.402.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dependabot/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/workflow_updater.rb +3 -1
- data/lib/dependabot/github_actions/file_updater.rb +74 -4
- data/lib/dependabot/github_actions/lockfile/cli_engine.rb +63 -28
- 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: 2b4e91bfa491ff4e2ec830e11a36ede433981bb4eed911892731ee86c1524e5a
|
|
4
|
+
data.tar.gz: 72215130e11597be9306a4d06aaef10c14f23324b7c05e9a9ede16ab407c8a8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0de27643b909bdc0145989098cd705eec0c96cf3d3a78dcba5556c15c5db16b2542dfd2d6a82d1ac73f1d07d0a3321427cc9fad93c732fc4a079bd6efd3040ca
|
|
7
|
+
data.tar.gz: cd48b7b7093a9f6c4b8a607ff4607272e0b993206deafca33d1c72731365c2c6af0e569bb1254d287d96c8087bbb5ef0bfcc26ae1fc51165a370ea249509cac4
|
|
@@ -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 }
|
|
@@ -176,11 +176,13 @@ module Dependabot
|
|
|
176
176
|
def uses_declaration_pattern(declaration)
|
|
177
177
|
escaped_declaration = Regexp.escape(declaration)
|
|
178
178
|
|
|
179
|
+
# The declaration must end at whitespace, a flow delimiter or the end of the line. `$` does not match
|
|
180
|
+
# before a carriage return, so a CRLF line ending has to be listed explicitly.
|
|
179
181
|
/
|
|
180
182
|
(?:^|[ \t{,])
|
|
181
183
|
(?:"uses"|'uses'|uses)[ \t]*:[ \t]*
|
|
182
184
|
(?:"#{escaped_declaration}"|'#{escaped_declaration}'|#{escaped_declaration})
|
|
183
|
-
(?=[ \t]|$|[,}\]])
|
|
185
|
+
(?=[ \t\r]|$|[,}\]])
|
|
184
186
|
/x
|
|
185
187
|
end
|
|
186
188
|
|
|
@@ -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 never follows
|
|
112
|
+
# `./` actions.
|
|
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,27 @@ 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
|
+
tag-commit-mismatch incomplete-lockfile ref-missing invalid-workflow
|
|
32
|
+
).freeze
|
|
33
|
+
|
|
34
|
+
# Host-identity failures from the engine: the lock is bound to a host it can't
|
|
35
|
+
# be verified on. User-actionable (restore/regenerate), never retryable. The
|
|
36
|
+
# engine wraps every lookup error, so only a 404 counts; 5xx/429/403/timeouts stay EngineError.
|
|
37
|
+
HOST_IDENTITY_ERROR = /verifying repository identity .*: HTTP 404\b|repository IDs|not the selected host/
|
|
38
|
+
|
|
39
|
+
# Write-time refusals (an incomplete lock graph, or a `uses:` cycle the lock
|
|
40
|
+
# can't record). Deterministic and user-actionable, so retrying never helps.
|
|
41
|
+
WRITE_REFUSED_ERROR = /refusing to write /
|
|
42
|
+
|
|
43
|
+
# `hostname` is the repository's home host. The engine binds every pin with an
|
|
44
|
+
# omitted lockfile `hostname` to it, so it must never be guessed.
|
|
45
|
+
sig { params(credentials: T::Array[Dependabot::Credential], hostname: String).void }
|
|
46
|
+
def initialize(credentials, hostname: GITHUB_COM)
|
|
32
47
|
@credentials = credentials
|
|
48
|
+
@hostname = hostname
|
|
33
49
|
end
|
|
34
50
|
|
|
35
51
|
# Binary baked into the ecosystem image; falls back to PATH for local dev.
|
|
@@ -41,24 +57,35 @@ module Dependabot
|
|
|
41
57
|
|
|
42
58
|
# Re-pins workflows already tracked in the lockfile. `--no-onboard` refuses new
|
|
43
59
|
# workflows/actions (surfaced as onboarding-required skips); `--no-narrow`
|
|
44
|
-
# keeps the exact ref Dependabot wrote.
|
|
60
|
+
# keeps the exact ref Dependabot wrote. `--ignore-local-actions` keeps `./`
|
|
61
|
+
# actions a warning: the lock can't record them, Dependabot never rewrites
|
|
62
|
+
# them, and whether their action.yml is readable here depends on whether the
|
|
63
|
+
# repo also uses `$/`.
|
|
45
64
|
sig do
|
|
46
65
|
params(
|
|
47
66
|
workflow_files: T::Array[Dependabot::DependencyFile],
|
|
48
67
|
lockfile: Dependabot::DependencyFile,
|
|
49
|
-
workflow_paths: T::Array[String]
|
|
68
|
+
workflow_paths: T::Array[String],
|
|
69
|
+
action_files: T::Array[Dependabot::DependencyFile]
|
|
50
70
|
).returns(String)
|
|
51
71
|
end
|
|
52
|
-
def relock(
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
def relock(
|
|
73
|
+
workflow_files:,
|
|
74
|
+
lockfile:,
|
|
75
|
+
workflow_paths: workflow_files.map { |file| repo_path(file) },
|
|
76
|
+
action_files: []
|
|
77
|
+
)
|
|
78
|
+
in_repo(workflow_files + action_files, lockfile) do |dir|
|
|
79
|
+
args = %w(--no-onboard --no-narrow --ignore-local-actions --no-interactive --json=findings) +
|
|
80
|
+
["--hostname", hostname] + workflow_paths
|
|
55
81
|
json, exit_status = run(dir, args)
|
|
56
82
|
|
|
57
83
|
skipped = onboarding_skips(json, lockfile)
|
|
58
84
|
log_skips(skipped)
|
|
59
85
|
|
|
60
|
-
# `findings` is the
|
|
61
|
-
#
|
|
86
|
+
# At exit 1 `findings` is the pre-fix diagnosis of what blocked the fix;
|
|
87
|
+
# at exit 0 it reports the post-fix state. Writes are all-or-nothing, so
|
|
88
|
+
# exit 1 means nothing was written.
|
|
62
89
|
raise_on_findings(json) if exit_status == 1
|
|
63
90
|
|
|
64
91
|
File.read(File.join(dir, LOCKFILE_PATH))
|
|
@@ -70,6 +97,9 @@ module Dependabot
|
|
|
70
97
|
sig { returns(T::Array[Dependabot::Credential]) }
|
|
71
98
|
attr_reader :credentials
|
|
72
99
|
|
|
100
|
+
sig { returns(String) }
|
|
101
|
+
attr_reader :hostname
|
|
102
|
+
|
|
73
103
|
sig do
|
|
74
104
|
type_parameters(:T)
|
|
75
105
|
.params(
|
|
@@ -99,11 +129,16 @@ module Dependabot
|
|
|
99
129
|
|
|
100
130
|
# Invokes the binary and returns [parsed JSON, exit code]. Exit is tri-state:
|
|
101
131
|
# 0 = valid, 1 = blocking findings (still well-formed JSON on stdout, parse it),
|
|
102
|
-
# 2+ = tool failure (no usable JSON).
|
|
103
|
-
# fix
|
|
132
|
+
# 2+ = tool failure (no usable JSON). In fix-mode, findings/valid describe the
|
|
133
|
+
# post-fix state after a successful fix but the pre-fix state at exit 1, so
|
|
134
|
+
# callers gate on exit.
|
|
104
135
|
sig { params(dir: String, args: T::Array[String]).returns([JsonObject, Integer]) }
|
|
105
136
|
def run(dir, args)
|
|
106
137
|
stdout, stderr, exit_status = invoke(dir, args)
|
|
138
|
+
if exit_status > 1 && stderr.match?(Regexp.union(HOST_IDENTITY_ERROR, WRITE_REFUSED_ERROR))
|
|
139
|
+
raise DependencyFileNotResolvable, stderr.strip
|
|
140
|
+
end
|
|
141
|
+
|
|
107
142
|
raise EngineError, "gh-actions-lock failed (exit #{exit_status}): #{stderr.strip}" if exit_status > 1
|
|
108
143
|
|
|
109
144
|
json = case (parsed = JSON.parse(stdout))
|
|
@@ -120,7 +155,9 @@ module Dependabot
|
|
|
120
155
|
# [stdout, stderr, exit_status].
|
|
121
156
|
sig { params(dir: String, args: T::Array[String]).returns([String, String, Integer]) }
|
|
122
157
|
def invoke(dir, args)
|
|
123
|
-
|
|
158
|
+
# The temp dir has no .git, so name the root that `$/` paths resolve against.
|
|
159
|
+
env = Env.build(credentials, hostname).merge("GH_ACTIONS_LOCK_REPO_ROOT" => dir)
|
|
160
|
+
env_cmd = [env, self.class.binary_path, *args, { chdir: dir }]
|
|
124
161
|
stdout, stderr, process = CommandHelpers.capture3_with_timeout(env_cmd)
|
|
125
162
|
|
|
126
163
|
# A failed spawn comes back as a nil status, not an exception.
|
|
@@ -133,23 +170,21 @@ module Dependabot
|
|
|
133
170
|
[stdout || "", stderr || "", process.exitstatus]
|
|
134
171
|
end
|
|
135
172
|
|
|
173
|
+
# Exit 1 wrote nothing. Any user-actionable finding wins; anything else retries.
|
|
136
174
|
sig { params(json: JsonObject).void }
|
|
137
175
|
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"
|
|
176
|
+
errors = T.cast(Array(json["findings"]), T::Array[JsonObject])
|
|
177
|
+
.select { |finding| finding["severity"].nil? || finding["severity"] == "error" }
|
|
178
|
+
unresolvable = errors.find { |finding| UNRESOLVABLE_CATEGORIES.include?(finding["category"].to_s) }
|
|
179
|
+
if unresolvable
|
|
180
|
+
raise UnresolvableDependency.new(
|
|
181
|
+
(unresolvable["dependency"] || unresolvable["workflow"] || "unknown").to_s,
|
|
182
|
+
(unresolvable["detail"] || unresolvable["category"]).to_s
|
|
183
|
+
)
|
|
152
184
|
end
|
|
185
|
+
|
|
186
|
+
categories = errors.map { |finding| finding["category"].to_s }.uniq - FIXED_FINDING_CATEGORIES
|
|
187
|
+
raise EngineError, "gh-actions-lock exited 1 without an actionable finding: #{categories.inspect}"
|
|
153
188
|
end
|
|
154
189
|
|
|
155
190
|
# 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.402.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.402.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.402.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -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.402.0
|
|
282
282
|
rdoc_options: []
|
|
283
283
|
require_paths:
|
|
284
284
|
- lib
|