Bladeren bron

Publish rustc bors commit list

The GitHub API for retrieving this data is both slow and requires
authentication; so we re-publish it here.
Mark Rousskov 5 jaren geleden
bovenliggende
commit
2822adc8be
6 gewijzigde bestanden met toevoegingen van 219 en 2 verwijderingen
  1. 8 0
      src/db.rs
  2. 50 0
      src/db/rustc_commits.rs
  3. 35 1
      src/github.rs
  4. 7 1
      src/handlers.rs
  5. 103 0
      src/handlers/rustc_commits.rs
  6. 16 0
      src/main.rs

+ 8 - 0
src/db.rs

@@ -4,6 +4,7 @@ use postgres_native_tls::MakeTlsConnector;
 pub use tokio_postgres::Client as DbClient;
 
 pub mod notifications;
+pub mod rustc_commits;
 
 const CERT_URL: &str = "https://s3.amazonaws.com/rds-downloads/rds-ca-2019-root.pem";
 
@@ -129,4 +130,11 @@ CREATE TABLE users (
     "ALTER TABLE notifications ADD COLUMN team_name TEXT;",
     "ALTER TABLE notifications ADD COLUMN idx INTEGER;",
     "ALTER TABLE notifications ADD COLUMN metadata TEXT;",
+    "
+CREATE TABLE rustc_commits (
+    sha PRIMARY KEY,
+    parent_sha TEXT NOT NULL,
+    time TIMESTAMP WITH TIME ZONE
+);
+",
 ];

+ 50 - 0
src/db/rustc_commits.rs

@@ -0,0 +1,50 @@
+use anyhow::Context as _;
+use chrono::{DateTime, FixedOffset};
+use tokio_postgres::Client as DbClient;
+
+/// A bors merge commit.
+#[derive(Debug, serde::Serialize)]
+pub struct Commit {
+    pub sha: String,
+    pub parent_sha: String,
+    pub time: DateTime<FixedOffset>,
+}
+
+pub async fn record_commit(db: &DbClient, commit: Commit) -> anyhow::Result<()> {
+    db.execute(
+        "INSERT INTO rustc_commits (sha, parent_sha, time) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
+        &[&commit.sha, &commit.parent_sha, &commit.time],
+    )
+    .await
+    .context("inserting commit")?;
+    Ok(())
+}
+
+pub async fn get_commits_with_artifacts(db: &DbClient) -> anyhow::Result<Vec<Commit>> {
+    let commits = db
+        .query(
+            "
+        select sha, parent_sha, time
+        from rustc_commits
+        where time >= current_date - interval '168 days'
+        order by time desc;",
+            &[],
+        )
+        .await
+        .context("Getting commit data")?;
+
+    let mut data = Vec::new();
+    for commit in commits {
+        let sha: String = commit.get(1);
+        let parent_sha: String = commit.get(2);
+        let time: DateTime<FixedOffset> = commit.get(3);
+
+        data.push(Commit {
+            sha,
+            parent_sha,
+            time,
+        });
+    }
+
+    Ok(data)
+}

+ 35 - 1
src/github.rs

@@ -1,6 +1,6 @@
 use anyhow::Context;
 
-use chrono::{FixedOffset, Utc};
+use chrono::{DateTime, FixedOffset, Utc};
 use futures::stream::{FuturesUnordered, StreamExt};
 use once_cell::sync::OnceCell;
 use reqwest::header::{AUTHORIZATION, USER_AGENT};
@@ -633,4 +633,38 @@ impl GithubClient {
         log::trace!("put {:?}", url);
         self.client.put(url).configure(self)
     }
+
+    /// This does not retrieve all of them, only the last several.
+    pub async fn bors_commits(&self) -> Vec<GithubCommit> {
+        let req = self.get("https://api.github.com/repos/rust-lang/rust/commits?author=bors");
+        match self.json(req).await {
+            Ok(r) => r,
+            Err(e) => {
+                log::error!("Failed to query commit list: {:?}", e);
+                Vec::new()
+            }
+        }
+    }
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct GithubCommit {
+    pub sha: String,
+    pub commit: GitCommit,
+    pub parents: Vec<Parent>,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct GitCommit {
+    pub author: GitUser,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct GitUser {
+    pub date: DateTime<FixedOffset>,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct Parent {
+    pub sha: String,
 }

+ 7 - 1
src/handlers.rs

@@ -21,10 +21,12 @@ impl fmt::Display for HandlerError {
     }
 }
 
+mod notification;
+mod rustc_commits;
+
 macro_rules! handlers {
     ($($name:ident = $handler:expr,)*) => {
         $(mod $name;)*
-        mod notification;
 
         pub async fn handle(ctx: &Context, event: &Event) -> Result<(), HandlerError> {
             let config = config::get(&ctx.github, event.repo_name()).await;
@@ -61,6 +63,10 @@ macro_rules! handlers {
                 log::error!("failed to process event {:?} with notification handler: {:?}", event, e);
             }
 
+            if let Err(e) = rustc_commits::handle(ctx, event).await {
+                log::error!("failed to process event {:?} with rustc_commits handler: {:?}", event, e);
+            }
+
             Ok(())
         }
     }

+ 103 - 0
src/handlers/rustc_commits.rs

@@ -0,0 +1,103 @@
+use crate::db::rustc_commits;
+use crate::{
+    github::{self, Event},
+    handlers::Context,
+};
+
+const BORS_GH_ID: i64 = 3372342;
+
+pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
+    let body = match event.comment_body() {
+        Some(v) => v,
+        // Skip events that don't have comment bodies associated
+        None => return Ok(()),
+    };
+
+    let event = if let Event::IssueComment(e) = event {
+        if e.action != github::IssueCommentAction::Created {
+            return Ok(());
+        }
+
+        e
+    } else {
+        return Ok(());
+    };
+
+    if !body.contains("Test successful") {
+        return Ok(());
+    }
+
+    if event.comment.user.id != Some(BORS_GH_ID) {
+        log::trace!("Ignoring non-bors comment, user: {:?}", event.comment.user);
+        return Ok(());
+    }
+
+    let repo = event.issue.repository();
+    if repo.organization != "rust-lang" && repo.repository != "rust" {
+        return Ok(());
+    }
+
+    let start = "<!-- homu: ";
+    let start = body.find(start).map(|s| s + start.len());
+    let end = body.find(" -->");
+    let (start, end) = if let (Some(start), Some(end)) = (start, end) {
+        (start, end)
+    } else {
+        log::warn!("Unable to extract build completion from comment {:?}", body);
+        return Ok(());
+    };
+
+    let bors: BorsMessage = match serde_json::from_str(&body[start..end]) {
+        Ok(bors) => bors,
+        Err(e) => {
+            log::error!(
+                "failed to parse build completion from {:?}: {:?}",
+                &body[start..end],
+                e
+            );
+            return Ok(());
+        }
+    };
+
+    if bors.type_ != "BuildCompleted" {
+        log::trace!("Not build completion? {:?}", bors);
+    }
+
+    if bors.base_ref != "master" {
+        log::trace!("Ignoring bors merge, not on master");
+        return Ok(());
+    }
+    // We get the last several commits, and insert all of them. Our database
+    // handling just skips over commits that already existed, so we're not going
+    // to run into trouble, and this way we mostly self-heal (unless we're down
+    // for a long time).
+    let resp = ctx.github.bors_commits().await;
+
+    for mut gc in resp {
+        let res = rustc_commits::record_commit(
+            &ctx.db,
+            rustc_commits::Commit {
+                sha: gc.sha,
+                parent_sha: gc.parents.remove(0).sha,
+                time: gc.commit.author.date,
+            },
+        )
+        .await;
+        match res {
+            Ok(()) => {}
+            Err(e) => {
+                log::error!("Failed to record commit {:?}", e);
+            }
+        }
+    }
+
+    Ok(())
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct BorsMessage {
+    #[serde(rename = "type")]
+    type_: String,
+    base_ref: String,
+    merge_sha: String,
+}

+ 16 - 0
src/main.rs

@@ -19,6 +19,22 @@ async fn serve_req(req: Request<Body>, ctx: Arc<Context>) -> Result<Response<Bod
             .body(Body::from("Triagebot is awaiting triage."))
             .unwrap());
     }
+    if req.uri.path() == "/bors-commit-list" {
+        let res = db::rustc_commits::get_commits_with_artifacts(&ctx.db).await;
+        let res = match res {
+            Ok(r) => r,
+            Err(e) => {
+                return Ok(Response::builder()
+                    .status(StatusCode::INTERNAL_SERVER_ERROR)
+                    .body(Body::from(format!("{:?}", e)))
+                    .unwrap());
+            }
+        };
+        return Ok(Response::builder()
+            .status(StatusCode::OK)
+            .body(Body::from(serde_json::to_string(&res).unwrap()))
+            .unwrap());
+    }
     if req.uri.path() == "/notifications" {
         if let Some(query) = req.uri.query() {
             let user = url::form_urlencoded::parse(query.as_bytes()).find(|(k, _)| k == "user");