Skip to content

Hide RLA comments when a new commit is pushed #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/bin/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ pub struct QueueItem {
pub enum QueueItemKind {
GitHubStatus(rla::github::CommitStatusEvent),
GitHubCheckRun(rla::github::CheckRunEvent),
GitHubPullRequest(rla::github::PullRequestEvent),
}
7 changes: 7 additions & 0 deletions src/bin/server/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ impl RlaService {

QueueItemKind::GitHubCheckRun(payload)
}
"pull_request" => match serde_json::from_slice(body) {
Ok(payload) => QueueItemKind::GitHubPullRequest(payload),
Err(err) => {
error!("Failed to decode 'pull_request' webhook payload: {}", err);
return reply(StatusCode::BAD_REQUEST, "Failed to decode payload\n");
}
},
"issue_comment" => {
debug!("Ignoring 'issue_comment' event.");
return reply(StatusCode::OK, "Event ignored.\n");
Expand Down
10 changes: 10 additions & 0 deletions src/bin/server/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ impl Worker {
return Ok(());
}
},
QueueItemKind::GitHubPullRequest(ev) => return self.process_pr(ev),
};

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

Ok(())
}

fn process_pr(&self, e: &rla::github::PullRequestEvent) -> rla::Result<()> {
// Hide all comments by the bot when a new commit is pushed.
if let rla::github::PullRequestAction::Synchronize = e.action {
self.github
.hide_own_comments(&e.repository.full_name, e.number)?;
}
Ok(())
}
}

/// Keeps track of the recently seen IDs for both the failed build reports and the learned jobs.
Expand Down
177 changes: 173 additions & 4 deletions src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::Result;
use crate::ci::Outcome;
use hyper::header;
use reqwest;
use serde::{de::DeserializeOwned, Serialize};
use std::env;
use std::str;
use std::time::Duration;
Expand Down Expand Up @@ -83,10 +84,6 @@ pub struct CommitParent {
pub sha: String,
}

pub struct Client {
internal: reqwest::Client,
}

#[derive(Serialize)]
struct Comment<'a> {
body: &'a str,
Expand Down Expand Up @@ -125,6 +122,55 @@ pub struct Repository {
pub full_name: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PullRequestAction {
Opened,
Edited,
Closed,
Assigned,
Unassigned,
ReviewRequested,
ReviewRequestRemoved,
ReadyForReview,
Labeled,
Unlabeled,
Synchronize,
Locked,
Unlocked,
Reopened,
}

#[derive(Deserialize)]
pub struct PullRequestEvent {
pub action: PullRequestAction,
pub number: u32,
pub repository: Repository,
}

#[derive(Deserialize)]
struct GraphResponse<T> {
data: T,
#[serde(default)]
errors: Vec<GraphError>,
}

#[derive(Debug, Deserialize)]
struct GraphError {
message: String,
path: serde_json::Value,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GraphPageInfo {
end_cursor: Option<String>,
}

pub struct Client {
internal: reqwest::Client,
}

impl Client {
pub fn new() -> Result<Client> {
let token = env::var("GITHUB_TOKEN")
Expand Down Expand Up @@ -192,9 +238,132 @@ impl Client {
Ok(())
}

pub fn hide_own_comments(&self, repo: &str, pull_request_id: u32) -> Result<()> {
const QUERY: &str = "query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
comments(first: 100, after: $cursor) {
nodes {
id
isMinimized
viewerDidAuthor
}
pageInfo {
endCursor
}
}
}
}
}";

#[derive(Debug, Deserialize)]
struct Response {
repository: ResponseRepo,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResponseRepo {
pull_request: ResponsePR,
}
#[derive(Debug, Deserialize)]
struct ResponsePR {
comments: ResponseComments,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResponseComments {
nodes: Vec<ResponseComment>,
page_info: GraphPageInfo,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResponseComment {
id: String,
is_minimized: bool,
viewer_did_author: bool,
}

let (owner, repo) = if let Some(mid) = repo.find('/') {
let split = repo.split_at(mid);
(split.0, split.1.trim_start_matches('/'))
} else {
bail!("invalid repository name: {}", repo);
};

let mut comments = Vec::new();
let mut cursor = None;
loop {
let mut resp: Response = self.graphql(
QUERY,
serde_json::json!({
"owner": owner,
"repo": repo,
"pr": pull_request_id,
"cursor": cursor,
}),
)?;
cursor = resp.repository.pull_request.comments.page_info.end_cursor;
comments.append(&mut resp.repository.pull_request.comments.nodes);

if cursor.is_none() {
break;
}
}

for comment in &comments {
if comment.viewer_did_author && !comment.is_minimized {
self.hide_comment(&comment.id, "OUTDATED")?;
}
}
Ok(())
}

fn hide_comment(&self, node_id: &str, reason: &str) -> Result<()> {
#[derive(Deserialize)]
struct MinimizeData {}

const MINIMIZE: &str = "mutation($node_id: ID!, $reason: ReportedContentClassifiers!) {
minimizeComment(input: {subjectId: $node_id, classifier: $reason}) {
__typename
}
}";

self.graphql::<MinimizeData, _>(
MINIMIZE,
serde_json::json!({
"node_id": node_id,
"reason": reason,
}),
)?;
Ok(())
}

pub fn internal(&self) -> &reqwest::Client {
&self.internal
}

fn graphql<T: DeserializeOwned, V: Serialize>(&self, query: &str, variables: V) -> Result<T> {
#[derive(Serialize)]
struct GraphPayload<'a, V> {
query: &'a str,
variables: V,
}

let response: GraphResponse<T> = self
.internal
.post(&format!("{}/graphql", API_BASE))
.json(&GraphPayload { query, variables })
.send()?
.error_for_status()?
.json()?;

if response.errors.is_empty() {
Ok(response.data)
} else {
dbg!(&response.errors);
bail!("GraphQL query failed: {}", response.errors[0].message);
}
}
}

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