Skip to content

Commit c772c80

Browse files
Merge pull request #56 from pietroalbini/hide
Hide RLA comments when a new commit is pushed
2 parents 918c381 + 85c542a commit c772c80

File tree

4 files changed

+191
-4
lines changed

4 files changed

+191
-4
lines changed

src/bin/server/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ pub struct QueueItem {
1414
pub enum QueueItemKind {
1515
GitHubStatus(rla::github::CommitStatusEvent),
1616
GitHubCheckRun(rla::github::CheckRunEvent),
17+
GitHubPullRequest(rla::github::PullRequestEvent),
1718
}

src/bin/server/service.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ impl RlaService {
100100

101101
QueueItemKind::GitHubCheckRun(payload)
102102
}
103+
"pull_request" => match serde_json::from_slice(body) {
104+
Ok(payload) => QueueItemKind::GitHubPullRequest(payload),
105+
Err(err) => {
106+
error!("Failed to decode 'pull_request' webhook payload: {}", err);
107+
return reply(StatusCode::BAD_REQUEST, "Failed to decode payload\n");
108+
}
109+
},
103110
"issue_comment" => {
104111
debug!("Ignoring 'issue_comment' event.");
105112
return reply(StatusCode::OK, "Event ignored.\n");

src/bin/server/worker.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ impl Worker {
115115
return Ok(());
116116
}
117117
},
118+
QueueItemKind::GitHubPullRequest(ev) => return self.process_pr(ev),
118119
};
119120

120121
span.record("build_id", &build_id);
@@ -324,6 +325,15 @@ impl Worker {
324325

325326
Ok(())
326327
}
328+
329+
fn process_pr(&self, e: &rla::github::PullRequestEvent) -> rla::Result<()> {
330+
// Hide all comments by the bot when a new commit is pushed.
331+
if let rla::github::PullRequestAction::Synchronize = e.action {
332+
self.github
333+
.hide_own_comments(&e.repository.full_name, e.number)?;
334+
}
335+
Ok(())
336+
}
327337
}
328338

329339
/// Keeps track of the recently seen IDs for both the failed build reports and the learned jobs.

src/github.rs

Lines changed: 173 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::Result;
22
use crate::ci::Outcome;
33
use hyper::header;
44
use reqwest;
5+
use serde::{de::DeserializeOwned, Serialize};
56
use std::env;
67
use std::str;
78
use std::time::Duration;
@@ -83,10 +84,6 @@ pub struct CommitParent {
8384
pub sha: String,
8485
}
8586

86-
pub struct Client {
87-
internal: reqwest::Client,
88-
}
89-
9087
#[derive(Serialize)]
9188
struct Comment<'a> {
9289
body: &'a str,
@@ -125,6 +122,55 @@ pub struct Repository {
125122
pub full_name: String,
126123
}
127124

125+
#[derive(Deserialize)]
126+
#[serde(rename_all = "snake_case")]
127+
pub enum PullRequestAction {
128+
Opened,
129+
Edited,
130+
Closed,
131+
Assigned,
132+
Unassigned,
133+
ReviewRequested,
134+
ReviewRequestRemoved,
135+
ReadyForReview,
136+
Labeled,
137+
Unlabeled,
138+
Synchronize,
139+
Locked,
140+
Unlocked,
141+
Reopened,
142+
}
143+
144+
#[derive(Deserialize)]
145+
pub struct PullRequestEvent {
146+
pub action: PullRequestAction,
147+
pub number: u32,
148+
pub repository: Repository,
149+
}
150+
151+
#[derive(Deserialize)]
152+
struct GraphResponse<T> {
153+
data: T,
154+
#[serde(default)]
155+
errors: Vec<GraphError>,
156+
}
157+
158+
#[derive(Debug, Deserialize)]
159+
struct GraphError {
160+
message: String,
161+
path: serde_json::Value,
162+
}
163+
164+
#[derive(Debug, Deserialize)]
165+
#[serde(rename_all = "camelCase")]
166+
struct GraphPageInfo {
167+
end_cursor: Option<String>,
168+
}
169+
170+
pub struct Client {
171+
internal: reqwest::Client,
172+
}
173+
128174
impl Client {
129175
pub fn new() -> Result<Client> {
130176
let token = env::var("GITHUB_TOKEN")
@@ -192,9 +238,132 @@ impl Client {
192238
Ok(())
193239
}
194240

241+
pub fn hide_own_comments(&self, repo: &str, pull_request_id: u32) -> Result<()> {
242+
const QUERY: &str = "query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) {
243+
repository(owner: $owner, name: $repo) {
244+
pullRequest(number: $pr) {
245+
comments(first: 100, after: $cursor) {
246+
nodes {
247+
id
248+
isMinimized
249+
viewerDidAuthor
250+
}
251+
pageInfo {
252+
endCursor
253+
}
254+
}
255+
}
256+
}
257+
}";
258+
259+
#[derive(Debug, Deserialize)]
260+
struct Response {
261+
repository: ResponseRepo,
262+
}
263+
#[derive(Debug, Deserialize)]
264+
#[serde(rename_all = "camelCase")]
265+
struct ResponseRepo {
266+
pull_request: ResponsePR,
267+
}
268+
#[derive(Debug, Deserialize)]
269+
struct ResponsePR {
270+
comments: ResponseComments,
271+
}
272+
#[derive(Debug, Deserialize)]
273+
#[serde(rename_all = "camelCase")]
274+
struct ResponseComments {
275+
nodes: Vec<ResponseComment>,
276+
page_info: GraphPageInfo,
277+
}
278+
#[derive(Debug, Deserialize)]
279+
#[serde(rename_all = "camelCase")]
280+
struct ResponseComment {
281+
id: String,
282+
is_minimized: bool,
283+
viewer_did_author: bool,
284+
}
285+
286+
let (owner, repo) = if let Some(mid) = repo.find('/') {
287+
let split = repo.split_at(mid);
288+
(split.0, split.1.trim_start_matches('/'))
289+
} else {
290+
bail!("invalid repository name: {}", repo);
291+
};
292+
293+
let mut comments = Vec::new();
294+
let mut cursor = None;
295+
loop {
296+
let mut resp: Response = self.graphql(
297+
QUERY,
298+
serde_json::json!({
299+
"owner": owner,
300+
"repo": repo,
301+
"pr": pull_request_id,
302+
"cursor": cursor,
303+
}),
304+
)?;
305+
cursor = resp.repository.pull_request.comments.page_info.end_cursor;
306+
comments.append(&mut resp.repository.pull_request.comments.nodes);
307+
308+
if cursor.is_none() {
309+
break;
310+
}
311+
}
312+
313+
for comment in &comments {
314+
if comment.viewer_did_author && !comment.is_minimized {
315+
self.hide_comment(&comment.id, "OUTDATED")?;
316+
}
317+
}
318+
Ok(())
319+
}
320+
321+
fn hide_comment(&self, node_id: &str, reason: &str) -> Result<()> {
322+
#[derive(Deserialize)]
323+
struct MinimizeData {}
324+
325+
const MINIMIZE: &str = "mutation($node_id: ID!, $reason: ReportedContentClassifiers!) {
326+
minimizeComment(input: {subjectId: $node_id, classifier: $reason}) {
327+
__typename
328+
}
329+
}";
330+
331+
self.graphql::<MinimizeData, _>(
332+
MINIMIZE,
333+
serde_json::json!({
334+
"node_id": node_id,
335+
"reason": reason,
336+
}),
337+
)?;
338+
Ok(())
339+
}
340+
195341
pub fn internal(&self) -> &reqwest::Client {
196342
&self.internal
197343
}
344+
345+
fn graphql<T: DeserializeOwned, V: Serialize>(&self, query: &str, variables: V) -> Result<T> {
346+
#[derive(Serialize)]
347+
struct GraphPayload<'a, V> {
348+
query: &'a str,
349+
variables: V,
350+
}
351+
352+
let response: GraphResponse<T> = self
353+
.internal
354+
.post(&format!("{}/graphql", API_BASE))
355+
.json(&GraphPayload { query, variables })
356+
.send()?
357+
.error_for_status()?
358+
.json()?;
359+
360+
if response.errors.is_empty() {
361+
Ok(response.data)
362+
} else {
363+
dbg!(&response.errors);
364+
bail!("GraphQL query failed: {}", response.errors[0].message);
365+
}
366+
}
198367
}
199368

200369
pub fn verify_webhook_signature(secret: &[u8], signature: Option<&str>, body: &[u8]) -> Result<()> {

0 commit comments

Comments
 (0)