dependabot-common 0.95.1 → 0.95.2

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot.rb +4 -0
  3. data/lib/dependabot/clients/bitbucket.rb +105 -0
  4. data/lib/dependabot/clients/github_with_retries.rb +121 -0
  5. data/lib/dependabot/clients/gitlab.rb +72 -0
  6. data/lib/dependabot/dependency.rb +115 -0
  7. data/lib/dependabot/dependency_file.rb +60 -0
  8. data/lib/dependabot/errors.rb +179 -0
  9. data/lib/dependabot/file_fetchers.rb +18 -0
  10. data/lib/dependabot/file_fetchers/README.md +65 -0
  11. data/lib/dependabot/file_fetchers/base.rb +368 -0
  12. data/lib/dependabot/file_parsers.rb +18 -0
  13. data/lib/dependabot/file_parsers/README.md +45 -0
  14. data/lib/dependabot/file_parsers/base.rb +31 -0
  15. data/lib/dependabot/file_parsers/base/dependency_set.rb +77 -0
  16. data/lib/dependabot/file_updaters.rb +18 -0
  17. data/lib/dependabot/file_updaters/README.md +58 -0
  18. data/lib/dependabot/file_updaters/base.rb +52 -0
  19. data/lib/dependabot/git_commit_checker.rb +412 -0
  20. data/lib/dependabot/metadata_finders.rb +18 -0
  21. data/lib/dependabot/metadata_finders/README.md +53 -0
  22. data/lib/dependabot/metadata_finders/base.rb +117 -0
  23. data/lib/dependabot/metadata_finders/base/changelog_finder.rb +321 -0
  24. data/lib/dependabot/metadata_finders/base/changelog_pruner.rb +177 -0
  25. data/lib/dependabot/metadata_finders/base/commits_finder.rb +221 -0
  26. data/lib/dependabot/metadata_finders/base/release_finder.rb +255 -0
  27. data/lib/dependabot/pull_request_creator.rb +155 -0
  28. data/lib/dependabot/pull_request_creator/branch_namer.rb +170 -0
  29. data/lib/dependabot/pull_request_creator/commit_signer.rb +63 -0
  30. data/lib/dependabot/pull_request_creator/github.rb +277 -0
  31. data/lib/dependabot/pull_request_creator/gitlab.rb +162 -0
  32. data/lib/dependabot/pull_request_creator/labeler.rb +373 -0
  33. data/lib/dependabot/pull_request_creator/message_builder.rb +906 -0
  34. data/lib/dependabot/pull_request_updater.rb +43 -0
  35. data/lib/dependabot/pull_request_updater/github.rb +165 -0
  36. data/lib/dependabot/shared_helpers.rb +224 -0
  37. data/lib/dependabot/source.rb +120 -0
  38. data/lib/dependabot/update_checkers.rb +18 -0
  39. data/lib/dependabot/update_checkers/README.md +67 -0
  40. data/lib/dependabot/update_checkers/base.rb +220 -0
  41. data/lib/dependabot/utils.rb +33 -0
  42. data/lib/dependabot/version.rb +5 -0
  43. data/lib/rubygems_version_patch.rb +14 -0
  44. metadata +44 -2
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/pull_request_updater/github"
4
+
5
+ module Dependabot
6
+ class PullRequestUpdater
7
+ attr_reader :source, :files, :base_commit, :credentials,
8
+ :pull_request_number, :author_details, :signature_key
9
+
10
+ def initialize(source:, base_commit:, files:, credentials:,
11
+ pull_request_number:, author_details: nil,
12
+ signature_key: nil)
13
+ @source = source
14
+ @base_commit = base_commit
15
+ @files = files
16
+ @credentials = credentials
17
+ @pull_request_number = pull_request_number
18
+ @author_details = author_details
19
+ @signature_key = signature_key
20
+ end
21
+
22
+ def update
23
+ case source.provider
24
+ when "github" then github_updater.update
25
+ else raise "Unsupported provider #{source.provider}"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def github_updater
32
+ Github.new(
33
+ source: source,
34
+ base_commit: base_commit,
35
+ files: files,
36
+ credentials: credentials,
37
+ pull_request_number: pull_request_number,
38
+ author_details: author_details,
39
+ signature_key: signature_key
40
+ )
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "octokit"
4
+ require "dependabot/clients/github_with_retries"
5
+ require "dependabot/pull_request_creator/commit_signer"
6
+ require "dependabot/pull_request_updater"
7
+
8
+ module Dependabot
9
+ class PullRequestUpdater
10
+ class Github
11
+ attr_reader :source, :files, :base_commit, :credentials,
12
+ :pull_request_number, :author_details, :signature_key
13
+
14
+ def initialize(source:, base_commit:, files:, credentials:,
15
+ pull_request_number:, author_details: nil,
16
+ signature_key: nil)
17
+ @source = source
18
+ @base_commit = base_commit
19
+ @files = files
20
+ @credentials = credentials
21
+ @pull_request_number = pull_request_number
22
+ @author_details = author_details
23
+ @signature_key = signature_key
24
+ end
25
+
26
+ def update
27
+ return unless branch_exists?
28
+
29
+ commit = create_commit
30
+ branch = update_branch(commit)
31
+ update_pull_request_target_branch
32
+ branch
33
+ end
34
+
35
+ private
36
+
37
+ def update_pull_request_target_branch
38
+ target_branch = source.branch || pull_request.base.repo.default_branch
39
+ return if target_branch == pull_request.base.ref
40
+
41
+ github_client_for_source.update_pull_request(
42
+ source.repo,
43
+ pull_request_number,
44
+ base: target_branch
45
+ )
46
+ end
47
+
48
+ def github_client_for_source
49
+ @github_client_for_source ||=
50
+ Dependabot::Clients::GithubWithRetries.for_source(
51
+ source: source,
52
+ credentials: credentials
53
+ )
54
+ end
55
+
56
+ def pull_request
57
+ @pull_request ||=
58
+ github_client_for_source.pull_request(
59
+ source.repo,
60
+ pull_request_number
61
+ )
62
+ end
63
+
64
+ def branch_exists?
65
+ github_client_for_source.branch(source.repo, pull_request.head.ref)
66
+ rescue Octokit::NotFound
67
+ false
68
+ end
69
+
70
+ def create_commit
71
+ tree = create_tree
72
+
73
+ options = author_details&.any? ? { author: author_details } : {}
74
+
75
+ if options[:author]&.any? && signature_key
76
+ options[:author][:date] = Time.now.utc.iso8601
77
+ options[:signature] = commit_signature(tree, options[:author])
78
+ end
79
+
80
+ github_client_for_source.create_commit(
81
+ source.repo,
82
+ commit_message,
83
+ tree.sha,
84
+ base_commit,
85
+ options
86
+ )
87
+ end
88
+
89
+ def create_tree
90
+ file_trees = files.map do |file|
91
+ if file.type == "file"
92
+ {
93
+ path: file.path.sub(%r{^/}, ""),
94
+ mode: "100644",
95
+ type: "blob",
96
+ content: file.content
97
+ }
98
+ elsif file.type == "submodule"
99
+ {
100
+ path: file.path.sub(%r{^/}, ""),
101
+ mode: "160000",
102
+ type: "commit",
103
+ sha: file.content
104
+ }
105
+ else
106
+ raise "Unknown file type #{file.type}"
107
+ end
108
+ end
109
+
110
+ github_client_for_source.create_tree(
111
+ source.repo,
112
+ file_trees,
113
+ base_tree: base_commit
114
+ )
115
+ end
116
+
117
+ def update_branch(commit)
118
+ github_client_for_source.update_ref(
119
+ source.repo,
120
+ "heads/" + pull_request.head.ref,
121
+ commit.sha,
122
+ true
123
+ )
124
+ rescue Octokit::UnprocessableEntity => error
125
+ # Return quietly if the branch has been deleted
126
+ return nil if error.message.match?(/Reference does not exist/i)
127
+
128
+ # Return quietly if the branch has been merged
129
+ return nil if error.message.match?(/Reference cannot be updated/i)
130
+
131
+ raise
132
+ end
133
+
134
+ def commit_message
135
+ @commit_message ||=
136
+ if pull_request.commits == 1
137
+ github_client_for_source.
138
+ git_commit(source.repo, pull_request.head.sha).
139
+ message
140
+ else
141
+ author_name = author_details&.fetch(:name, nil) || "dependabot[bot]"
142
+ commits =
143
+ github_client_for_source.
144
+ pull_request_commits(source.repo, pull_request_number)
145
+
146
+ commit =
147
+ commits.find { |c| c.commit.author.name == author_name } ||
148
+ commits.first
149
+
150
+ commit.commit.message
151
+ end
152
+ end
153
+
154
+ def commit_signature(tree, author_details_with_date)
155
+ PullRequestCreator::CommitSigner.new(
156
+ author_details: author_details_with_date,
157
+ commit_message: commit_message,
158
+ tree_sha: tree.sha,
159
+ parent_sha: base_commit,
160
+ signature_key: signature_key
161
+ ).signature
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "tmpdir"
5
+ require "excon"
6
+ require "English"
7
+ require "digest"
8
+ require "open3"
9
+
10
+ module Dependabot
11
+ module SharedHelpers
12
+ BUMP_TMP_FILE_PREFIX = "dependabot_"
13
+ BUMP_TMP_DIR_PATH = "tmp"
14
+ GIT_CONFIG_GLOBAL_PATH = File.expand_path("~/.gitconfig")
15
+
16
+ class ChildProcessFailed < StandardError
17
+ attr_reader :error_class, :error_message, :error_backtrace
18
+
19
+ def initialize(error_class:, error_message:, error_backtrace:)
20
+ @error_class = error_class
21
+ @error_message = error_message
22
+ @error_backtrace = error_backtrace
23
+
24
+ msg = "Child process raised #{error_class} with message: "\
25
+ "#{error_message}"
26
+ super(msg)
27
+ set_backtrace(error_backtrace)
28
+ end
29
+ end
30
+
31
+ def self.in_a_temporary_directory(directory = "/")
32
+ Dir.mkdir(BUMP_TMP_DIR_PATH) unless Dir.exist?(BUMP_TMP_DIR_PATH)
33
+ Dir.mktmpdir(BUMP_TMP_FILE_PREFIX, BUMP_TMP_DIR_PATH) do |dir|
34
+ path = Pathname.new(File.join(dir, directory)).expand_path
35
+ FileUtils.mkpath(path)
36
+ Dir.chdir(path) { yield(path) }
37
+ end
38
+ end
39
+
40
+ def self.in_a_forked_process
41
+ read, write = IO.pipe
42
+
43
+ pid = fork do
44
+ read.close
45
+ result = yield
46
+ rescue Exception => error # rubocop:disable Lint/RescueException
47
+ result = { _error_details: { error_class: error.class.to_s,
48
+ error_message: error.message,
49
+ error_backtrace: error.backtrace } }
50
+ ensure
51
+ Marshal.dump(result, write)
52
+ exit!(0)
53
+ end
54
+
55
+ write.close
56
+ result = read.read
57
+ Process.wait(pid)
58
+ result = Marshal.load(result) # rubocop:disable Security/MarshalLoad
59
+
60
+ return result unless result.is_a?(Hash) && result[:_error_details]
61
+
62
+ raise ChildProcessFailed, result[:_error_details]
63
+ end
64
+
65
+ class HelperSubprocessFailed < StandardError
66
+ def initialize(message:, error_context:)
67
+ super(message)
68
+ @error_context = error_context
69
+ @command = error_context[:command]
70
+ end
71
+
72
+ def raven_context
73
+ { fingerprint: [@command], extra: @error_context }
74
+ end
75
+ end
76
+
77
+ def self.run_helper_subprocess(command:, function:, args:, env: nil,
78
+ stderr_to_stdout: false)
79
+ start = Time.now
80
+ stdin_data = JSON.dump(function: function, args: args)
81
+ env_cmd = [env, command].compact
82
+ stdout, stderr, process = Open3.capture3(*env_cmd, stdin_data: stdin_data)
83
+ time_taken = Time.now - start
84
+
85
+ # Some package managers output useful stuff to stderr instead of stdout so
86
+ # we want to parse this, most package manager will output garbage here so
87
+ # would mess up json response from stdout
88
+ stdout = "#{stderr}\n#{stdout}" if stderr_to_stdout
89
+
90
+ error_context = {
91
+ command: command,
92
+ function: function,
93
+ args: args,
94
+ time_taken: time_taken,
95
+ stderr_output: stderr ? stderr[0..50_000] : "", # Truncate to ~100kb
96
+ process_exit_value: process.to_s
97
+ }
98
+
99
+ response = JSON.parse(stdout)
100
+ return response["result"] if process.success?
101
+
102
+ raise HelperSubprocessFailed.new(
103
+ message: response["error"],
104
+ error_context: error_context
105
+ )
106
+ rescue JSON::ParserError
107
+ raise HelperSubprocessFailed.new(
108
+ message: stdout || "No output from command",
109
+ error_context: error_context
110
+ )
111
+ end
112
+
113
+ def self.excon_middleware
114
+ Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower]
115
+ end
116
+
117
+ def self.excon_defaults
118
+ {
119
+ connect_timeout: 5,
120
+ write_timeout: 5,
121
+ read_timeout: 5,
122
+ omit_default_port: true,
123
+ middlewares: excon_middleware
124
+ }
125
+ end
126
+
127
+ def self.with_git_configured(credentials:)
128
+ backup_git_config_path = stash_global_git_config
129
+ configure_git_to_use_https_with_credentials(credentials)
130
+ yield
131
+ ensure
132
+ reset_global_git_config(backup_git_config_path)
133
+ end
134
+
135
+ def self.configure_git_to_use_https_with_credentials(credentials)
136
+ configure_git_to_use_https
137
+ configure_git_credentials(credentials)
138
+ end
139
+
140
+ def self.configure_git_to_use_https
141
+ # Note: we use --global here (rather than --system) so that Dependabot
142
+ # can be run without privileged access
143
+ run_shell_command(
144
+ 'git config --global --replace-all url."https://github.com/".'\
145
+ "insteadOf ssh://git@github.com/ && "\
146
+ 'git config --global --add url."https://github.com/".'\
147
+ "insteadOf ssh://git@github.com: && "\
148
+ 'git config --global --add url."https://github.com/".'\
149
+ "insteadOf git@github.com: && "\
150
+ 'git config --global --add url."https://github.com/".'\
151
+ "insteadOf git@github.com/ && "\
152
+ 'git config --global --add url."https://github.com/".'\
153
+ "insteadOf git://github.com/"
154
+ )
155
+ end
156
+
157
+ def self.configure_git_credentials(credentials)
158
+ # Then add a file-based credential store that loads a file in this repo.
159
+ # Under the hood this uses git credential-store, but it's invoked through
160
+ # an wrapper binary that only allows non-mutative commands. Without this,
161
+ # whenever the credentials are deemed to be invalid, they're erased.
162
+ credential_helper_path =
163
+ File.join(__dir__, "../../bin/git-credential-store-immutable")
164
+ run_shell_command(
165
+ "git config --global credential.helper "\
166
+ "'#{credential_helper_path} --file=#{Dir.pwd}/git.store'"
167
+ )
168
+
169
+ # Build the content for our credentials file
170
+ git_store_content = ""
171
+ credentials.each do |cred|
172
+ next unless cred["type"] == "git_source"
173
+
174
+ authenticated_url =
175
+ "https://#{cred.fetch('username')}:#{cred.fetch('password')}"\
176
+ "@#{cred.fetch('host')}"
177
+
178
+ git_store_content += authenticated_url + "\n"
179
+ end
180
+
181
+ # Save the file
182
+ File.write("git.store", git_store_content)
183
+ end
184
+
185
+ def self.stash_global_git_config
186
+ return unless File.exist?(GIT_CONFIG_GLOBAL_PATH)
187
+
188
+ contents = File.read(GIT_CONFIG_GLOBAL_PATH)
189
+ digest = Digest::SHA2.hexdigest(contents)[0...10]
190
+ backup_path = GIT_CONFIG_GLOBAL_PATH + ".backup-#{digest}"
191
+
192
+ FileUtils.mv(GIT_CONFIG_GLOBAL_PATH, backup_path)
193
+ backup_path
194
+ end
195
+
196
+ def self.reset_global_git_config(backup_path)
197
+ return if backup_path.nil?
198
+ return unless File.exist?(backup_path)
199
+
200
+ FileUtils.mv(backup_path, GIT_CONFIG_GLOBAL_PATH)
201
+ end
202
+
203
+ def self.run_shell_command(command)
204
+ start = Time.now
205
+ stdout, process = Open3.capture2e(command)
206
+ time_taken = start - Time.now
207
+
208
+ # Raise an error with the output from the shell session if the
209
+ # command returns a non-zero status
210
+ return if process.success?
211
+
212
+ error_context = {
213
+ command: command,
214
+ time_taken: time_taken,
215
+ process_exit_value: process.to_s
216
+ }
217
+
218
+ raise SharedHelpers::HelperSubprocessFailed.new(
219
+ message: stdout,
220
+ error_context: error_context
221
+ )
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dependabot
4
+ class Source
5
+ GITHUB_SOURCE = %r{
6
+ (?<provider>github)
7
+ (?:\.com)[/:]
8
+ (?<repo>[\w.-]+/(?:(?!\.git|\.\s)[\w.-])+)
9
+ (?:(?:/tree|/blob)/(?<branch>[^/]+)/(?<directory>.*)[\#|/])?
10
+ }x.freeze
11
+
12
+ GITLAB_SOURCE = %r{
13
+ (?<provider>gitlab)
14
+ (?:\.com)[/:]
15
+ (?<repo>[^/\s]+/(?:(?!\.git|\.\s)[^/\s#"',])+)
16
+ (?:(?:/tree|/blob)/(?<branch>[^/]+)/(?<directory>.*)[\#|/])?
17
+ }x.freeze
18
+
19
+ BITBUCKET_SOURCE = %r{
20
+ (?<provider>bitbucket)
21
+ (?:\.org)[/:]
22
+ (?<repo>[^/\s]+/(?:(?!\.git|\.\s)[^/\s#"',])+)
23
+ (?:(?:/src)/(?<branch>[^/]+)/(?<directory>.*)[\#|/])?
24
+ }x.freeze
25
+
26
+ AZURE_SOURCE = %r{
27
+ (?<provider>azure)
28
+ (?:\.com)[/:]
29
+ (?<repo>[^/\s]+/([^/\s]+/)?(?:_git/)(?:(?!\.git|\.\s)[^/\s#?"',])+)
30
+ }x.freeze
31
+
32
+ SOURCE_REGEX = /
33
+ (?:#{GITHUB_SOURCE})|
34
+ (?:#{GITLAB_SOURCE})|
35
+ (?:#{BITBUCKET_SOURCE})|
36
+ (?:#{AZURE_SOURCE})
37
+ /x.freeze
38
+
39
+ attr_reader :provider, :repo, :directory, :branch, :hostname, :api_endpoint
40
+
41
+ def self.from_url(url_string)
42
+ return unless url_string&.match?(SOURCE_REGEX)
43
+
44
+ captures = url_string.match(SOURCE_REGEX).named_captures
45
+
46
+ new(
47
+ provider: captures.fetch("provider"),
48
+ repo: captures.fetch("repo"),
49
+ directory: captures.fetch("directory"),
50
+ branch: captures.fetch("branch")
51
+ )
52
+ end
53
+
54
+ def initialize(provider:, repo:, directory: nil, branch: nil, hostname: nil,
55
+ api_endpoint: nil)
56
+ if hostname.nil? ^ api_endpoint.nil?
57
+ msg = "Both hostname and api_endpoint must be specified if either "\
58
+ "are. Alternatively, both may be left blank to use the "\
59
+ "provider's defaults."
60
+ raise msg
61
+ end
62
+
63
+ @provider = provider
64
+ @repo = repo
65
+ @directory = directory
66
+ @branch = branch
67
+ @hostname = hostname || default_hostname(provider)
68
+ @api_endpoint = api_endpoint || default_api_endpoint(provider)
69
+ end
70
+
71
+ def url
72
+ case provider
73
+ when "github" then "https://github.com/" + repo
74
+ when "bitbucket" then "https://bitbucket.org/" + repo
75
+ when "gitlab" then "https://gitlab.com/" + repo
76
+ when "azure" then "https://dev.azure.com/" + repo
77
+ else raise "Unexpected repo provider '#{provider}'"
78
+ end
79
+ end
80
+
81
+ def organization
82
+ repo.split("/").first
83
+ end
84
+
85
+ def project
86
+ raise "Project is an Azure DevOps concept only" unless provider == "azure"
87
+
88
+ parts = repo.split("/_git/")
89
+ return parts.first.split("/").last if parts.first.split("/").count == 2
90
+
91
+ parts.last
92
+ end
93
+
94
+ def unscoped_repo
95
+ repo.split("/").last
96
+ end
97
+
98
+ private
99
+
100
+ def default_hostname(provider)
101
+ case provider
102
+ when "github" then "github.com"
103
+ when "bitbucket" then "bitbucket.org"
104
+ when "gitlab" then "gitlab.com"
105
+ when "azure" then "dev.azure.com"
106
+ else raise "Unexpected provider '#{provider}'"
107
+ end
108
+ end
109
+
110
+ def default_api_endpoint(provider)
111
+ case provider
112
+ when "github" then "https://api.github.com/"
113
+ when "bitbucket" then "https://api.bitbucket.org/2.0/"
114
+ when "gitlab" then "https://gitlab.com/api/v4"
115
+ when "azure" then "https://dev.azure.com/"
116
+ else raise "Unexpected provider '#{provider}'"
117
+ end
118
+ end
119
+ end
120
+ end