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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 127143db09496d31ce04baf778857fb6c24220b8281339e4e20d3b02952817a7
4
- data.tar.gz: 57070b737dc53b4c2afb8304abfe3401afabd173d8c24d593f49803e7d43582d
3
+ metadata.gz: 2b4e91bfa491ff4e2ec830e11a36ede433981bb4eed911892731ee86c1524e5a
4
+ data.tar.gz: 72215130e11597be9306a4d06aaef10c14f23324b7c05e9a9ede16ab407c8a8c
5
5
  SHA512:
6
- metadata.gz: 9c16394377ec48df4232a55acdc5f8af3705b4b91358630732d365084b44230ca1f6316b65e64f6f6b301ea3f73299798b87ada26f5fa2e9df184cc6770ed48b
7
- data.tar.gz: cdf8bf664b45b30d395b842972bd6fb55693849f56cf54c679582cd0da682e5d25f4e85881f2b7844bf50b1fd90be7d4cec22b36cae76455e23190dac4c2ad9d
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
- # The only lockfile schema version this ecosystem understands.
38
- SUPPORTED_LOCKFILE_VERSION = "v0.0.2"
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 += [actions_lockfile].compact if incorrectly_encoded_workflow_files.empty?
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
- # Fetches the canonical `.github/workflows/actions.lock` when the update covers
80
- # repository workflows. Composite-action directories cannot own a lockfile.
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
- return fetch_file_if_present(LOCKFILE_PATH) if directory == "/"
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
- fetch_file_if_present(LOCKFILE_NAME) if directory.delete_prefix("/") == WORKFLOW_DIRECTORY
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.select do |file|
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
- return [] if changed_repository_workflows.empty?
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 = changed_repository_workflows.select { |f| reader.onboarded?(repo_relative_path(f)) }
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(impostor-commit lockfile-forgery).freeze
29
-
30
- sig { params(credentials: T::Array[Dependabot::Credential]).void }
31
- def initialize(credentials)
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(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
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 PRE-fix diagnosis; at exit 0 fix-mode already resolved
61
- # them. Only exit 1 can carry a survivor (impostor/forgery or a skip).
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). JSON and exit are not interchangeable in
103
- # fix-mode: findings/valid are PRE-fix, exit is POST-fix, so callers gate on exit.
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
- env_cmd = [Env.build(credentials), self.class.binary_path, *args, { chdir: dir }]
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]).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"
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 github.com token in `credentials`.
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 = github_dot_com_credential(credentials)
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.github_dot_com_credential(credentials)
44
+ def self.home_credential(credentials, hostname)
43
45
  candidates = credentials.select do |c|
44
- c["type"] == "git_source" && c["host"] == GITHUB_COM && c["password"]
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.inspect}. " \
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 version is safe to read and rewrite.
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, SUPPORTED_LOCKFILE_VERSION)
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 == SUPPORTED_LOCKFILE_VERSION
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.400.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.400.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.400.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.400.0
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