Skip to content

Commit b92cc90

Browse files
author
Vo Hoang Long
committed
feat: allow a ci dedicated repo to run workflow
1 parent ea91f11 commit b92cc90

File tree

15 files changed

+238
-124
lines changed

15 files changed

+238
-124
lines changed

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ chrono = "0.4"
5454

5555
itertools = "0.13.0"
5656

57+
derive_builder = "0.20.0"
58+
5759
[dev-dependencies]
5860
insta = "1.26"
59-
derive_builder = "0.20.0"
6061
wiremock = "0.6.0"
6162
base64 = "0.22.1"
6263
tracing-test = "0.2.4"

src/bin/bors.rs

+39-13
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ use std::time::Duration;
66

77
use anyhow::Context;
88
use bors::{
9-
create_app, create_bors_process, create_github_client, load_repositories, BorsContext,
10-
BorsGlobalEvent, CommandParser, PgDbClient, ServerState, TeamApiClient, WebhookSecret,
9+
create_app, create_bors_process, create_github_client, create_github_client_from_access_token,
10+
load_repositories, BorsContextBuilder, BorsGlobalEvent, CommandParser, PgDbClient, ServerState,
11+
TeamApiClient, WebhookSecret,
1112
};
1213
use clap::Parser;
1314
use sqlx::postgres::PgConnectOptions;
@@ -18,6 +19,8 @@ use tracing_subscriber::filter::EnvFilter;
1819
/// How often should the bot check DB state, e.g. for handling timeouts.
1920
const PERIODIC_REFRESH: Duration = Duration::from_secs(120);
2021

22+
const GITHUB_API_URL: &str = "https://api.github.com";
23+
2124
#[derive(clap::Parser)]
2225
struct Opts {
2326
/// Github App ID.
@@ -39,6 +42,10 @@ struct Opts {
3942
/// Prefix used for bot commands in PR comments.
4043
#[arg(long, env = "CMD_PREFIX", default_value = "@bors")]
4144
cmd_prefix: String,
45+
46+
/// Prefix used for bot commands in PR comments.
47+
#[arg(long, env = "CI_ACCESS_TOKEN")]
48+
ci_access_token: Option<String>,
4249
}
4350

4451
/// Starts a server that receives GitHub webhooks and generates events into a queue
@@ -81,15 +88,25 @@ fn try_main(opts: Opts) -> anyhow::Result<()> {
8188
let db = runtime
8289
.block_on(initialize_db(&opts.db))
8390
.context("Cannot initialize database")?;
84-
let team_api = TeamApiClient::default();
85-
let (client, loaded_repos) = runtime.block_on(async {
86-
let client = create_github_client(
87-
opts.app_id.into(),
88-
"https://api.github.com".to_string(),
89-
opts.private_key.into(),
90-
)?;
91-
let repos = load_repositories(&client, &team_api).await?;
92-
Ok::<_, anyhow::Error>((client, repos))
91+
let team_api_client = TeamApiClient::default();
92+
let client = create_github_client(
93+
opts.app_id.into(),
94+
GITHUB_API_URL.to_string(),
95+
opts.private_key.into(),
96+
)?;
97+
let ci_client = match opts.ci_access_token {
98+
Some(access_token) => {
99+
let client = create_github_client_from_access_token(
100+
GITHUB_API_URL.to_string(),
101+
access_token.into(),
102+
)?;
103+
Some(client)
104+
}
105+
None => None,
106+
};
107+
let loaded_repos = runtime.block_on(async {
108+
let repos = load_repositories(&client, ci_client.clone(), &team_api_client).await?;
109+
Ok::<_, anyhow::Error>(repos)
93110
})?;
94111

95112
let mut repos = HashMap::default();
@@ -108,8 +125,17 @@ fn try_main(opts: Opts) -> anyhow::Result<()> {
108125
repos.insert(name, Arc::new(repo));
109126
}
110127

111-
let ctx = BorsContext::new(CommandParser::new(opts.cmd_prefix), Arc::new(db), repos);
112-
let (repository_tx, global_tx, bors_process) = create_bors_process(ctx, client, team_api);
128+
let ctx = BorsContextBuilder::default()
129+
.parser(CommandParser::new(opts.cmd_prefix))
130+
.db(Arc::new(db))
131+
.repositories(repos)
132+
.gh_client(client)
133+
.ci_client(ci_client)
134+
.team_api_client(team_api_client)
135+
.build()
136+
.unwrap();
137+
138+
let (repository_tx, global_tx, bors_process) = create_bors_process(ctx);
113139

114140
let refresh_tx = global_tx.clone();
115141
let refresh_process = async move {

src/bors/command/parser.rs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ enum CommandPart<'a> {
2222
KeyValue { key: &'a str, value: &'a str },
2323
}
2424

25+
#[derive(Clone)]
2526
pub struct CommandParser {
2627
prefix: String,
2728
}

src/bors/context.rs

+13-16
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,24 @@ use std::{
33
sync::{Arc, RwLock},
44
};
55

6-
use crate::{bors::command::CommandParser, github::GithubRepoName, PgDbClient};
6+
use derive_builder::Builder;
7+
use octocrab::Octocrab;
8+
9+
use crate::{bors::command::CommandParser, github::GithubRepoName, PgDbClient, TeamApiClient};
710

811
use super::RepositoryState;
912

13+
#[derive(Builder)]
1014
pub struct BorsContext {
1115
pub parser: CommandParser,
1216
pub db: Arc<PgDbClient>,
17+
#[builder(field(
18+
ty = "HashMap<GithubRepoName, Arc<RepositoryState>>",
19+
build = "RwLock::new(self.repositories.clone())"
20+
))]
1321
pub repositories: RwLock<HashMap<GithubRepoName, Arc<RepositoryState>>>,
14-
}
15-
16-
impl BorsContext {
17-
pub fn new(
18-
parser: CommandParser,
19-
db: Arc<PgDbClient>,
20-
repositories: HashMap<GithubRepoName, Arc<RepositoryState>>,
21-
) -> Self {
22-
let repositories = RwLock::new(repositories);
23-
Self {
24-
parser,
25-
db,
26-
repositories,
27-
}
28-
}
22+
pub gh_client: Octocrab,
23+
#[builder(default)]
24+
pub ci_client: Option<Octocrab>,
25+
pub team_api_client: TeamApiClient,
2926
}

src/bors/handlers/mod.rs

+6-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::sync::Arc;
22

33
use anyhow::Context;
4-
use octocrab::Octocrab;
54
use tracing::Instrument;
65

76
use crate::bors::command::{BorsCommand, CommandParseError};
@@ -17,7 +16,7 @@ use crate::bors::handlers::workflow::{
1716
handle_check_suite_completed, handle_workflow_completed, handle_workflow_started,
1817
};
1918
use crate::bors::{BorsContext, Comment, RepositoryState};
20-
use crate::{load_repositories, PgDbClient, TeamApiClient};
19+
use crate::{load_repositories, PgDbClient};
2120

2221
#[cfg(test)]
2322
use crate::tests::util::TestSyncMarker;
@@ -142,16 +141,12 @@ pub static WAIT_FOR_REFRESH: TestSyncMarker = TestSyncMarker::new();
142141
pub async fn handle_bors_global_event(
143142
event: BorsGlobalEvent,
144143
ctx: Arc<BorsContext>,
145-
gh_client: &Octocrab,
146-
team_api_client: &TeamApiClient,
147144
) -> anyhow::Result<()> {
148145
let db = Arc::clone(&ctx.db);
149146
match event {
150147
BorsGlobalEvent::InstallationsChanged => {
151148
let span = tracing::info_span!("Installations changed");
152-
reload_repos(ctx, gh_client, team_api_client)
153-
.instrument(span)
154-
.await?;
149+
reload_repos(ctx).instrument(span).await?;
155150
}
156151
BorsGlobalEvent::Refresh => {
157152
let span = tracing::info_span!("Refresh");
@@ -161,7 +156,7 @@ pub async fn handle_bors_global_event(
161156
let repo = Arc::clone(&repo);
162157
async {
163158
let subspan = tracing::info_span!("Repo", repo = repo.repository().to_string());
164-
refresh_repository(repo, Arc::clone(&db), team_api_client)
159+
refresh_repository(repo, Arc::clone(&db), &ctx.team_api_client)
165160
.instrument(subspan)
166161
.await
167162
}
@@ -274,12 +269,9 @@ async fn handle_comment(
274269
Ok(())
275270
}
276271

277-
async fn reload_repos(
278-
ctx: Arc<BorsContext>,
279-
gh_client: &Octocrab,
280-
team_api_client: &TeamApiClient,
281-
) -> anyhow::Result<()> {
282-
let reloaded_repos = load_repositories(gh_client, team_api_client).await?;
272+
async fn reload_repos(ctx: Arc<BorsContext>) -> anyhow::Result<()> {
273+
let reloaded_repos =
274+
load_repositories(&ctx.gh_client, ctx.ci_client.clone(), &ctx.team_api_client).await?;
283275
let mut repositories = ctx.repositories.write().unwrap();
284276
for repo in repositories.values() {
285277
if !reloaded_repos.contains_key(repo.repository()) {

src/bors/handlers/trybuild.rs

+79-43
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::bors::Comment;
1010
use crate::bors::RepositoryState;
1111
use crate::database::RunId;
1212
use crate::database::{BuildModel, BuildStatus, PullRequestModel, WorkflowStatus, WorkflowType};
13+
use crate::github::api::client::GithubRepositoryClient;
1314
use crate::github::GithubRepoName;
1415
use crate::github::{
1516
CommitSha, GithubUser, LabelTrigger, MergeError, PullRequest, PullRequestNumber,
@@ -62,66 +63,94 @@ pub(super) async fn command_try_build(
6263
}
6364
};
6465

66+
match attempt_merge(
67+
&repo.ci_client,
68+
&pr.head.sha,
69+
&base_sha,
70+
&auto_merge_commit_message(pr, repo.client.repository(), "<try>", jobs),
71+
)
72+
.await?
73+
{
74+
MergeResult::Success(merge_sha) => {
75+
// If the merge was succesful, run CI with merged commit
76+
run_try_build(&repo.ci_client, &db, pr_model, merge_sha.clone(), base_sha).await?;
77+
78+
handle_label_trigger(repo, pr.number, LabelTrigger::TryBuildStarted).await?;
79+
80+
repo.client
81+
.post_comment(pr.number, trying_build_comment(&pr.head.sha, &merge_sha))
82+
.await
83+
}
84+
MergeResult::Conflict => {
85+
repo.client
86+
.post_comment(pr.number, merge_conflict_comment(&pr.head.name))
87+
.await
88+
}
89+
}
90+
}
91+
92+
async fn attempt_merge(
93+
ci_client: &GithubRepositoryClient,
94+
head_sha: &CommitSha,
95+
base_sha: &CommitSha,
96+
merge_message: &str,
97+
) -> anyhow::Result<MergeResult> {
6598
tracing::debug!("Attempting to merge with base SHA {base_sha}");
6699

67100
// First set the try branch to our base commit (either the selected parent or the main branch).
68-
repo.client
69-
.set_branch_to_sha(TRY_MERGE_BRANCH_NAME, &base_sha)
101+
ci_client
102+
.set_branch_to_sha(TRY_MERGE_BRANCH_NAME, base_sha)
70103
.await
71104
.map_err(|error| anyhow!("Cannot set try merge branch to {}: {error:?}", base_sha.0))?;
72105

73106
// Then merge the PR commit into the try branch
74-
match repo
75-
.client
76-
.merge_branches(
77-
TRY_MERGE_BRANCH_NAME,
78-
&pr.head.sha,
79-
&auto_merge_commit_message(pr, repo.client.repository(), "<try>", jobs),
80-
)
107+
match ci_client
108+
.merge_branches(TRY_MERGE_BRANCH_NAME, head_sha, merge_message)
81109
.await
82110
{
83111
Ok(merge_sha) => {
84112
tracing::debug!("Merge successful, SHA: {merge_sha}");
85-
// If the merge was succesful, then set the actual try branch that will run CI to the
86-
// merged commit.
87-
repo.client
88-
.set_branch_to_sha(TRY_BRANCH_NAME, &merge_sha)
89-
.await
90-
.map_err(|error| anyhow!("Cannot set try branch to main branch: {error:?}"))?;
91-
92-
db.attach_try_build(
93-
pr_model,
94-
TRY_BRANCH_NAME.to_string(),
95-
merge_sha.clone(),
96-
base_sha.clone(),
97-
)
98-
.await?;
99-
tracing::info!("Try build started");
100-
101-
handle_label_trigger(repo, pr.number, LabelTrigger::TryBuildStarted).await?;
102113

103-
let comment = Comment::new(format!(
104-
":hourglass: Trying commit {} with merge {}…",
105-
pr.head.sha.clone(),
106-
merge_sha
107-
));
108-
repo.client.post_comment(pr.number, comment).await?;
109-
Ok(())
114+
Ok(MergeResult::Success(merge_sha))
110115
}
111116
Err(MergeError::Conflict) => {
112117
tracing::warn!("Merge conflict");
113-
repo.client
114-
.post_comment(
115-
pr.number,
116-
Comment::new(merge_conflict_message(&pr.head.name)),
117-
)
118-
.await?;
119-
Ok(())
118+
119+
Ok(MergeResult::Conflict)
120120
}
121121
Err(error) => Err(error.into()),
122122
}
123123
}
124124

125+
async fn run_try_build(
126+
ci_client: &GithubRepositoryClient,
127+
db: &PgDbClient,
128+
pr_model: PullRequestModel,
129+
commit_sha: CommitSha,
130+
parent_sha: CommitSha,
131+
) -> anyhow::Result<()> {
132+
ci_client
133+
.set_branch_to_sha(TRY_BRANCH_NAME, &commit_sha)
134+
.await
135+
.map_err(|error| anyhow!("Cannot set try branch to main branch: {error:?}"))?;
136+
137+
db.attach_try_build(
138+
pr_model,
139+
TRY_BRANCH_NAME.to_string(),
140+
commit_sha,
141+
parent_sha,
142+
)
143+
.await?;
144+
145+
tracing::info!("Try build started");
146+
Ok(())
147+
}
148+
149+
enum MergeResult {
150+
Success(CommitSha),
151+
Conflict,
152+
}
153+
125154
fn get_base_sha(
126155
pr_model: &PullRequestModel,
127156
parent: Option<Parent>,
@@ -267,8 +296,14 @@ fn auto_merge_commit_message(
267296
message
268297
}
269298

270-
fn merge_conflict_message(branch: &str) -> String {
271-
format!(
299+
fn trying_build_comment(head_sha: &CommitSha, merge_sha: &CommitSha) -> Comment {
300+
Comment::new(format!(
301+
":hourglass: Trying commit {head_sha} with merge {merge_sha}…"
302+
))
303+
}
304+
305+
fn merge_conflict_comment(branch: &str) -> Comment {
306+
let message = format!(
272307
r#":lock: Merge conflict
273308
274309
This pull request and the master branch diverged in a way that cannot
@@ -298,7 +333,8 @@ handled during merge and rebase. This is normal, and you should still perform st
298333
299334
</details>
300335
"#
301-
)
336+
);
337+
Comment::new(message)
302338
}
303339

304340
async fn check_try_permissions(

0 commit comments

Comments
 (0)