Skip to content

Commit 45f059f

Browse files
Merge pull request #107 from RodrigoMNardi/feature/allow-multiple-repos-and-ci
Allow multiples repos and GitHub Apps
2 parents 6835daa + b448988 commit 45f059f

File tree

11 files changed

+242
-19
lines changed

11 files changed

+242
-19
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,32 @@ If you need to uninstall RVM, you can do so with the following command:
209209
rvm implode
210210
```
211211

212+
# Multiple Repositories
213+
214+
## GitHub Repositories
215+
216+
The GitHub Hook Server is designed to work with multiple repositories.
217+
To add a new repository, you need to create a new entry in the `config.yml` file.
218+
The `config.yml` file is located in the root directory of the project.
219+
220+
### Example Configuration
221+
222+
```yaml
223+
github_apps:
224+
- login: 1234567
225+
cert: github_private_key.pem
226+
repo: 'opensourcerouting/frr-ci-test'
227+
228+
- login: 9876543
229+
cert: github_private_key.pem
230+
repo: 'RodrigoMNardi/frr'
231+
```
232+
233+
### Configuration Parameters
234+
- `login`: The GitHub login for the application.
235+
- `cert`: The path to the private key file for the GitHub application.
236+
- `repo`: The GitHub repository to monitor.
237+
212238
# Console
213239

214240
The bin/console script allows you to interact with the application in an interactive Ruby (IRB) session.

lib/github/build_plan.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ def create_pull_request
9797
end
9898

9999
def start_new_execution
100+
create_pull_request if @pull_request.nil?
101+
102+
@check_suite.pull_request = @pull_request
103+
100104
Github::UserInfo.new(@payload.dig('pull_request', 'user', 'id'), check_suite: @check_suite)
101105

102106
@bamboo_plan_run = BambooCi::PlanRun.new(@check_suite, logger_level: @logger.level)

lib/github/check.rb

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,19 @@
1818
require_relative '../helpers/configuration'
1919

2020
module Github
21+
# Class responsible for interacting with GitHub's Check API.
22+
#
23+
# This class provides methods to authenticate with GitHub, fetch pull request information,
24+
# manage check runs, and handle comments and reactions on pull requests.
25+
#
26+
# @attr_reader [Object] app the authenticated GitHub app client.
27+
# @attr_reader [Object] check_suite the check suite associated with the pull request.
2128
class Check
2229
attr_reader :app, :check_suite
2330

31+
# Initializes a new Github::Check object.
32+
#
33+
# @param check_suite [Object] the check suite associated with the pull request.
2434
def initialize(check_suite)
2535
@check_suite = check_suite
2636
@config = GitHubApp::Configuration.instance.config
@@ -29,10 +39,21 @@ def initialize(check_suite)
2939
authenticate_app
3040
end
3141

42+
# Fetches information about a pull request.
43+
#
44+
# @param pr_id [Integer] the pull request ID.
45+
# @param repo [String] the repository name.
46+
# @return [Hash] the pull request information.
3247
def pull_request_info(pr_id, repo)
3348
@app.pull_request(repo, pr_id).to_h
3449
end
3550

51+
# Fetches commits associated with a pull request.
52+
#
53+
# @param pr_id [Integer] the pull request ID.
54+
# @param repo [String] the repository name.
55+
# @param page [Integer] the page number for pagination.
56+
# @return [Array<Hash>] the list of commits.
3657
def fetch_pull_request_commits(pr_id, repo, page)
3758
@app.pull_request_commits(
3859
repo,
@@ -42,6 +63,12 @@ def fetch_pull_request_commits(pr_id, repo, page)
4263
)
4364
end
4465

66+
# Adds a comment to a pull request.
67+
#
68+
# @param pr_id [Integer] the pull request ID.
69+
# @param comment [String] the comment text.
70+
# @param repo [String] the repository name.
71+
# @return [Hash] the added comment information.
4572
def add_comment(pr_id, comment, repo)
4673
@app.add_comment(
4774
repo,
@@ -50,18 +77,36 @@ def add_comment(pr_id, comment, repo)
5077
).to_h
5178
end
5279

80+
# Adds a thumbs-up reaction to a comment.
81+
#
82+
# @param repo [String] the repository name.
83+
# @param comment_id [Integer] the comment ID.
5384
def comment_reaction_thumb_up(repo, comment_id)
5485
@app.create_issue_comment_reaction(repo, comment_id, '+1')
5586
end
5687

88+
# Adds a thumbs-down reaction to a comment.
89+
#
90+
# @param repo [String] the repository name.
91+
# @param comment_id [Integer] the comment ID.
5792
def comment_reaction_thumb_down(repo, comment_id)
5893
@app.create_issue_comment_reaction(repo, comment_id, '-1')
5994
end
6095

96+
# Fetches check runs for a specific commit SHA.
97+
#
98+
# @param repo [String] the repository name.
99+
# @param sha [String] the commit SHA.
100+
# @param status [String] the status of the check runs to fetch.
101+
# @return [Array<Hash>] the list of check runs.
61102
def check_runs_for_ref(repo, sha, status: 'queued')
62103
@app.check_runs_for_ref(repo, sha, status: status)
63104
end
64105

106+
# Creates a new check run.
107+
#
108+
# @param name [String] the name of the check run.
109+
# @return [Hash] the created check run information.
65110
def create(name)
66111
@app.create_check_run(
67112
@check_suite.pull_request.repository,
@@ -70,30 +115,64 @@ def create(name)
70115
)
71116
end
72117

118+
# Updates the status of a check run to 'queued'.
119+
#
120+
# @param check_ref [Integer] the check run reference ID.
121+
# @param output [Hash] the output information for the check run.
122+
# @return [Hash] the updated check run information.
73123
def queued(check_ref, output = {})
74124
basic_status(check_ref, 'queued', output)
75125
end
76126

127+
# Updates the status of a check run to 'in_progress'.
128+
#
129+
# @param check_ref [Integer] the check run reference ID.
130+
# @param output [Hash] the output information for the check run.
131+
# @return [Hash] the updated check run information.
77132
def in_progress(check_ref, output = {})
78133
basic_status(check_ref, 'in_progress', output)
79134
end
80135

136+
# Updates the status of a check run to 'cancelled'.
137+
#
138+
# @param check_ref [Integer] the check run reference ID.
139+
# @param output [Hash] the output information for the check run.
140+
# @return [Hash] the updated check run information.
81141
def cancelled(check_ref, output = {})
82142
completed(check_ref, 'completed', 'cancelled', output)
83143
end
84144

145+
# Updates the status of a check run to 'success'.
146+
#
147+
# @param check_ref [Integer] the check run reference ID.
148+
# @param output [Hash] the output information for the check run.
149+
# @return [Hash] the updated check run information.
85150
def success(check_ref, output = {})
86151
completed(check_ref, 'completed', 'success', output)
87152
end
88153

154+
# Updates the status of a check run to 'failure'.
155+
#
156+
# @param check_ref [Integer] the check run reference ID.
157+
# @param output [Hash] the output information for the check run.
158+
# @return [Hash] the updated check run information.
89159
def failure(check_ref, output = {})
90160
completed(check_ref, 'completed', 'failure', output)
91161
end
92162

163+
# Updates the status of a check run to 'skipped'.
164+
#
165+
# @param check_ref [Integer] the check run reference ID.
166+
# @param output [Hash] the output information for the check run.
167+
# @return [Hash] the updated check run information.
93168
def skipped(check_ref, output = {})
94169
completed(check_ref, 'completed', 'skipped', output)
95170
end
96171

172+
# Fetches a specific check run.
173+
#
174+
# @param check_ref [Integer] the check run reference ID.
175+
# @return [Hash] the check run information.
97176
def get_check_run(check_ref)
98177
@app.check_run(@check_suite.pull_request.repository, check_ref).to_h
99178
end
@@ -106,6 +185,10 @@ def signature
106185
@config.dig('auth_signature', 'password')
107186
end
108187

188+
# Fetches the username associated with a GitHub user.
189+
#
190+
# @param username [String] the GitHub username.
191+
# @return [Hash, false] the user information if found, otherwise false.
109192
def fetch_username(username)
110193
@app.user(username)
111194
rescue StandardError
@@ -114,6 +197,12 @@ def fetch_username(username)
114197

115198
private
116199

200+
# Updates the status of a check run.
201+
#
202+
# @param check_ref [Integer] the check run reference ID.
203+
# @param status [String] the status of the check run.
204+
# @param output [Hash] the output information for the check run.
205+
# @return [Hash] the updated check run information.
117206
def basic_status(check_ref, status, output)
118207
opts = {
119208
status: status
@@ -133,6 +222,13 @@ def basic_status(check_ref, status, output)
133222
resp
134223
end
135224

225+
# Completes a check run with a specific conclusion.
226+
#
227+
# @param check_ref [Integer] the check run reference ID.
228+
# @param status [String] the status of the check run.
229+
# @param conclusion [String] the conclusion of the check run.
230+
# @param output [Hash] the output information for the check run.
231+
# @return [Hash] the updated check run information.
136232
# PS: Conclusion and status are the same name from GitHub Check doc.
137233
# https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28#update-a-check-run
138234
def completed(check_ref, status, conclusion, output)
@@ -163,26 +259,68 @@ def completed(check_ref, status, conclusion, output)
163259
end
164260
end
165261

262+
# Authenticates the GitHub app.
263+
#
264+
# This method attempts to authenticate the GitHub app by repository. If the
265+
# authentication by repository fails or the check suite is nil, it falls back
266+
# to authenticating the default GitHub app.
267+
#
268+
# @raise [RuntimeError] if the GitHub authentication fails.
166269
def authenticate_app
167-
@config['github_apps'].each do |app|
168-
payload = generate_payload(app)
270+
github_app_by_repo
271+
272+
github_default_app if @check_suite.nil? or @app.nil?
273+
end
169274

170-
rsa = OpenSSL::PKey::RSA.new(File.read(app['cert']))
275+
# Authenticates the GitHub app by repository.
276+
#
277+
# This method finds the GitHub app configuration for the repository associated
278+
# with the check suite and creates a GitHub app client using that configuration.
279+
def github_app_by_repo
280+
app =
281+
@config['github_apps'].find do |entry|
282+
entry.key? 'repo' and entry['repo'] == @check_suite.pull_request.repository
283+
end
171284

172-
jwt = JWT.encode(payload, rsa, 'RS256')
285+
@logger.info("github_app_by_repo: #{app.inspect}")
173286

174-
authenticate(jwt)
287+
create_app(app) unless app.nil?
288+
end
289+
290+
def github_default_app
291+
@config['github_apps'].each do |app|
292+
create_app(app)
175293

176294
break unless @app.nil?
177295
end
178296

179297
raise 'Github Authentication Failed' if @app.nil?
180298
end
181299

300+
# Creates a GitHub app client.
301+
#
302+
# @param app [Hash] the app configuration.
303+
def create_app(app)
304+
payload = generate_payload(app)
305+
306+
rsa = OpenSSL::PKey::RSA.new(File.read(app['cert']))
307+
308+
jwt = JWT.encode(payload, rsa, 'RS256')
309+
310+
authenticate(jwt)
311+
end
312+
313+
# Generates a payload for authentication.
314+
#
315+
# @param app [Hash] the app configuration.
316+
# @return [Hash] the generated payload.
182317
def generate_payload(app)
183318
{ iat: Time.now.to_i, exp: Time.now.to_i + (10 * 60) - 30, iss: app['login'] }
184319
end
185320

321+
# Authenticates the GitHub app with a JWT.
322+
#
323+
# @param jwt [String] the JWT token.
186324
def authenticate(jwt)
187325
@authenticate_app = Octokit::Client.new(bearer_token: jwt)
188326

@@ -195,6 +333,12 @@ def authenticate(jwt)
195333
@app = Octokit::Client.new(bearer_token: token)
196334
end
197335

336+
# Sends an update to GitHub for a check run.
337+
#
338+
# @param check_ref [Integer] the check run reference ID.
339+
# @param opts [Hash] the options for the update.
340+
# @param conclusion [String] the conclusion of the check run.
341+
# @return [Hash] the updated check run information.
198342
def send_update(check_ref, opts, conclusion)
199343
resp =
200344
@app.update_check_run(

lib/github/parsers/pull_request_commit.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,25 @@
1010

1111
module Github
1212
module Parsers
13+
# Class responsible for parsing pull request commits.
1314
class PullRequestCommit
15+
# Initializes a new PullRequestCommit parser.
16+
#
17+
# @param repo [String] the repository name.
18+
# @param pr_id [Integer] the pull request ID.
1419
def initialize(repo, pr_id)
1520
@repo = repo
1621
@pr_id = pr_id
1722

18-
@github_check = Github::Check.new(nil)
23+
pull_request = PullRequest.find_by(github_pr_id: pr_id)
24+
25+
@github_check = Github::Check.new(pull_request.check_suites.last)
1926
end
2027

28+
# Finds a commit by its SHA.
29+
#
30+
# @param sha256 [String] the SHA256 hash of the commit.
31+
# @return [Hash, nil] the commit data if found, otherwise nil.
2132
def find_by_sha(sha256)
2233
return nil if sha256.nil?
2334

@@ -37,6 +48,9 @@ def find_by_sha(sha256)
3748
nil
3849
end
3950

51+
# Retrieves the last commit in the pull request.
52+
#
53+
# @return [Hash, nil] the last commit data if found, otherwise nil.
4054
def last_commit_in_pr
4155
page = 1
4256
last_commit = nil

lib/github/re_run/comment.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def sha256_or_comment?
5454

5555
def comment_flow
5656
commit = fetch_last_commit_or_sha256
57-
github_check = Github::Check.new(nil)
57+
github_check = fetch_github_check
5858
pull_request_info = github_check.pull_request_info(pr_id, repo)
5959
pull_request = fetch_or_create_pr(pull_request_info)
6060

@@ -68,6 +68,20 @@ def comment_flow
6868
check_suite
6969
end
7070

71+
# Fetches the GitHub check associated with the pull request.
72+
#
73+
# This method finds the pull request by its GitHub PR ID and then retrieves
74+
# the last check suite associated with that pull request. It then initializes
75+
# a new `Github::Check` object with the last check suite.
76+
#
77+
# @return [Github::Check] the GitHub check associated with the pull request.
78+
#
79+
# @raise [ActiveRecord::RecordNotFound] if the pull request is not found.
80+
def fetch_github_check
81+
pull_request = PullRequest.find_by(github_pr_id: pr_id)
82+
Github::Check.new(pull_request.check_suites.last)
83+
end
84+
7185
def create_check_suite_by_commit(commit, pull_request, pull_request_info)
7286
CheckSuite.create(
7387
pull_request: pull_request,

0 commit comments

Comments
 (0)