Ver código fonte

Automatically milestone merged PRs

In the rust-lang/rust repository, any merged PR will now be assigned to a (if
needed, freshly created) milestone. This means that it should be easy to at a
glance figure out what version a PR is going to be released in.
Mark Rousskov 4 anos atrás
pai
commit
eeea6a80bd
3 arquivos alterados com 154 adições e 0 exclusões
  1. 52 0
      src/github.rs
  2. 9 0
      src/handlers.rs
  3. 93 0
      src/handlers/milestone_prs.rs

+ 52 - 0
src/github.rs

@@ -244,12 +244,16 @@ pub struct Issue {
     pub number: u64,
     pub body: String,
     created_at: chrono::DateTime<Utc>,
+    #[serde(default)]
+    pub merge_commit_sha: Option<String>,
     pub title: String,
     pub html_url: String,
     pub user: User,
     pub labels: Vec<Label>,
     pub assignees: Vec<User>,
     pub pull_request: Option<PullRequestDetails>,
+    #[serde(default)]
+    pub merged: bool,
     // API URL
     comments_url: String,
     #[serde(skip)]
@@ -543,6 +547,54 @@ impl Issue {
             .await?;
         Ok(())
     }
+
+    pub async fn set_milestone(&self, client: &GithubClient, title: &str) -> anyhow::Result<()> {
+        let create_url = format!("{}/milestones", self.repository().url());
+        client
+            .send_req(
+                client
+                    .post(&create_url)
+                    .body(serde_json::to_vec(&MilestoneCreateBody { title }).unwrap()),
+            )
+            .await?;
+
+        let list_url = format!("{}/milestones", self.repository().url());
+        let milestone_list: Vec<Milestone> = client.json(client.get(&list_url)).await?;
+        let milestone_no = if let Some(milestone) = milestone_list.iter().find(|v| v.title == title)
+        {
+            milestone.number
+        } else {
+            anyhow::bail!(
+                "Despite just creating milestone {} on {}, it does not exist?",
+                title,
+                self.repository()
+            )
+        };
+
+        #[derive(serde::Serialize)]
+        struct SetMilestone {
+            milestone: u64,
+        }
+        let url = format!("{}/issues/{}", self.repository().url(), self.number);
+        client
+            ._send_req(client.patch(&url).json(&SetMilestone {
+                milestone: milestone_no,
+            }))
+            .await
+            .context("failed to set milestone")?;
+        Ok(())
+    }
+}
+
+#[derive(serde::Serialize)]
+struct MilestoneCreateBody<'a> {
+    title: &'a str,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct Milestone {
+    number: u64,
+    title: String,
 }
 
 #[derive(Debug, serde::Deserialize)]

+ 9 - 0
src/handlers.rs

@@ -27,6 +27,7 @@ mod assign;
 mod autolabel;
 mod glacier;
 mod major_change;
+mod milestone_prs;
 mod nominate;
 mod notification;
 mod notify_zulip;
@@ -63,6 +64,14 @@ pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
         );
     }
 
+    if let Err(e) = milestone_prs::handle(ctx, event).await {
+        log::error!(
+            "failed to process event {:?} with rustc_commits handler: {:?}",
+            event,
+            e
+        );
+    }
+
     errors
 }
 

+ 93 - 0
src/handlers/milestone_prs.rs

@@ -0,0 +1,93 @@
+use crate::{
+    github::{Event, IssuesAction},
+    handlers::Context,
+};
+use anyhow::Context as _;
+
+pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
+    let e = if let Event::Issue(e) = event {
+        e
+    } else {
+        return Ok(());
+    };
+
+    // Only trigger on closed issues
+    if e.action != IssuesAction::Closed {
+        return Ok(());
+    }
+
+    let repo = e.issue.repository();
+    if repo.organization != "rust-lang" && repo.repository != "rust" {
+        return Ok(());
+    }
+
+    if !e.issue.merged {
+        log::trace!(
+            "Ignoring closing of rust-lang/rust#{}: not merged",
+            e.issue.number
+        );
+        return Ok(());
+    }
+
+    let merge_sha = if let Some(s) = &e.issue.merge_commit_sha {
+        s
+    } else {
+        log::error!(
+            "rust-lang/rust#{}: no merge_commit_sha in event",
+            e.issue.number
+        );
+        return Ok(());
+    };
+
+    // Fetch channel.rs from the upstream repository
+
+    let resp = ctx
+        .github
+        .raw()
+        .get(&format!(
+            "https://raw.githubusercontent.com/rust-lang/rust/{}/src/bootstrap/channel.rs",
+            merge_sha
+        ))
+        .send()
+        .await
+        .with_context(|| format!("retrieving channel.rs for {}", merge_sha))?;
+
+    let resp = resp
+        .text()
+        .await
+        .with_context(|| format!("deserializing text channel.rs for {}", merge_sha))?;
+
+    let prefix = r#"const CFG_RELEASE_NUM: &str = ""#;
+    let start = if let Some(idx) = resp.find(prefix) {
+        idx + prefix.len()
+    } else {
+        log::error!(
+            "No {:?} in contents of channel.rs at {:?}",
+            prefix,
+            merge_sha
+        );
+        return Ok(());
+    };
+    let after = &resp[start..];
+    let end = if let Some(idx) = after.find('"') {
+        idx
+    } else {
+        log::error!("No suffix in contents of channel.rs at {:?}", merge_sha);
+        return Ok(());
+    };
+    let version = &after[..end];
+    if !version.starts_with("1.") && version.len() < 8 {
+        log::error!("Weird version {:?} for {:?}", version, merge_sha);
+        return Ok(());
+    }
+
+    // Associate this merged PR with the version it merged into.
+    //
+    // Note that this should work for rollup-merged PRs too. It will *not*
+    // auto-update when merging a beta-backport, for example, but that seems
+    // fine; we can manually update without too much trouble in that case, and
+    // eventually automate it separately.
+    e.issue.set_milestone(&ctx.github, version).await?;
+
+    Ok(())
+}