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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 127143db09496d31ce04baf778857fb6c24220b8281339e4e20d3b02952817a7
4
- data.tar.gz: 57070b737dc53b4c2afb8304abfe3401afabd173d8c24d593f49803e7d43582d
3
+ metadata.gz: f52dcc4ffca3fffd85efa133c46ee31fee800ddca84ed787876441ec34414b47
4
+ data.tar.gz: a623cf219e7f0b21463f6e3345cccce0c30773340695adf102cbf332d018f163
5
5
  SHA512:
6
- metadata.gz: 9c16394377ec48df4232a55acdc5f8af3705b4b91358630732d365084b44230ca1f6316b65e64f6f6b301ea3f73299798b87ada26f5fa2e9df184cc6770ed48b
7
- data.tar.gz: cdf8bf664b45b30d395b842972bd6fb55693849f56cf54c679582cd0da682e5d25f4e85881f2b7844bf50b1fd90be7d4cec22b36cae76455e23190dac4c2ad9d
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
- # 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 }
@@ -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 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(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
+ ).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(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
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. Only exit 1 can carry a survivor (impostor/forgery or a skip).
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
- env_cmd = [Env.build(credentials), self.class.binary_path, *args, { chdir: dir }]
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]).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"
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 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.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.400.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.400.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.400.0
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