Skip to content

Commit 3c712ea

Browse files
authored
Merge pull request #72 from vohoanglong0107/refactor-remove-bors-state
refactor: remove GithubAppState
2 parents 2b3e9b2 + 0d28139 commit 3c712ea

File tree

11 files changed

+153
-170
lines changed

11 files changed

+153
-170
lines changed

docs/development.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@ Directory structure:
1515
- Communication with the GitHub API and definitions of GitHub webhook messages.
1616

1717
## Architecture diagram
18-
The following diagram shows a simplified view on the important state entities of Bors. `bors_process` handles events generated by webhooks. It uses a shared global state through `BorsContext`, which holds a shared connection to the database and a command parser. It also has access to `GithubAppState`, which has a map of repository state. Each repository state contains an API client for that repository, its loaded config, and permissions loaded from the Team API.
18+
The following diagram shows a simplified view on the important state entities of Bors. `bors_process` handles events generated by webhooks. It uses a shared global state through `BorsContext`, which holds a shared connection to the database and a command parser. It also has access to a map of repository state. Each repository state contains an API client for that repository, its loaded config, and permissions loaded from the Team API.
1919

2020
```mermaid
2121
---
2222
title: Important entities
2323
---
2424
flowchart
25-
GithubAppState
25+
BorsContext
2626
repo_client_1["GithubRepositoryClient"]
2727
repo_client_2["GithubRepositoryClient"]
2828
2929
repo_state_1["RepositoryState"]
3030
repo_state_2["RepositoryState"]
3131
32-
GithubAppState --> repo_state_1 --> repo_client_1 --> repo1
33-
GithubAppState --> repo_state_2 --> repo_client_2 --> repo2
32+
BorsContext --> repo_state_1 --> repo_client_1 --> repo1
33+
BorsContext --> repo_state_2 --> repo_client_2 --> repo2
3434
3535
repo_state_1 --> cr1["Config (repo 1)"]
3636
repo_state_1 --> pr1["Permissions (repo 1)"]
@@ -40,7 +40,6 @@ flowchart
4040
4141
BorsContext --> db[(Database)]
4242
43-
bors_process --> GithubAppState
4443
bors_process --> BorsContext
4544
```
4645

src/bin/bors.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use std::time::Duration;
55

66
use anyhow::Context;
77
use bors::{
8-
create_app, create_bors_process, BorsContext, BorsGlobalEvent, CommandParser, GithubAppState,
9-
SeaORMClient, ServerState, WebhookSecret,
8+
create_app, create_bors_process, create_github_client, BorsContext, BorsGlobalEvent,
9+
CommandParser, SeaORMClient, ServerState, WebhookSecret,
1010
};
1111
use clap::Parser;
1212
use sea_orm::Database;
@@ -71,15 +71,18 @@ fn try_main(opts: Opts) -> anyhow::Result<()> {
7171
let db = runtime
7272
.block_on(initialize_db(&opts.db))
7373
.context("Cannot initialize database")?;
74-
75-
let state = runtime
76-
.block_on(GithubAppState::load(
77-
opts.app_id.into(),
78-
opts.private_key.into_bytes().into(),
74+
let client = runtime.block_on(async move {
75+
create_github_client(opts.app_id.into(), opts.private_key.into_bytes().into())
76+
})?;
77+
78+
let ctx = runtime
79+
.block_on(BorsContext::new(
80+
CommandParser::new(opts.cmd_prefix),
81+
Arc::new(db),
82+
Arc::new(client),
7983
))
80-
.context("Cannot load GitHub repository state")?;
81-
let ctx = BorsContext::new(CommandParser::new(opts.cmd_prefix), Arc::new(db));
82-
let (repository_tx, global_tx, bors_process) = create_bors_process(state, ctx);
84+
.context("Cannot initialize bors context")?;
85+
let (repository_tx, global_tx, bors_process) = create_bors_process(ctx);
8386

8487
let refresh_tx = global_tx.clone();
8588
let refresh_process = async move {

src/bors/context.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
1-
use crate::{bors::command::CommandParser, database::DbClient};
2-
use std::sync::Arc;
1+
use crate::{bors::command::CommandParser, database::DbClient, github::GithubRepoName};
2+
use std::{
3+
collections::HashMap,
4+
sync::{Arc, RwLock},
5+
};
36

4-
pub struct BorsContext {
7+
use super::{RepositoryClient, RepositoryLoader, RepositoryState};
8+
9+
pub struct BorsContext<Client: RepositoryClient> {
510
pub parser: CommandParser,
611
pub db: Arc<dyn DbClient>,
12+
pub repository_loader: Arc<dyn RepositoryLoader<Client>>,
13+
pub repositories: RwLock<HashMap<GithubRepoName, Arc<RepositoryState<Client>>>>,
714
}
815

9-
impl BorsContext {
10-
pub fn new(parser: CommandParser, db: Arc<dyn DbClient>) -> Self {
11-
Self { parser, db }
16+
impl<Client: RepositoryClient> BorsContext<Client> {
17+
pub async fn new(
18+
parser: CommandParser,
19+
db: Arc<dyn DbClient>,
20+
global_client: Arc<dyn RepositoryLoader<Client>>,
21+
) -> anyhow::Result<Self> {
22+
let repositories = global_client.load_repositories().await?;
23+
let repositories = RwLock::new(repositories);
24+
Ok(Self {
25+
parser,
26+
db,
27+
repository_loader: global_client,
28+
repositories,
29+
})
1230
}
1331
}

src/bors/handlers/mod.rs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ use crate::bors::handlers::trybuild::{command_try_build, command_try_cancel, TRY
1111
use crate::bors::handlers::workflow::{
1212
handle_check_suite_completed, handle_workflow_completed, handle_workflow_started,
1313
};
14-
use crate::bors::{BorsContext, BorsState, Comment, RepositoryClient, RepositoryState};
14+
use crate::bors::{BorsContext, Comment, RepositoryClient, RepositoryState};
1515
use crate::database::DbClient;
16-
use crate::github::GithubRepoName;
1716
use crate::utils::logging::LogError;
1817

1918
mod help;
@@ -26,11 +25,16 @@ mod workflow;
2625
/// This function executes a single BORS repository event
2726
pub async fn handle_bors_repository_event<Client: RepositoryClient>(
2827
event: BorsRepositoryEvent,
29-
state: Arc<dyn BorsState<Client>>,
30-
ctx: Arc<BorsContext>,
28+
ctx: Arc<BorsContext<Client>>,
3129
) -> anyhow::Result<()> {
3230
let db = Arc::clone(&ctx.db);
33-
let Some(repo) = get_repo_state(state, event.repository()) else {
31+
let Some(repo) = ctx
32+
.repositories
33+
.read()
34+
.unwrap()
35+
.get(event.repository())
36+
.map(Arc::clone)
37+
else {
3438
return Err(anyhow::anyhow!(
3539
"Repository {} not found in the bot state",
3640
event.repository()
@@ -113,20 +117,29 @@ pub async fn handle_bors_repository_event<Client: RepositoryClient>(
113117
/// This function executes a single BORS global event
114118
pub async fn handle_bors_global_event<Client: RepositoryClient>(
115119
event: BorsGlobalEvent,
116-
state: Arc<dyn BorsState<Client>>,
117-
ctx: Arc<BorsContext>,
120+
ctx: Arc<BorsContext<Client>>,
118121
) -> anyhow::Result<()> {
119122
let db = Arc::clone(&ctx.db);
120123
match event {
121124
BorsGlobalEvent::InstallationsChanged => {
122-
let span = tracing::info_span!("Repository reload");
123-
if let Err(error) = state.reload_repositories().instrument(span.clone()).await {
124-
span.log_error(error);
125+
let span = tracing::info_span!("Repository reload").entered();
126+
127+
match ctx.repository_loader.load_repositories().await {
128+
Ok(repos) => {
129+
let mut repositories = ctx.repositories.write().unwrap();
130+
// TODO: keep old repos in case of repo loading failure
131+
*repositories = repos;
132+
}
133+
Err(error) => {
134+
span.log_error(error);
135+
}
125136
}
137+
span.exit();
126138
}
127139
BorsGlobalEvent::Refresh => {
128140
let span = tracing::info_span!("Refresh");
129-
let repos = state.get_all_repos();
141+
let repos: Vec<Arc<RepositoryState<Client>>> =
142+
ctx.repositories.read().unwrap().values().cloned().collect();
130143
futures::future::join_all(repos.into_iter().map(|repo| {
131144
let repo = Arc::clone(&repo);
132145
async {
@@ -143,23 +156,10 @@ pub async fn handle_bors_global_event<Client: RepositoryClient>(
143156
Ok(())
144157
}
145158

146-
fn get_repo_state<Client: RepositoryClient>(
147-
state: Arc<dyn BorsState<Client>>,
148-
repo: &GithubRepoName,
149-
) -> Option<Arc<RepositoryState<Client>>> {
150-
match state.get_repo_state(repo) {
151-
Some(result) => Some(result),
152-
None => {
153-
tracing::warn!("Repository {} not found", repo);
154-
None
155-
}
156-
}
157-
}
158-
159159
async fn handle_comment<Client: RepositoryClient>(
160160
repo: Arc<RepositoryState<Client>>,
161161
database: Arc<dyn DbClient>,
162-
ctx: Arc<BorsContext>,
162+
ctx: Arc<BorsContext<Client>>,
163163
comment: PullRequestComment,
164164
) -> anyhow::Result<()> {
165165
let pr_number = comment.pr_number;

src/bors/mod.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod context;
44
pub mod event;
55
mod handlers;
66

7+
use std::collections::HashMap;
78
use std::sync::Arc;
89

910
use arc_swap::ArcSwap;
@@ -70,6 +71,16 @@ pub trait RepositoryClient: Send + Sync {
7071
fn get_workflow_url(&self, run_id: RunId) -> String;
7172
}
7273

74+
/// Temporary trait to sastify the test mocking.
75+
/// TODO: Remove this trait once we move to mock REST API call.
76+
#[async_trait]
77+
pub trait RepositoryLoader<Client: RepositoryClient>: Send + Sync {
78+
/// Load state of repositories.
79+
async fn load_repositories(
80+
&self,
81+
) -> anyhow::Result<HashMap<GithubRepoName, Arc<RepositoryState<Client>>>>;
82+
}
83+
7384
#[derive(Clone)]
7485
pub enum CheckSuiteStatus {
7586
Pending,
@@ -84,20 +95,6 @@ pub struct CheckSuite {
8495
pub(crate) status: CheckSuiteStatus,
8596
}
8697

87-
/// Main state holder for the bot.
88-
/// It is behind a trait to allow easier mocking in tests.
89-
#[async_trait]
90-
pub trait BorsState<Client: RepositoryClient>: Send + Sync {
91-
/// Get repository and database state for the given repository name.
92-
fn get_repo_state(&self, repo: &GithubRepoName) -> Option<Arc<RepositoryState<Client>>>;
93-
94-
/// Get all repositories.
95-
fn get_all_repos(&self) -> Vec<Arc<RepositoryState<Client>>>;
96-
97-
/// Reload state of repositories due to some external change.
98-
async fn reload_repositories(&self) -> anyhow::Result<()>;
99-
}
100-
10198
/// An access point to a single repository.
10299
/// Can be used to query permissions for the repository, and also to perform various
103100
/// actions using the stored client.

src/github/api/client.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
use std::collections::HashMap;
2+
use std::sync::Arc;
3+
14
use anyhow::Context;
25
use axum::async_trait;
36
use octocrab::models::{App, Repository, RunId};
47
use octocrab::{Error, Octocrab};
58
use tracing::log;
69

710
use crate::bors::event::PullRequestComment;
8-
use crate::bors::{CheckSuite, CheckSuiteStatus, Comment, RepositoryClient};
11+
use crate::bors::{
12+
CheckSuite, CheckSuiteStatus, Comment, RepositoryClient, RepositoryLoader, RepositoryState,
13+
};
914
use crate::config::{RepositoryConfig, CONFIG_FILE_PATH};
1015
use crate::github::api::base_github_html_url;
1116
use crate::github::api::operations::{merge_branches, set_branch_to_commit, MergeError};
1217
use crate::github::{Branch, CommitSha, GithubRepoName, PullRequest, PullRequestNumber};
1318

19+
use super::load_repositories;
20+
1421
/// Provides access to a single app installation (repository) using the GitHub API.
1522
pub struct GithubRepositoryClient {
1623
pub app: App,
@@ -263,6 +270,15 @@ impl RepositoryClient for GithubRepositoryClient {
263270
}
264271
}
265272

273+
#[async_trait]
274+
impl RepositoryLoader<GithubRepositoryClient> for Octocrab {
275+
async fn load_repositories(
276+
&self,
277+
) -> anyhow::Result<HashMap<GithubRepoName, Arc<RepositoryState<GithubRepositoryClient>>>> {
278+
load_repositories(self).await
279+
}
280+
}
281+
266282
fn github_pr_to_pr(pr: octocrab::models::pulls::PullRequest) -> PullRequest {
267283
PullRequest {
268284
number: pr.number.into(),

src/github/api/mod.rs

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ use std::sync::Arc;
33

44
use anyhow::Context;
55
use arc_swap::ArcSwap;
6-
use axum::async_trait;
76
use octocrab::models::{App, AppId, InstallationRepositories, Repository};
87
use octocrab::Octocrab;
98
use secrecy::{ExposeSecret, SecretVec};
109

1110
use client::GithubRepositoryClient;
1211

13-
use crate::bors::{BorsState, RepositoryClient, RepositoryState};
12+
use crate::bors::{RepositoryClient, RepositoryState};
1413
use crate::github::GithubRepoName;
1514
use crate::permissions::load_permissions;
1615

@@ -19,39 +18,24 @@ pub(crate) mod operations;
1918

2019
type GithubRepositoryState = RepositoryState<GithubRepositoryClient>;
2120

22-
type RepositoryMap = HashMap<GithubRepoName, Arc<GithubRepositoryState>>;
21+
type GithubRepositoryMap = HashMap<GithubRepoName, Arc<GithubRepositoryState>>;
2322

2423
fn base_github_html_url() -> &'static str {
2524
"https://github.com"
2625
}
2726

28-
/// Provides access to managed GitHub repositories.
29-
pub struct GithubAppState {
30-
client: Octocrab,
31-
repositories: ArcSwap<RepositoryMap>,
32-
}
27+
pub fn create_github_client(app_id: AppId, private_key: SecretVec<u8>) -> anyhow::Result<Octocrab> {
28+
let key = jsonwebtoken::EncodingKey::from_rsa_pem(private_key.expose_secret().as_ref())
29+
.context("Could not encode private key")?;
3330

34-
impl GithubAppState {
35-
/// Loads repositories managed by the Bors GitHub app with the given ID.
36-
pub async fn load(app_id: AppId, private_key: SecretVec<u8>) -> anyhow::Result<GithubAppState> {
37-
let key = jsonwebtoken::EncodingKey::from_rsa_pem(private_key.expose_secret().as_ref())
38-
.context("Could not encode private key")?;
39-
40-
let client = Octocrab::builder()
41-
.app(app_id, key)
42-
.build()
43-
.context("Could not create octocrab builder")?;
44-
45-
let repositories = load_repositories(&client).await?;
46-
Ok(GithubAppState {
47-
client,
48-
repositories: ArcSwap::new(Arc::new(repositories)),
49-
})
50-
}
31+
Octocrab::builder()
32+
.app(app_id, key)
33+
.build()
34+
.context("Could not create octocrab builder")
5135
}
5236

5337
/// Loads repositories that are connected to the given GitHub App client.
54-
pub async fn load_repositories(client: &Octocrab) -> anyhow::Result<RepositoryMap> {
38+
pub async fn load_repositories(client: &Octocrab) -> anyhow::Result<GithubRepositoryMap> {
5539
let installations = client
5640
.apps()
5741
.installations()
@@ -179,27 +163,3 @@ async fn create_repo_state(
179163
permissions: ArcSwap::new(Arc::new(permissions)),
180164
})
181165
}
182-
183-
#[async_trait]
184-
impl BorsState<GithubRepositoryClient> for GithubAppState {
185-
fn get_repo_state(
186-
&self,
187-
repo: &GithubRepoName,
188-
) -> Option<Arc<RepositoryState<GithubRepositoryClient>>> {
189-
self.repositories
190-
.load()
191-
.get(repo)
192-
.map(|repo| Arc::clone(&repo))
193-
}
194-
195-
fn get_all_repos(&self) -> Vec<Arc<RepositoryState<GithubRepositoryClient>>> {
196-
self.repositories.load().values().cloned().collect()
197-
}
198-
199-
/// Re-download information about repositories connected to this GitHub app.
200-
async fn reload_repositories(&self) -> anyhow::Result<()> {
201-
self.repositories
202-
.store(Arc::new(load_repositories(&self.client).await?));
203-
Ok(())
204-
}
205-
}

src/github/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ pub mod server;
1111
mod webhook;
1212

1313
pub use api::operations::MergeError;
14-
pub use api::GithubAppState;
1514
pub use labels::{LabelModification, LabelTrigger};
1615
pub use webhook::WebhookSecret;
1716

0 commit comments

Comments
 (0)