dependabot-github_actions 0.388.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 +4 -4
- data/lib/dependabot/github_actions/constants.rb +7 -0
- data/lib/dependabot/github_actions/containing_branch_finder.rb +44 -0
- data/lib/dependabot/github_actions/file_fetcher.rb +20 -1
- data/lib/dependabot/github_actions/file_parser.rb +1 -3
- data/lib/dependabot/github_actions/file_updater.rb +100 -11
- data/lib/dependabot/github_actions/helpers.rb +7 -3
- data/lib/dependabot/github_actions/lockfile/cli_engine.rb +225 -0
- data/lib/dependabot/github_actions/lockfile/env.rb +52 -0
- data/lib/dependabot/github_actions/lockfile/errors.rb +52 -0
- data/lib/dependabot/github_actions/lockfile/reader.rb +129 -0
- data/lib/dependabot/github_actions/lockfile/version_gate.rb +31 -0
- data/lib/dependabot/github_actions/lockfile.rb +23 -0
- data/lib/dependabot/github_actions/package/package_details_fetcher.rb +15 -26
- data/lib/dependabot/github_actions/update_checker/latest_version_finder.rb +20 -6
- data/lib/dependabot/github_actions/update_checker.rb +141 -45
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 915f664971e3665b0f6ee89557dc064b19fac2a49600250d2762a286fca20da7
|
|
4
|
+
data.tar.gz: a02dc5897d62ee777fac539862d8e0cd728695d43f31ad531e1dc8c5a65db287
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/shared_helpers"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module GithubActions
|
|
10
|
+
module ContainingBranchFinder
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
# A missing commit has no containing branch, so callers can handle it like
|
|
14
|
+
# an empty branch lookup without masking unrelated subprocess failures.
|
|
15
|
+
COMMIT_NOT_FOUND_REGEX = T.let(
|
|
16
|
+
/no such commit|malformed object name|bad object|not a valid object name/i,
|
|
17
|
+
Regexp
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
sig { params(sha: String).returns(T.nilable(String)) }
|
|
21
|
+
def self.find(sha)
|
|
22
|
+
branches_including_ref = SharedHelpers.run_shell_command(
|
|
23
|
+
"git branch --remotes --contains #{sha}",
|
|
24
|
+
fingerprint: "git branch --remotes --contains <sha>"
|
|
25
|
+
).split("\n").map { |branch| branch.strip.gsub("origin/", "") }
|
|
26
|
+
return if branches_including_ref.empty?
|
|
27
|
+
|
|
28
|
+
current_branch = branches_including_ref.find { |branch| branch.start_with?("HEAD -> ") }
|
|
29
|
+
|
|
30
|
+
if current_branch
|
|
31
|
+
current_branch.delete_prefix("HEAD -> ")
|
|
32
|
+
elsif branches_including_ref.size > 1
|
|
33
|
+
raise "Multiple ambiguous branches (#{branches_including_ref.join(', ')}) include #{sha}!"
|
|
34
|
+
else
|
|
35
|
+
branches_including_ref.first
|
|
36
|
+
end
|
|
37
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
38
|
+
raise unless e.message.match?(COMMIT_NOT_FOUND_REGEX)
|
|
39
|
+
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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"
|
|
@@ -8,6 +8,7 @@ require "time"
|
|
|
8
8
|
|
|
9
9
|
require "dependabot/errors"
|
|
10
10
|
require "dependabot/git_tag_with_detail"
|
|
11
|
+
require "dependabot/github_actions/containing_branch_finder"
|
|
11
12
|
require "dependabot/github_actions/helpers"
|
|
12
13
|
require "dependabot/github_actions/requirement"
|
|
13
14
|
require "dependabot/github_actions/update_checker"
|
|
@@ -30,7 +31,8 @@ module Dependabot
|
|
|
30
31
|
credentials: T::Array[Dependabot::Credential],
|
|
31
32
|
ignored_versions: T::Array[String],
|
|
32
33
|
raise_on_ignored: T::Boolean,
|
|
33
|
-
security_advisories: T::Array[Dependabot::SecurityAdvisory]
|
|
34
|
+
security_advisories: T::Array[Dependabot::SecurityAdvisory],
|
|
35
|
+
git_metadata_fetcher: T.nilable(Dependabot::GitMetadataFetcher)
|
|
34
36
|
).void
|
|
35
37
|
end
|
|
36
38
|
def initialize(
|
|
@@ -38,13 +40,15 @@ module Dependabot
|
|
|
38
40
|
credentials:,
|
|
39
41
|
ignored_versions: [],
|
|
40
42
|
raise_on_ignored: false,
|
|
41
|
-
security_advisories: []
|
|
43
|
+
security_advisories: [],
|
|
44
|
+
git_metadata_fetcher: nil
|
|
42
45
|
)
|
|
43
46
|
@dependency = dependency
|
|
44
47
|
@credentials = credentials
|
|
45
48
|
@raise_on_ignored = raise_on_ignored
|
|
46
49
|
@ignored_versions = ignored_versions
|
|
47
50
|
@security_advisories = security_advisories
|
|
51
|
+
@git_metadata_fetcher = git_metadata_fetcher
|
|
48
52
|
|
|
49
53
|
@git_helper = T.let(git_helper, Dependabot::GithubActions::Helpers::Githelper)
|
|
50
54
|
end
|
|
@@ -122,7 +126,10 @@ module Dependabot
|
|
|
122
126
|
ref = git_commit_checker.local_ref_for_latest_version_matching_existing_precision
|
|
123
127
|
return ref if ref && ref.fetch(:version) > current_version
|
|
124
128
|
|
|
125
|
-
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
|
|
126
133
|
end,
|
|
127
134
|
T.nilable(T::Hash[Symbol, T.untyped])
|
|
128
135
|
)
|
|
@@ -224,7 +231,9 @@ module Dependabot
|
|
|
224
231
|
SharedHelpers.run_shell_command("git clone --no-recurse-submodules #{url} #{repo_contents_path}")
|
|
225
232
|
|
|
226
233
|
Dir.chdir(repo_contents_path) do
|
|
227
|
-
ref_branch =
|
|
234
|
+
ref_branch = ContainingBranchFinder.find(
|
|
235
|
+
T.must(git_commit_checker.dependency_source_details&.ref)
|
|
236
|
+
)
|
|
228
237
|
git_commit_checker.head_commit_for_local_branch(ref_branch) if ref_branch
|
|
229
238
|
end
|
|
230
239
|
end
|
|
@@ -234,27 +243,6 @@ module Dependabot
|
|
|
234
243
|
)
|
|
235
244
|
end
|
|
236
245
|
|
|
237
|
-
sig { params(sha: String).returns(T.nilable(String)) }
|
|
238
|
-
def find_container_branch(sha)
|
|
239
|
-
branches_including_ref = SharedHelpers.run_shell_command(
|
|
240
|
-
"git branch --remotes --contains #{sha}",
|
|
241
|
-
fingerprint: "git branch --remotes --contains <sha>"
|
|
242
|
-
).split("\n").map { |branch| branch.strip.gsub("origin/", "") }
|
|
243
|
-
return if branches_including_ref.empty?
|
|
244
|
-
|
|
245
|
-
current_branch = branches_including_ref.find { |branch| branch.start_with?("HEAD -> ") }
|
|
246
|
-
|
|
247
|
-
if current_branch
|
|
248
|
-
current_branch.delete_prefix("HEAD -> ")
|
|
249
|
-
elsif branches_including_ref.size > 1
|
|
250
|
-
# If there are multiple non default branches including the pinned SHA,
|
|
251
|
-
# then it's unclear how we should proceed
|
|
252
|
-
raise "Multiple ambiguous branches (#{branches_including_ref.join(', ')}) include #{sha}!"
|
|
253
|
-
else
|
|
254
|
-
branches_including_ref.first
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
|
|
258
246
|
sig do
|
|
259
247
|
params(tags: T::Array[T::Hash[Symbol, T.untyped]]).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
|
260
248
|
end
|
|
@@ -288,7 +276,8 @@ module Dependabot
|
|
|
288
276
|
ignored_versions: ignored_versions,
|
|
289
277
|
raise_on_ignored: raise_on_ignored,
|
|
290
278
|
consider_version_branches_pinned: false,
|
|
291
|
-
dependency_source_details: nil
|
|
279
|
+
dependency_source_details: nil,
|
|
280
|
+
git_metadata_fetcher: @git_metadata_fetcher
|
|
292
281
|
)
|
|
293
282
|
end
|
|
294
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
|
)
|
|
@@ -345,7 +358,7 @@ module Dependabot
|
|
|
345
358
|
|
|
346
359
|
sig { returns(T.nilable(T.any(Dependabot::Version, String))) }
|
|
347
360
|
def current_version
|
|
348
|
-
return dependency.
|
|
361
|
+
return dependency.source_string("ref", allowed_types: ["git"]) if release_type_sha?
|
|
349
362
|
|
|
350
363
|
T.let(dependency.numeric_version, T.nilable(Dependabot::Version))
|
|
351
364
|
end
|
|
@@ -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,7 +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"
|
|
9
|
+
require "dependabot/github_actions/containing_branch_finder"
|
|
10
|
+
require "dependabot/github_actions/lockfile/reader"
|
|
8
11
|
require "dependabot/github_actions/requirement"
|
|
9
12
|
require "dependabot/github_actions/version"
|
|
10
13
|
require "dependabot/update_checkers"
|
|
@@ -16,6 +19,8 @@ module Dependabot
|
|
|
16
19
|
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
|
17
20
|
extend T::Sig
|
|
18
21
|
|
|
22
|
+
GitSource = T.type_alias { T::Hash[Symbol, String] }
|
|
23
|
+
|
|
19
24
|
require_relative "update_checker/latest_version_finder"
|
|
20
25
|
|
|
21
26
|
sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
|
|
@@ -52,11 +57,16 @@ module Dependabot
|
|
|
52
57
|
)
|
|
53
58
|
end
|
|
54
59
|
|
|
60
|
+
sig { override.returns(T::Boolean) }
|
|
61
|
+
def up_to_date?
|
|
62
|
+
super && !onboarded_requirements_changed?
|
|
63
|
+
end
|
|
64
|
+
|
|
55
65
|
sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
56
66
|
def updated_requirements
|
|
57
67
|
updated_reqs = dependency.requirements.map do |req|
|
|
58
|
-
source = req
|
|
59
|
-
updated = updated_ref(source)
|
|
68
|
+
source = T.cast(req.source, GitSource)
|
|
69
|
+
updated = updated_ref(source, onboarded: onboarded_requirement?(req))
|
|
60
70
|
next req unless updated
|
|
61
71
|
|
|
62
72
|
current = source[:ref]
|
|
@@ -64,8 +74,8 @@ module Dependabot
|
|
|
64
74
|
# Maintain a short git hash only if it matches the latest
|
|
65
75
|
if req[:type] == "git" &&
|
|
66
76
|
git_commit_checker.ref_looks_like_commit_sha?(updated) &&
|
|
67
|
-
git_commit_checker.ref_looks_like_commit_sha?(current) &&
|
|
68
|
-
updated.start_with?(current)
|
|
77
|
+
git_commit_checker.ref_looks_like_commit_sha?(T.must(current)) &&
|
|
78
|
+
updated.start_with?(T.must(current))
|
|
69
79
|
next req
|
|
70
80
|
end
|
|
71
81
|
|
|
@@ -77,23 +87,118 @@ module Dependabot
|
|
|
77
87
|
|
|
78
88
|
private
|
|
79
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
|
+
|
|
80
131
|
sig { returns(T.nilable(Dependabot::GithubActions::UpdateChecker::LatestVersionFinder)) }
|
|
81
132
|
def latest_version_finder
|
|
82
133
|
@latest_version_finder ||=
|
|
83
134
|
T.let(
|
|
84
|
-
|
|
85
|
-
dependency: dependency,
|
|
86
|
-
credentials: credentials,
|
|
87
|
-
dependency_files: dependency_files,
|
|
88
|
-
security_advisories: security_advisories,
|
|
89
|
-
ignored_versions: ignored_versions,
|
|
90
|
-
raise_on_ignored: raise_on_ignored,
|
|
91
|
-
cooldown_options: update_cooldown
|
|
92
|
-
),
|
|
135
|
+
build_latest_version_finder(dependency),
|
|
93
136
|
T.nilable(Dependabot::GithubActions::UpdateChecker::LatestVersionFinder)
|
|
94
137
|
)
|
|
95
138
|
end
|
|
96
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
|
+
|
|
97
202
|
sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
|
|
98
203
|
def active_advisories
|
|
99
204
|
security_advisories.select do |advisory|
|
|
@@ -130,7 +235,9 @@ module Dependabot
|
|
|
130
235
|
SharedHelpers.run_shell_command("git clone --no-recurse-submodules #{url} #{repo_contents_path}")
|
|
131
236
|
|
|
132
237
|
Dir.chdir(repo_contents_path) do
|
|
133
|
-
ref_branch =
|
|
238
|
+
ref_branch = ContainingBranchFinder.find(
|
|
239
|
+
T.must(git_commit_checker.dependency_source_details&.ref)
|
|
240
|
+
)
|
|
134
241
|
git_commit_checker.head_commit_for_local_branch(ref_branch) if ref_branch
|
|
135
242
|
end
|
|
136
243
|
end
|
|
@@ -140,12 +247,17 @@ module Dependabot
|
|
|
140
247
|
)
|
|
141
248
|
end
|
|
142
249
|
|
|
143
|
-
sig
|
|
144
|
-
|
|
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)
|
|
145
255
|
# TODO: Support Docker sources
|
|
146
256
|
return unless git_commit_checker.git_dependency?
|
|
147
257
|
|
|
148
|
-
|
|
258
|
+
finder = onboarded ? latest_version_finder_for(source) : T.must(latest_version_finder)
|
|
259
|
+
|
|
260
|
+
if vulnerable? && (new_tag = finder.lowest_security_fix_release)
|
|
149
261
|
return new_tag.fetch(:tag)
|
|
150
262
|
end
|
|
151
263
|
|
|
@@ -153,13 +265,13 @@ module Dependabot
|
|
|
153
265
|
|
|
154
266
|
# Return the git tag if updating a pinned version
|
|
155
267
|
if source_git_commit_checker.pinned_ref_looks_like_version? &&
|
|
156
|
-
(new_tag =
|
|
268
|
+
(new_tag = finder.latest_version_tag_respecting_cooldown)
|
|
157
269
|
return new_tag.fetch(:tag)
|
|
158
270
|
end
|
|
159
271
|
|
|
160
272
|
# Return the pinned git commit if one is available
|
|
161
273
|
if source_git_commit_checker.pinned_ref_looks_like_commit_sha? &&
|
|
162
|
-
(new_commit_sha = latest_commit_sha(source_git_commit_checker))
|
|
274
|
+
(new_commit_sha = latest_commit_sha(source_git_commit_checker, finder))
|
|
163
275
|
return new_commit_sha
|
|
164
276
|
end
|
|
165
277
|
|
|
@@ -167,17 +279,20 @@ module Dependabot
|
|
|
167
279
|
nil
|
|
168
280
|
end
|
|
169
281
|
|
|
170
|
-
sig
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
173
288
|
return unless latest_tag
|
|
174
289
|
|
|
175
290
|
if source_checker.local_tag_for_pinned_sha
|
|
176
|
-
new_tag =
|
|
291
|
+
new_tag = finder.latest_version_tag_respecting_cooldown
|
|
177
292
|
new_tag&.fetch(:commit_sha)
|
|
178
293
|
else
|
|
179
294
|
# Keep SHA rewrites aligned with the checker decision (including cooldown filtering).
|
|
180
|
-
latest =
|
|
295
|
+
latest = finder.latest_release_version
|
|
181
296
|
latest.is_a?(String) ? latest : latest_commit_for_pinned_ref
|
|
182
297
|
end
|
|
183
298
|
end
|
|
@@ -195,29 +310,10 @@ module Dependabot
|
|
|
195
310
|
ignored_versions: ignored_versions,
|
|
196
311
|
raise_on_ignored: raise_on_ignored,
|
|
197
312
|
consider_version_branches_pinned: false,
|
|
198
|
-
dependency_source_details: nil
|
|
313
|
+
dependency_source_details: nil,
|
|
314
|
+
git_metadata_fetcher: git_metadata_fetcher
|
|
199
315
|
)
|
|
200
316
|
end
|
|
201
|
-
|
|
202
|
-
sig { params(sha: String).returns(T.nilable(String)) }
|
|
203
|
-
def find_container_branch(sha)
|
|
204
|
-
branches_including_ref = SharedHelpers.run_shell_command(
|
|
205
|
-
"git branch --remotes --contains #{sha}",
|
|
206
|
-
fingerprint: "git branch --remotes --contains <sha>"
|
|
207
|
-
).split("\n").map { |branch| branch.strip.gsub("origin/", "") }
|
|
208
|
-
return if branches_including_ref.empty?
|
|
209
|
-
|
|
210
|
-
current_branch = branches_including_ref.find { |branch| branch.start_with?("HEAD -> ") }
|
|
211
|
-
|
|
212
|
-
if current_branch
|
|
213
|
-
current_branch.delete_prefix("HEAD -> ")
|
|
214
|
-
elsif branches_including_ref.size > 1
|
|
215
|
-
# If there are multiple non default branches including the pinned SHA, then it's unclear how we should proceed
|
|
216
|
-
raise "Multiple ambiguous branches (#{branches_including_ref.join(', ')}) include #{sha}!"
|
|
217
|
-
else
|
|
218
|
-
branches_including_ref.first
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
317
|
end
|
|
222
318
|
end
|
|
223
319
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-github_actions
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.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.
|
|
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.
|
|
25
|
+
version: 0.390.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -243,10 +243,17 @@ extra_rdoc_files: []
|
|
|
243
243
|
files:
|
|
244
244
|
- lib/dependabot/github_actions.rb
|
|
245
245
|
- lib/dependabot/github_actions/constants.rb
|
|
246
|
+
- lib/dependabot/github_actions/containing_branch_finder.rb
|
|
246
247
|
- lib/dependabot/github_actions/file_fetcher.rb
|
|
247
248
|
- lib/dependabot/github_actions/file_parser.rb
|
|
248
249
|
- lib/dependabot/github_actions/file_updater.rb
|
|
249
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
|
|
250
257
|
- lib/dependabot/github_actions/metadata_finder.rb
|
|
251
258
|
- lib/dependabot/github_actions/package/package_details_fetcher.rb
|
|
252
259
|
- lib/dependabot/github_actions/package_manager.rb
|
|
@@ -259,7 +266,7 @@ licenses:
|
|
|
259
266
|
- MIT
|
|
260
267
|
metadata:
|
|
261
268
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
262
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
269
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.390.0
|
|
263
270
|
rdoc_options: []
|
|
264
271
|
require_paths:
|
|
265
272
|
- lib
|