Skip to content

Commit aab6d9d

Browse files
committed
Add workflow links to try build started comments
1 parent 14d1076 commit aab6d9d

File tree

7 files changed

+223
-30
lines changed

7 files changed

+223
-30
lines changed

src/bors/comment.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ pub fn try_build_started_comment(
198198
Comment::new(msg)
199199
}
200200

201+
pub fn append_workflow_links_to_comment(comment_content: &mut String, workflow_links: Vec<String>) {
202+
comment_content.push_str("\n\n**Workflows**:\n\n");
203+
204+
for link in workflow_links {
205+
comment_content.push_str(&format!("- {link}\n"));
206+
}
207+
}
208+
201209
pub fn merge_conflict_comment(branch: &str) -> Comment {
202210
let message = format!(
203211
r#":lock: Merge conflict

src/bors/handlers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub async fn handle_bors_repository_event(
108108
repo = payload.repository.to_string(),
109109
id = payload.run_id.into_inner()
110110
);
111-
handle_workflow_started(db, payload)
111+
handle_workflow_started(repo, db, payload)
112112
.instrument(span.clone())
113113
.await?;
114114

src/bors/handlers/workflow.rs

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
use super::trybuild::TRY_BRANCH_NAME;
12
use crate::PgDbClient;
2-
use crate::bors::comment::{build_failed_comment, try_build_succeeded_comment};
3+
use crate::bors::comment::{
4+
CommentTag, append_workflow_links_to_comment, build_failed_comment, try_build_succeeded_comment,
5+
};
36
use crate::bors::event::{WorkflowRunCompleted, WorkflowRunStarted};
47
use crate::bors::handlers::labels::handle_label_trigger;
58
use crate::bors::handlers::{BuildType, is_bors_observed_branch};
@@ -17,6 +20,7 @@ use std::sync::Arc;
1720
use std::time::Duration;
1821

1922
pub(super) async fn handle_workflow_started(
23+
repo: Arc<RepositoryState>,
2024
db: Arc<PgDbClient>,
2125
payload: WorkflowRunStarted,
2226
) -> anyhow::Result<()> {
@@ -53,14 +57,53 @@ pub(super) async fn handle_workflow_started(
5357
tracing::info!("Storing workflow started into DB");
5458
db.create_workflow(
5559
&build,
56-
payload.name,
57-
payload.url,
60+
payload.name.clone(),
61+
payload.url.clone(),
5862
payload.run_id.into(),
59-
payload.workflow_type,
63+
payload.workflow_type.clone(),
6064
WorkflowStatus::Pending,
6165
)
6266
.await?;
6367

68+
if build.branch == TRY_BRANCH_NAME {
69+
add_workflow_links_to_try_build_start_comment(repo, db, &build, payload).await?;
70+
}
71+
72+
Ok(())
73+
}
74+
75+
async fn add_workflow_links_to_try_build_start_comment(
76+
repo: Arc<RepositoryState>,
77+
db: Arc<PgDbClient>,
78+
build: &BuildModel,
79+
payload: WorkflowRunStarted,
80+
) -> anyhow::Result<()> {
81+
let Some(pr) = db.find_pr_by_build(&build).await? else {
82+
tracing::warn!("PR for build not found");
83+
return Ok(());
84+
};
85+
let comments = db
86+
.get_tagged_bot_comments(&payload.repository, pr.number, CommentTag::TryBuildStarted)
87+
.await?;
88+
89+
let Some(try_build_comment) = comments.last() else {
90+
tracing::warn!("No try build comment found for PR");
91+
return Ok(());
92+
};
93+
94+
let workflows = db.get_workflow_urls_for_build(&build).await?;
95+
96+
let mut comment_content = repo
97+
.client
98+
.get_comment_content(&try_build_comment.node_id)
99+
.await?;
100+
101+
append_workflow_links_to_comment(&mut comment_content, workflows);
102+
103+
repo.client
104+
.update_comment_content(&try_build_comment.node_id, &comment_content)
105+
.await?;
106+
64107
Ok(())
65108
}
66109

@@ -108,7 +151,6 @@ pub(super) async fn handle_workflow_completed(
108151
)
109152
.await
110153
}
111-
112154
/// Attempt to complete a pending build after a workflow run has been completed.
113155
/// We assume that the status of the completed workflow run has already been updated in the
114156
/// database.

src/database/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ impl PullRequestModel {
394394

395395
/// Describes whether a workflow is a Github Actions workflow or if it's a job from some external
396396
/// CI.
397-
#[derive(Debug, PartialEq, sqlx::Type)]
397+
#[derive(Debug, Clone, PartialEq, sqlx::Type)]
398398
#[sqlx(type_name = "TEXT")]
399399
#[sqlx(rename_all = "lowercase")]
400400
pub enum WorkflowType {

src/database/operations.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,7 @@ pub(crate) async fn get_tagged_bot_comments(
10431043
WHERE repository = $1
10441044
AND pr_number = $2
10451045
AND tag = $3
1046+
ORDER BY created_at
10461047
"#,
10471048
repo as &GithubRepoName,
10481049
pr_number.0 as i32,

src/github/api/client.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,109 @@ impl GithubRepositoryClient {
498498
.await?;
499499
Ok(())
500500
}
501+
502+
pub async fn get_comment_content(&self, node_id: &str) -> anyhow::Result<String> {
503+
const QUERY: &str = r#"
504+
query($node_id: ID!) {
505+
node(id: $node_id) {
506+
... on IssueComment {
507+
body
508+
}
509+
}
510+
}
511+
"#;
512+
513+
#[derive(Serialize)]
514+
struct Variables<'a> {
515+
node_id: &'a str,
516+
}
517+
518+
#[derive(Deserialize)]
519+
struct Output {
520+
node: Option<IssueCommentNode>,
521+
}
522+
523+
#[derive(Deserialize)]
524+
struct IssueCommentNode {
525+
body: String,
526+
}
527+
528+
tracing::debug!(node_id, "Fetching comment content");
529+
530+
let output: Output =
531+
perform_retryable("get_comment_content", RetryMethod::default(), || async {
532+
self.graphql::<Output, Variables>(QUERY, Variables { node_id })
533+
.await
534+
.context("Failed to fetch comment content")
535+
})
536+
.await?;
537+
538+
match output.node {
539+
Some(comment) => Ok(comment.body),
540+
None => anyhow::bail!("No comment found for node_id: {}", node_id),
541+
}
542+
}
543+
pub async fn update_comment_content(
544+
&self,
545+
node_id: &str,
546+
new_body: &str,
547+
) -> anyhow::Result<()> {
548+
const QUERY: &str = r#"
549+
mutation($id: ID!, $body: String!) {
550+
updateComment(input: {id: $id, body: $body}) {
551+
comment {
552+
id
553+
body
554+
}
555+
}
556+
}
557+
"#;
558+
559+
#[derive(Serialize)]
560+
struct Variables<'a> {
561+
id: &'a str,
562+
body: &'a str,
563+
}
564+
565+
#[derive(Deserialize)]
566+
struct Output {
567+
update_comment: Option<UpdatedComment>,
568+
}
569+
570+
#[derive(Deserialize)]
571+
struct UpdatedComment {
572+
comment: CommentNode,
573+
}
574+
575+
#[derive(Deserialize)]
576+
struct CommentNode {
577+
body: String,
578+
}
579+
580+
tracing::debug!(node_id, "Updating comment content");
581+
582+
let output: Output =
583+
perform_retryable("update_comment_content", RetryMethod::default(), || async {
584+
self.graphql::<Output, Variables>(
585+
QUERY,
586+
Variables {
587+
id: node_id,
588+
body: new_body,
589+
},
590+
)
591+
.await
592+
.context("Failed to update comment")
593+
})
594+
.await?;
595+
596+
match output.update_comment {
597+
Some(updated_comment) => {
598+
tracing::debug!("Updated comment content: {}", updated_comment.comment.body);
599+
Ok(())
600+
}
601+
None => anyhow::bail!("Failed to update comment: {node_id}"),
602+
}
603+
}
501604
}
502605

503606
/// The reasons a piece of content can be reported or hidden.

src/tests/mocks/github.rs

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -108,40 +108,79 @@ async fn mock_graphql(github: Arc<tokio::sync::Mutex<GitHubState>>, mock_server:
108108
let query: Document<&str> =
109109
graphql_parser::parse_query(&body.query).expect("Could not parse GraphQL query");
110110
let definition = query.definitions.into_iter().next().unwrap();
111-
let mutation = match definition {
112-
Definition::Operation(OperationDefinition::Mutation(mutation)) => mutation,
111+
let selection_set = match definition {
112+
Definition::Operation(OperationDefinition::Mutation(mutation)) => {
113+
mutation.selection_set
114+
}
115+
Definition::Operation(OperationDefinition::Query(query)) => query.selection_set,
113116
_ => panic!("Unexpected GraphQL query: {}", body.query),
114117
};
115-
let selection = mutation.selection_set.items.into_iter().next().unwrap();
118+
let selection = selection_set.items.into_iter().next().unwrap();
116119
let operation = match selection {
117120
Selection::Field(field) => field,
118121
_ => panic!("Unexpected GraphQL selection"),
119122
};
120-
if operation.name == "minimizeComment" {
121-
#[derive(serde::Deserialize)]
122-
struct Variables {
123-
node_id: String,
124-
reason: HideCommentReason,
123+
124+
match operation.name {
125+
"minimizeComment" => {
126+
#[derive(serde::Deserialize)]
127+
struct Variables {
128+
node_id: String,
129+
reason: HideCommentReason,
130+
}
131+
132+
let github = github.clone();
133+
let data: Variables = serde_json::from_value(body.variables).unwrap();
134+
135+
// We have to use e.g. `blocking_lock` to lock from a sync function.
136+
// It has to happen in a separate thread though.
137+
std::thread::spawn(move || {
138+
github.blocking_lock().add_hidden_comment(HiddenComment {
139+
node_id: data.node_id,
140+
reason: data.reason,
141+
});
142+
})
143+
.join()
144+
.unwrap();
145+
ResponseTemplate::new(200).set_body_json(HashMap::<String, String>::new())
146+
}
147+
"updateComment" => {
148+
#[derive(serde::Deserialize)]
149+
struct Variables {
150+
id: String,
151+
body: String,
152+
}
153+
154+
let data: Variables = serde_json::from_value(body.variables).unwrap();
155+
let response = serde_json::json!({
156+
"update_comment": {
157+
"comment": {
158+
"id": data.id,
159+
"body": data.body
160+
}
161+
}
162+
});
163+
164+
ResponseTemplate::new(200).set_body_json(response)
125165
}
166+
"node" => {
167+
#[derive(serde::Deserialize)]
168+
struct Variables {
169+
node_id: String,
170+
}
126171

127-
let github = github.clone();
128-
let data: Variables = serde_json::from_value(body.variables).unwrap();
172+
let data: Variables = serde_json::from_value(body.variables).unwrap();
129173

130-
// We have to use e.g. `blocking_lock` to lock from a sync function.
131-
// It has to happen in a separate thread though.
132-
std::thread::spawn(move || {
133-
github.blocking_lock().add_hidden_comment(HiddenComment {
134-
node_id: data.node_id,
135-
reason: data.reason,
174+
let response = serde_json::json!({
175+
"node": {
176+
"__typename": "IssueComment",
177+
"body":format!(":hourglass: Trying commit {} \n To cancel the try build, run the command @bors try cancel`.", data.node_id)
178+
}
136179
});
137-
})
138-
.join()
139-
.unwrap();
140-
} else {
141-
panic!("Unexpected operation {}", operation.name);
180+
ResponseTemplate::new(200).set_body_json(response)
181+
}
182+
_ => panic!("Unexpected GraphQL operation {}", operation.name),
142183
}
143-
144-
ResponseTemplate::new(200).set_body_json(HashMap::<String, String>::new())
145184
},
146185
"POST",
147186
"^/graphql$".to_string(),

0 commit comments

Comments
 (0)