-
Notifications
You must be signed in to change notification settings - Fork 920
/
github.rb
295 lines (252 loc) 路 8.89 KB
/
github.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# typed: strict
# frozen_string_literal: true
require "octokit"
require "sorbet-runtime"
require "dependabot/clients/github_with_retries"
require "dependabot/pull_request_creator/commit_signer"
require "dependabot/pull_request_updater"
module Dependabot
class PullRequestUpdater
class Github
extend T::Sig
sig { returns(Dependabot::Source) }
attr_reader :source
sig { returns(T::Array[Dependabot::DependencyFile]) }
attr_reader :files
sig { returns(String) }
attr_reader :base_commit
sig { returns(String) }
attr_reader :old_commit
sig { returns(T::Array[Dependabot::Credential]) }
attr_reader :credentials
sig { returns(Integer) }
attr_reader :pull_request_number
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
attr_reader :author_details
sig { returns(T.nilable(String)) }
attr_reader :signature_key
sig do
params(
source: Dependabot::Source,
base_commit: String,
old_commit: String,
files: T::Array[Dependabot::DependencyFile],
credentials: T::Array[Dependabot::Credential],
pull_request_number: Integer,
author_details: T.nilable(T::Hash[Symbol, T.untyped]),
signature_key: T.nilable(String)
)
.void
end
def initialize(source:, base_commit:, old_commit:, files:,
credentials:, pull_request_number:,
author_details: nil, signature_key: nil)
@source = source
@base_commit = base_commit
@old_commit = old_commit
@files = files
@credentials = credentials
@pull_request_number = pull_request_number
@author_details = author_details
@signature_key = signature_key
end
sig { returns(T.nilable(Sawyer::Resource)) }
def update
return unless pull_request_exists?
return unless branch_exists?(pull_request.head.ref)
commit = create_commit
branch = update_branch(commit)
update_pull_request_target_branch
branch
end
private
sig { void }
def update_pull_request_target_branch
target_branch = source.branch || pull_request.base.repo.default_branch
return if target_branch == pull_request.base.ref
T.unsafe(github_client_for_source).update_pull_request(
source.repo,
pull_request_number,
base: target_branch
)
rescue Octokit::UnprocessableEntity => e
handle_pr_update_error(e)
end
sig { params(error: Octokit::Error).void }
def handle_pr_update_error(error)
# Return quietly if the PR has been closed
return if error.message.match?(/closed pull request/i)
# Ignore cases where the target branch has been deleted
return if error.message.include?("field: base") &&
source.branch &&
!branch_exists?(T.must(source.branch))
raise error
end
sig { returns(Dependabot::Clients::GithubWithRetries) }
def github_client_for_source
@github_client_for_source ||=
T.let(
Dependabot::Clients::GithubWithRetries.for_source(
source: source,
credentials: credentials
),
T.nilable(Dependabot::Clients::GithubWithRetries)
)
end
sig { returns(T::Boolean) }
def pull_request_exists?
pull_request
true
rescue Octokit::NotFound
false
end
sig { returns(T.untyped) }
def pull_request
@pull_request ||=
T.let(
T.unsafe(github_client_for_source).pull_request(
source.repo,
pull_request_number
),
T.untyped
)
end
sig { params(name: String).returns(T::Boolean) }
def branch_exists?(name)
T.unsafe(github_client_for_source).branch(source.repo, name)
true
rescue Octokit::NotFound
false
end
sig { returns(T.untyped) }
def create_commit
tree = create_tree
options = author_details&.any? ? { author: author_details } : {}
if options[:author]&.any? && signature_key
options[:author][:date] = Time.now.utc.iso8601
options[:signature] = commit_signature(tree, options[:author])
end
begin
T.unsafe(github_client_for_source).create_commit(
source.repo,
commit_message,
tree.sha,
base_commit,
options
)
rescue Octokit::UnprocessableEntity => e
raise unless e.message == "Tree SHA does not exist"
# Sometimes a race condition on GitHub's side means we get an error
# here. No harm in retrying if we do.
retry_count ||= 0
retry_count += 1
raise if retry_count > 10
sleep(rand(1..1.99))
retry
end
end
sig { returns(T.untyped) }
def create_tree
file_trees = files.map do |file|
if file.type == "submodule"
{
path: file.path.sub(%r{^/}, ""),
mode: Dependabot::DependencyFile::Mode::SUBMODULE,
type: "commit",
sha: file.content
}
else
content = if file.operation == Dependabot::DependencyFile::Operation::DELETE
{ sha: nil }
elsif file.binary?
sha = T.unsafe(github_client_for_source).create_blob(
source.repo, file.content, "base64"
)
{ sha: sha }
else
{ content: file.content }
end
{
path: file.realpath,
mode: Dependabot::DependencyFile::Mode::FILE,
type: "blob"
}.merge(content)
end
end
T.unsafe(github_client_for_source).create_tree(
source.repo,
file_trees,
base_tree: base_commit
)
end
BRANCH_PROTECTION_ERROR_MESSAGES = T.let(
[
/protected branch/i,
/not authorized to push/i,
/must not contain merge commits/i,
/required status check/i,
/cannot force-push to this branch/i,
/pull request for this branch has been added to a merge queue/i,
# Unverified commits can be present when PR contains commits from other authors
/commits must have verified signatures/i,
/changes must be made through a pull request/i
].freeze,
T::Array[Regexp]
)
sig { params(commit: T.untyped).returns(T.untyped) }
def update_branch(commit)
T.unsafe(github_client_for_source).update_ref(
source.repo,
"heads/" + pull_request.head.ref,
commit.sha,
true
)
rescue Octokit::UnprocessableEntity => e
# Return quietly if the branch has been deleted or merged
return nil if e.message.match?(/Reference does not exist/i)
return nil if e.message.match?(/Reference cannot be updated/i)
raise BranchProtected, e.message if BRANCH_PROTECTION_ERROR_MESSAGES.any? { |msg| e.message.match?(msg) }
raise
end
sig { returns(String) }
def commit_message
fallback_message =
"#{pull_request.title}" \
"\n\n" \
"Dependabot couldn't find the original pull request head commit, " \
"#{old_commit}."
# Take the commit message from the old commit. If the old commit can't
# be found, use the PR title as the commit message.
commit_being_updated&.message || fallback_message
end
sig { returns(T.untyped) }
def commit_being_updated
return @commit_being_updated if defined?(@commit_being_updated)
@commit_being_updated =
T.let(
if pull_request.commits == 1
T.unsafe(github_client_for_source)
.git_commit(source.repo, pull_request.head.sha)
else
commits =
T.unsafe(github_client_for_source)
.pull_request_commits(source.repo, pull_request_number)
commit = commits.find { |c| c.sha == old_commit }
commit&.commit
end,
T.untyped
)
end
sig { params(tree: T.untyped, author_details_with_date: T::Hash[Symbol, T.untyped]).returns(String) }
def commit_signature(tree, author_details_with_date)
PullRequestCreator::CommitSigner.new(
author_details: author_details_with_date,
commit_message: commit_message,
tree_sha: tree.sha,
parent_sha: base_commit,
signature_key: T.must(signature_key)
).signature
end
end
end
end