dependabot-github_actions 0.389.0 → 0.390.0

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