浏览代码

Merge pull request #579 from spastorino/prioritization-meeting-automation

Prioritization meeting automation
Mark Rousskov 4 年之前
父节点
当前提交
aaca50be3c

+ 1 - 0
Cargo.lock

@@ -1420,6 +1420,7 @@ name = "triagebot"
 version = "0.1.0"
 dependencies = [
  "anyhow 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
+ "async-trait 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "dotenv 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",

+ 1 - 0
Cargo.toml

@@ -24,6 +24,7 @@ toml = "0.5.1"
 hyper = "0.13"
 tokio = { version = "0.2", features = ["macros", "time"] }
 futures = { version = "0.3", default-features = false, features = ["std"] }
+async-trait = "0.1.31"
 uuid = { version = "0.8", features = ["v4"] }
 url = "2.1.0"
 once_cell = "1"

+ 28 - 0
src/bin/prioritization.rs

@@ -0,0 +1,28 @@
+use std::io::{self, Write};
+use triagebot::{logger, meeting::Action, prioritization};
+
+#[tokio::main]
+async fn main() {
+    dotenv::dotenv().ok();
+    logger::init();
+
+    let meeting = prioritization::prepare_meeting();
+
+    for step in &meeting.steps {
+        println!("{}", step.call().await);
+
+        press_key_to_continue();
+    }
+}
+
+fn press_key_to_continue() {
+    let mut stdout = io::stdout();
+    stdout
+        .write(b"Press a key to continue ...")
+        .expect("Unable to write to stdout");
+    stdout.flush().expect("Unable to flush stdout");
+
+    io::stdin()
+        .read_line(&mut String::new())
+        .expect("Unable to read user input");
+}

+ 242 - 8
src/github.rs

@@ -2,10 +2,14 @@ use anyhow::Context;
 
 use chrono::{DateTime, FixedOffset, Utc};
 use futures::stream::{FuturesUnordered, StreamExt};
+use futures::{future::BoxFuture, FutureExt};
 use once_cell::sync::OnceCell;
 use reqwest::header::{AUTHORIZATION, USER_AGENT};
-use reqwest::{Client, RequestBuilder, Response, StatusCode};
-use std::fmt;
+use reqwest::{Client, Request, RequestBuilder, Response, StatusCode};
+use std::{
+    fmt,
+    time::{Duration, SystemTime},
+};
 
 #[derive(Debug, PartialEq, Eq, serde::Deserialize)]
 pub struct User {
@@ -15,17 +19,130 @@ pub struct User {
 
 impl GithubClient {
     async fn _send_req(&self, req: RequestBuilder) -> Result<(Response, String), reqwest::Error> {
+        const MAX_ATTEMPTS: usize = 2;
         log::debug!("_send_req with {:?}", req);
         let req = req.build()?;
-
         let req_dbg = format!("{:?}", req);
 
-        let resp = self.client.execute(req).await?;
+        let mut resp = self.client.execute(req.try_clone().unwrap()).await?;
+        if let Some(sleep) = Self::needs_retry(&resp).await {
+            resp = self.retry(req, sleep, MAX_ATTEMPTS).await?;
+        }
 
         resp.error_for_status_ref()?;
 
         Ok((resp, req_dbg))
     }
+
+    async fn needs_retry(resp: &Response) -> Option<Duration> {
+        const REMAINING: &str = "X-RateLimit-Remaining";
+        const RESET: &str = "X-RateLimit-Reset";
+
+        if resp.status().is_success() {
+            return None;
+        }
+
+        let headers = resp.headers();
+        if !(headers.contains_key(REMAINING) && headers.contains_key(RESET)) {
+            return None;
+        }
+
+        // Weird github api behavior. It asks us to retry but also has a remaining count above 1
+        // Try again immediately and hope for the best...
+        if headers[REMAINING] != "0" {
+            return Some(Duration::from_secs(0));
+        }
+
+        let reset_time = headers[RESET].to_str().unwrap().parse::<u64>().unwrap();
+        Some(Duration::from_secs(Self::calc_sleep(reset_time) + 10))
+    }
+
+    fn calc_sleep(reset_time: u64) -> u64 {
+        let epoch_time = SystemTime::UNIX_EPOCH.elapsed().unwrap().as_secs();
+        reset_time.saturating_sub(epoch_time)
+    }
+
+    fn retry(
+        &self,
+        req: Request,
+        sleep: Duration,
+        remaining_attempts: usize,
+    ) -> BoxFuture<Result<Response, reqwest::Error>> {
+        #[derive(Debug, serde::Deserialize)]
+        struct RateLimit {
+            pub limit: u64,
+            pub remaining: u64,
+            pub reset: u64,
+        }
+
+        #[derive(Debug, serde::Deserialize)]
+        struct RateLimitResponse {
+            pub resources: Resources,
+        }
+
+        #[derive(Debug, serde::Deserialize)]
+        struct Resources {
+            pub core: RateLimit,
+            pub search: RateLimit,
+            pub graphql: RateLimit,
+            pub source_import: RateLimit,
+        }
+
+        log::warn!(
+            "Retrying after {} seconds, remaining attepts {}",
+            sleep.as_secs(),
+            remaining_attempts,
+        );
+
+        async move {
+            tokio::time::delay_for(sleep).await;
+
+            // check rate limit
+            let rate_resp = self
+                .client
+                .execute(
+                    self.client
+                        .get("https://api.github.com/rate_limit")
+                        .configure(self)
+                        .build()
+                        .unwrap(),
+                )
+                .await?;
+            let rate_limit_response = rate_resp.json::<RateLimitResponse>().await?;
+
+            // Check url for search path because github has different rate limits for the search api
+            let rate_limit = if req
+                .url()
+                .path_segments()
+                .map(|mut segments| matches!(segments.next(), Some("search")))
+                .unwrap_or(false)
+            {
+                rate_limit_response.resources.search
+            } else {
+                rate_limit_response.resources.core
+            };
+
+            // If we still don't have any more remaining attempts, try sleeping for the remaining
+            // period of time
+            if rate_limit.remaining == 0 {
+                let sleep = Self::calc_sleep(rate_limit.reset);
+                if sleep > 0 {
+                    tokio::time::delay_for(Duration::from_secs(sleep)).await;
+                }
+            }
+
+            let resp = self.client.execute(req.try_clone().unwrap()).await?;
+            if let Some(sleep) = Self::needs_retry(&resp).await {
+                if remaining_attempts > 0 {
+                    return self.retry(req, sleep, remaining_attempts - 1).await;
+                }
+            }
+
+            Ok(resp)
+        }
+        .boxed()
+    }
+
     async fn send_req(&self, req: RequestBuilder) -> anyhow::Result<Vec<u8>> {
         let (mut resp, req_dbg) = self._send_req(req).await?;
 
@@ -120,11 +237,11 @@ pub struct Issue {
     pub body: String,
     created_at: chrono::DateTime<Utc>,
     pub title: String,
-    html_url: String,
+    pub html_url: String,
     pub user: User,
-    labels: Vec<Label>,
-    assignees: Vec<User>,
-    pull_request: Option<PullRequestDetails>,
+    pub labels: Vec<Label>,
+    pub assignees: Vec<User>,
+    pub pull_request: Option<PullRequestDetails>,
     // API URL
     comments_url: String,
     #[serde(skip)]
@@ -500,11 +617,128 @@ pub struct IssuesEvent {
     pub label: Option<Label>,
 }
 
+#[derive(Debug, serde::Deserialize)]
+pub struct IssueSearchResult {
+    pub total_count: usize,
+    pub incomplete_results: bool,
+    pub items: Vec<Issue>,
+}
+
 #[derive(Debug, serde::Deserialize)]
 pub struct Repository {
     pub full_name: String,
 }
 
+impl Repository {
+    const GITHUB_API_URL: &'static str = "https://api.github.com";
+
+    pub async fn get_issues<'a>(
+        &self,
+        client: &GithubClient,
+        query: &Query<'a>,
+    ) -> anyhow::Result<Vec<Issue>> {
+        let Query {
+            filters,
+            include_labels,
+            exclude_labels,
+            ..
+        } = query;
+
+        let use_issues = exclude_labels.is_empty() || filters.iter().any(|&(key, _)| key == "no");
+        // negating filters can only be handled by the search api
+        let url = if use_issues {
+            self.build_issues_url(filters, include_labels)
+        } else {
+            self.build_search_issues_url(filters, include_labels, exclude_labels)
+        };
+
+        let result = client.get(&url);
+        if use_issues {
+            client
+                .json(result)
+                .await
+                .with_context(|| format!("failed to list issues from {}", url))
+        } else {
+            let result = client
+                .json::<IssueSearchResult>(result)
+                .await
+                .with_context(|| format!("failed to list issues from {}", url))?;
+            Ok(result.items)
+        }
+    }
+
+    pub async fn get_issues_count<'a>(
+        &self,
+        client: &GithubClient,
+        query: &Query<'a>,
+    ) -> anyhow::Result<usize> {
+        Ok(self.get_issues(client, query).await?.len())
+    }
+
+    fn build_issues_url(&self, filters: &Vec<(&str, &str)>, include_labels: &Vec<&str>) -> String {
+        let filters = filters
+            .iter()
+            .map(|(key, val)| format!("{}={}", key, val))
+            .chain(std::iter::once(format!(
+                "labels={}",
+                include_labels.join(",")
+            )))
+            .chain(std::iter::once("filter=all".to_owned()))
+            .chain(std::iter::once(format!("sort=created")))
+            .chain(std::iter::once(format!("direction=asc")))
+            .collect::<Vec<_>>()
+            .join("&");
+        format!(
+            "{}/repos/{}/issues?{}",
+            Repository::GITHUB_API_URL,
+            self.full_name,
+            filters
+        )
+    }
+
+    fn build_search_issues_url(
+        &self,
+        filters: &Vec<(&str, &str)>,
+        include_labels: &Vec<&str>,
+        exclude_labels: &Vec<&str>,
+    ) -> String {
+        let filters = filters
+            .iter()
+            .map(|(key, val)| format!("{}:{}", key, val))
+            .chain(
+                include_labels
+                    .iter()
+                    .map(|label| format!("label:{}", label)),
+            )
+            .chain(
+                exclude_labels
+                    .iter()
+                    .map(|label| format!("-label:{}", label)),
+            )
+            .chain(std::iter::once(format!("repo:{}", self.full_name)))
+            .collect::<Vec<_>>()
+            .join("+");
+        format!(
+            "{}/search/issues?q={}&sort=created&order=asc",
+            Repository::GITHUB_API_URL,
+            filters
+        )
+    }
+}
+
+pub struct Query<'a> {
+    pub kind: QueryKind,
+    // key/value filter
+    pub filters: Vec<(&'a str, &'a str)>,
+    pub include_labels: Vec<&'a str>,
+    pub exclude_labels: Vec<&'a str>,
+}
+
+pub enum QueryKind {
+    List,
+    Count,
+}
+
 #[derive(Debug)]
 pub enum Event {
     IssueComment(IssueCommentEvent),

+ 3 - 0
src/lib.rs

@@ -10,8 +10,11 @@ pub mod db;
 pub mod github;
 pub mod handlers;
 pub mod interactions;
+pub mod logger;
+pub mod meeting;
 pub mod notification_listing;
 pub mod payload;
+pub mod prioritization;
 pub mod team;
 mod team_data;
 pub mod zulip;

+ 1 - 3
src/main.rs

@@ -5,11 +5,9 @@ use futures::{future::FutureExt, stream::StreamExt};
 use hyper::{header, Body, Request, Response, Server, StatusCode};
 use reqwest::Client;
 use std::{env, net::SocketAddr, sync::Arc};
-use triagebot::{db, github, handlers::Context, notification_listing, payload, EventName};
+use triagebot::{db, github, handlers::Context, logger, notification_listing, payload, EventName};
 use uuid::Uuid;
 
-mod logger;
-
 async fn serve_req(req: Request<Body>, ctx: Arc<Context>) -> Result<Response<Body>, hyper::Error> {
     log::info!("request = {:?}", req);
     let (req, body_stream) = req.into_parts();

+ 208 - 0
src/meeting.rs

@@ -0,0 +1,208 @@
+use async_trait::async_trait;
+
+use reqwest::Client;
+use std::env;
+use std::fs::File;
+use std::io::Read;
+
+use crate::github::{self, GithubClient, Issue, Repository};
+
+pub struct Meeting<A: Action> {
+    pub steps: Vec<A>,
+}
+
+#[async_trait]
+pub trait Action {
+    async fn call(&self) -> String;
+}
+
+pub struct Step<'a> {
+    pub name: &'a str,
+    pub actions: Vec<Query<'a>>,
+}
+
+pub struct Query<'a> {
+    pub repo: &'a str,
+    pub queries: Vec<QueryMap<'a>>,
+}
+
+pub struct QueryMap<'a> {
+    pub name: &'a str,
+    pub query: github::Query<'a>,
+}
+
+pub trait Template: Send {
+    fn render(&self, pre: &str, post: &str) -> String;
+}
+
+pub struct FileTemplate<'a> {
+    name: &'a str,
+    map: Vec<(&'a str, Box<dyn Template>)>,
+}
+
+pub struct IssuesTemplate {
+    issues: Vec<Issue>,
+}
+
+pub struct IssueCountTemplate {
+    count: usize,
+}
+
+#[async_trait]
+impl<'a> Action for Step<'a> {
+    async fn call(&self) -> String {
+        let gh = GithubClient::new(
+            Client::new(),
+            env::var("GITHUB_API_TOKEN").expect("Missing GITHUB_API_TOKEN"),
+        );
+
+        let mut map: Vec<(&str, Box<dyn Template>)> = Vec::new();
+
+        for Query { repo, queries } in &self.actions {
+            let repository = Repository {
+                full_name: repo.to_string(),
+            };
+
+            for QueryMap { name, query } in queries {
+                match query.kind {
+                    github::QueryKind::List => {
+                        let issues_search_result = repository.get_issues(&gh, &query).await;
+
+                        match issues_search_result {
+                            Ok(issues) => {
+                                map.push((*name, Box::new(IssuesTemplate::new(issues))));
+                            }
+                            Err(err) => {
+                                eprintln!("ERROR: {}", err);
+                                err.chain()
+                                    .skip(1)
+                                    .for_each(|cause| eprintln!("because: {}", cause));
+                                std::process::exit(1);
+                            }
+                        }
+                    }
+
+                    github::QueryKind::Count => {
+                        let count = repository.get_issues_count(&gh, &query).await;
+
+                        match count {
+                            Ok(count) => {
+                                map.push((*name, Box::new(IssueCountTemplate::new(count))));
+                            }
+                            Err(err) => {
+                                eprintln!("ERROR: {}", err);
+                                err.chain()
+                                    .skip(1)
+                                    .for_each(|cause| eprintln!("because: {}", cause));
+                                std::process::exit(1);
+                            }
+                        }
+                    }
+                };
+            }
+        }
+
+        let template = FileTemplate::new(self.name, map);
+        template.render("", "")
+    }
+}
+
+impl<'a> FileTemplate<'a> {
+    fn new(name: &'a str, map: Vec<(&'a str, Box<dyn Template>)>) -> Self {
+        Self { name, map }
+    }
+}
+
+impl<'a> Template for FileTemplate<'a> {
+    fn render(&self, _pre: &str, _post: &str) -> String {
+        let relative_path = format!("templates/{}.tt", self.name);
+        let path = env::current_dir().unwrap().join(relative_path);
+        let path = path.as_path();
+        let mut file = File::open(path).unwrap();
+        let mut contents = String::new();
+        file.read_to_string(&mut contents).unwrap();
+
+        let mut replacements = Vec::new();
+
+        for (var, template) in &self.map {
+            let var = format!("{{{}}}", var);
+            for line in contents.lines() {
+                if line.contains(&var) {
+                    if let Some(var_idx) = line.find(&var) {
+                        let pre = &line[..var_idx];
+                        let post = &line[var_idx + var.len()..];
+                        replacements.push((line.to_string(), template.render(pre, post)));
+                    }
+                }
+            }
+        }
+
+        for (line, content) in replacements {
+            contents = contents.replace(&line, &content);
+        }
+
+        contents
+    }
+}
+
+impl IssuesTemplate {
+    fn new(issues: Vec<Issue>) -> Self {
+        Self { issues }
+    }
+}
+
+impl Template for IssuesTemplate {
+    fn render(&self, pre: &str, post: &str) -> String {
+        let mut out = String::new();
+
+        if !self.issues.is_empty() {
+            for issue in &self.issues {
+                let pr = if issue.pull_request.is_some() {
+                    // FIXME: link to PR.
+                    // We need to tweak PullRequestDetails for this
+                    "[has_pr] "
+                } else {
+                    ""
+                };
+
+                out.push_str(&format!(
+                    "{}\"{}\" [#{}]({}) {}labels=[{}] assignees=[{}]{}\n",
+                    pre,
+                    issue.title,
+                    issue.number,
+                    issue.html_url,
+                    pr,
+                    issue
+                        .labels
+                        .iter()
+                        .map(|l| l.name.as_ref())
+                        .collect::<Vec<_>>()
+                        .join(", "),
+                    issue
+                        .assignees
+                        .iter()
+                        .map(|u| u.login.as_ref())
+                        .collect::<Vec<_>>()
+                        .join(", "),
+                    post,
+                ));
+            }
+        } else {
+            out = format!("Empty");
+        }
+
+        out
+    }
+}
+
+impl IssueCountTemplate {
+    fn new(count: usize) -> Self {
+        Self { count }
+    }
+}
+
+impl Template for IssueCountTemplate {
+    fn render(&self, pre: &str, post: &str) -> String {
+        format!("{}{}{}", pre, self.count, post)
+    }
+}

+ 512 - 0
src/prioritization.rs

@@ -0,0 +1,512 @@
+use crate::github;
+use crate::meeting::{Meeting, Query, QueryMap, Step};
+
+pub fn prepare_meeting<'a>() -> Meeting<Step<'a>> {
+    Meeting {
+        steps: vec![
+            unpri_i_prioritize(),
+            regressions(),
+            nominations(),
+            prs_waiting_on_team(),
+            agenda(),
+            final_review(),
+        ],
+    }
+}
+
+pub fn unpri_i_prioritize<'a>() -> Step<'a> {
+    let mut queries = Vec::new();
+
+    queries.push(QueryMap {
+        name: "unpri_i_prioritize.all",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["I-prioritize"],
+            exclude_labels: vec!["P-critical", "P-high", "P-medium", "P-low"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "unpri_i_prioritize.t_compiler",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["I-prioritize", "T-compiler"],
+            exclude_labels: vec!["P-critical", "P-high", "P-medium", "P-low"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "unpri_i_prioritize.libs_impl",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["I-prioritize", "libs-impl"],
+            exclude_labels: vec!["P-critical", "P-high", "P-medium", "P-low"],
+        },
+    });
+
+    Step {
+        name: "unpri_i_prioritize",
+        actions: vec![Query {
+            repo: "rust-lang/rust",
+            queries,
+        }],
+    }
+}
+
+// FIXME: we should filter out `T-libs` ones given that we only want `libs-impl` but meanwhile
+// we are in a kind of transition state we have all of them.
+pub fn regressions<'a>() -> Step<'a> {
+    let mut queries = Vec::new();
+
+    queries.push(QueryMap {
+        name: "regressions.stable_to_beta",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["regression-from-stable-to-beta"],
+            exclude_labels: vec![
+                "P-critical",
+                "P-high",
+                "P-medium",
+                "P-low",
+                "T-infra",
+                "T-release",
+                "T-libs",
+            ],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "regressions.stable_to_nightly",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["regression-from-stable-to-nightly"],
+            exclude_labels: vec![
+                "P-critical",
+                "P-high",
+                "P-medium",
+                "P-low",
+                "T-infra",
+                "T-release",
+                "T-libs",
+            ],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "regressions.stable_to_stable",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["regression-from-stable-to-stable"],
+            exclude_labels: vec![
+                "P-critical",
+                "P-high",
+                "P-medium",
+                "P-low",
+                "T-infra",
+                "T-release",
+                "T-libs",
+            ],
+        },
+    });
+
+    Step {
+        name: "regressions",
+        actions: vec![Query {
+            repo: "rust-lang/rust",
+            queries,
+        }],
+    }
+}
+
+pub fn nominations<'a>() -> Step<'a> {
+    let mut queries = Vec::new();
+
+    queries.push(QueryMap {
+        name: "nominations.stable_nominated",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![],
+            include_labels: vec!["stable-nominated"],
+            exclude_labels: vec!["stable-accepted"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "nominations.beta_nominated",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![],
+            include_labels: vec!["beta-nominated"],
+            exclude_labels: vec!["beta-accepted"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "nominations.i_nominated",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["I-nominated"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "nominations.i_nominated_t_compiler",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["I-nominated", "T-compiler"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "nominations.i_nominated_libs_impl",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["I-nominated", "libs-impl"],
+            exclude_labels: vec![],
+        },
+    });
+
+    Step {
+        name: "nominations",
+        actions: vec![Query {
+            repo: "rust-lang/rust",
+            queries,
+        }],
+    }
+}
+
+pub fn prs_waiting_on_team<'a>() -> Step<'a> {
+    let mut queries = Vec::new();
+
+    queries.push(QueryMap {
+        name: "prs_waiting_on_team.all",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["S-waiting-on-team"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "prs_waiting_on_team.t_compiler",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["S-waiting-on-team", "T-compiler"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "prs_waiting_on_team.libs_impl",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["S-waiting-on-team", "libs-impl"],
+            exclude_labels: vec![],
+        },
+    });
+
+    Step {
+        name: "prs_waiting_on_team",
+        actions: vec![Query {
+            repo: "rust-lang/rust",
+            queries,
+        }],
+    }
+}
+
+pub fn agenda<'a>() -> Step<'a> {
+    let mut queries = Vec::new();
+    let mut actions = Vec::new();
+
+    queries.push(QueryMap {
+        name: "mcp.seconded",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["major-change", "final-comment-period"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "mcp.new_not_seconded",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["major-change", "to-announce"],
+            exclude_labels: vec!["final-comment-period"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "mcp.old_not_seconded",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["major-change"],
+            exclude_labels: vec!["to-announce", "final-comment-period"],
+        },
+    });
+
+    actions.push(Query {
+        repo: "rust-lang/compiler-team",
+        queries,
+    });
+
+    let mut queries = Vec::new();
+
+    queries.push(QueryMap {
+        name: "beta_nominated.t_compiler",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![],
+            include_labels: vec!["beta-nominated", "T-compiler"],
+            exclude_labels: vec!["beta-accepted"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "beta_nominated.libs_impl",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![],
+            include_labels: vec!["beta-nominated", "libs-impl"],
+            exclude_labels: vec!["beta-accepted"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "beta_nominated.t_rustdoc",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![],
+            include_labels: vec!["beta-nominated", "T-rustdoc"],
+            exclude_labels: vec!["beta-accepted"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "stable_nominated.t_compiler",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![],
+            include_labels: vec!["stable-nominated", "T-compiler"],
+            exclude_labels: vec!["stable-accepted"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "stable_nominated.t_rustdoc",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![],
+            include_labels: vec!["stable-nominated", "T-rustdoc"],
+            exclude_labels: vec!["stable-accepted"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "stable_nominated.libs_impl",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![],
+            include_labels: vec!["stable-nominated", "libs-impl"],
+            exclude_labels: vec!["stable-accepted"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "prs_waiting_on_team.t_compiler",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["S-waiting-on-team", "T-compiler"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "prs_waiting_on_team.libs_impl",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["S-waiting-on-team", "libs-impl"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "issues_of_note.p_critical",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["T-compiler", "P-critical"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "issues_of_note.unassigned_p_critical",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open"), ("no", "assignee")],
+            include_labels: vec!["T-compiler", "P-critical"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "issues_of_note.p_high",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["T-compiler", "P-high"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "issues_of_note.unassigned_p_high",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open"), ("no", "assignee")],
+            include_labels: vec!["T-compiler", "P-high"],
+            exclude_labels: vec![],
+        },
+    });
+
+    // - [N regression-from-stable-to-stable](https://github.com/rust-lang/rust/labels/regression-from-stable-to-stable)
+    //   - [M of those are not prioritized](https://github.com/rust-lang/rust/issues?q=is%3Aopen+label%3Aregression-from-stable-to-stable+-label%3AP-critical+-label%3AP-high+-label%3AP-medium+-label%3AP-low).
+    //
+    // There are N (more|less) `P-critical` issues and M (more|less) `P-high` issues in comparison with last week.
+    queries.push(QueryMap {
+        name: "issues_of_note.regression_from_stable_to_beta",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["regression-from-stable-to-beta"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "issues_of_note.regression_from_stable_to_nightly",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["regression-from-stable-to-nightly"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "issues_of_note.regression_from_stable_to_stable",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["regression-from-stable-to-stable"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "p_critical.t_compiler",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["T-compiler", "P-critical"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "p_critical.libs_impl",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["libs-impl", "P-critical"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "p_critical.t_rustdoc",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["T-rustdoc", "P-critical"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "beta_regressions.unassigned_p_high",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open"), ("no", "assignee")],
+            include_labels: vec!["regression-from-stable-to-beta", "P-high"],
+            exclude_labels: vec!["T-infra", "T-release"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "nightly_regressions.unassigned_p_high",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open"), ("no", "assignee")],
+            include_labels: vec!["regression-from-stable-to-nightly", "P-high"],
+            exclude_labels: vec!["T-infra", "T-release"],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "i_nominated.t_compiler",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["I-nominated", "T-compiler"],
+            exclude_labels: vec![],
+        },
+    });
+
+    queries.push(QueryMap {
+        name: "i_nominated.libs_impl",
+        query: github::Query {
+            kind: github::QueryKind::List,
+            filters: vec![("state", "open")],
+            include_labels: vec!["I-nominated", "libs-impl"],
+            exclude_labels: vec![],
+        },
+    });
+
+    actions.push(Query {
+        repo: "rust-lang/rust",
+        queries,
+    });
+
+    Step {
+        name: "agenda",
+        actions,
+    }
+}
+
+pub fn final_review<'a>() -> Step<'a> {
+    Step {
+        name: "final_review",
+        actions: vec![],
+    }
+}

+ 118 - 0
templates/agenda.tt

@@ -0,0 +1,118 @@
+---
+tags: prioritization, rustc
+---
+
+# T-compiler Meeting Agenda YYYY-MM-DD
+
+[Tracking Issue](https://github.com/rust-lang/rust/issues/54818)
+
+## Announcements
+
+- Major Changes Proposals:
+  - Seconded proposals (in FCP)
+    - {mcp.seconded}
+  - New proposals (not seconded)
+    - {mcp.new_not_seconded}
+  - Old proposals (not seconded)
+    - {mcp.old_not_seconded}
+
+## Beta-nominations
+
+[T-compiler beta noms](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=label%3Abeta-nominated+label%3AT-compiler)
+
+- {beta_nominated.t_compiler} :back: / :hand:
+
+[libs-impl beta noms](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=label%3Abeta-nominated+label%3Alibs-impl)
+
+- {beta_nominated.libs_impl} :back: / :hand:
+
+[T-rustdoc beta noms](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=label%3Abeta-nominated+label%3AT-rustdoc)
+
+- {beta_nominated.t_rustdoc} :back: / :hand:
+
+## Stable-nominations
+
+[T-compiler stable noms](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=label%3Astable-nominated+label%3AT-compiler)
+
+- {stable_nominated.t_compiler} :back: / :hand:
+
+[libs-impl stable noms](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=label%3Astable-nominated+label%3Alibs-impl)
+
+- {stable_nominated.libs_impl} :back: / :hand:
+
+[T-rustdoc stable noms](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=label%3Astable-nominated+label%3AT-rustdoc)
+
+- {stable_nominated.t_rustdoc} :back: / :hand:
+
+## PR's S-waiting-on-team
+
+[T-compiler S-waiting-on-team](https://github.com/rust-lang/rust/pulls?utf8=%E2%9C%93&q=is%3Aopen+label%3AS-waiting-on-team+label%3AT-compiler)
+
+- {prs_waiting_on_team.t_compiler}
+
+[libs-impl S-waiting-on-team](https://github.com/rust-lang/rust/pulls?utf8=%E2%9C%93&q=is%3Aopen+label%3AS-waiting-on-team+label%3Alibs-impl)
+
+- {prs_waiting_on_team.libs_impl}
+
+## Issues of Note
+
+### Short Summary
+
+- [N P-critical issues](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3AT-compiler+label%3AP-critical+)
+  - [M of those are unassigned](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3AT-compiler+label%3AP-critical+no%3Aassignee)
+- [N P-high issues](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3AT-compiler+label%3AP-high+)
+  - [M of those are unassigned](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3AT-compiler+label%3AP-high+no%3Aassignee)
+- [N P-high, M P-medium, P P-low regression-from-stable-to-beta](https://github.com/rust-lang/rust/labels/regression-from-stable-to-beta)
+  - The only assigned are the P-high ones.
+- [N P-high, M P-medium, P P-low regression-from-stable-to-nightly](https://github.com/rust-lang/rust/labels/regression-from-stable-to-nightly)
+  - There are N P-medium assigned.
+- [N regression-from-stable-to-stable](https://github.com/rust-lang/rust/labels/regression-from-stable-to-stable)
+  - [M of those are not prioritized](https://github.com/rust-lang/rust/issues?q=is%3Aopen+label%3Aregression-from-stable-to-stable+-label%3AP-critical+-label%3AP-high+-label%3AP-medium+-label%3AP-low).
+
+There are N (more|less) `P-critical` issues and M (more|less) `P-high` issues in comparison with last week.
+
+### P-critical
+
+- {p_critical.t_compiler}
+  - This issue is assigned to @person
+
+- {p_critical.libs_impl}
+
+- {p_critical.t_rustdoc}
+
+### Unassigned P-high regressions
+
+- {beta_regressions.unassigned_p_high}
+- {nightly_regressions.unassigned_p_high}
+
+## Performance logs
+
+[Triage done by njn](https://github.com/rust-lang/rustc-perf/tree/master/triage#triage-logs)
+
+Regressions
+
+- Regression #1
+
+Improvements
+
+- Improvement #1
+
+## Nominated Issues
+
+[T-compiler I-nominated](https://github.com/rust-lang/rust/issues?q=is%3Aopen+label%3AI-nominated+label%3AT-compiler)
+
+- {i_nominated.t_compiler}
+
+[libs-impl I-nominated](https://github.com/rust-lang/rust/issues?q=is%3Aopen+label%3AI-nominated+label%3Alibs-impl)
+
+- {i_nominated.libs_impl}
+
+## WG checkins
+
+@**WG-X** checkin by @**person1**:
+
+> Checkin text
+
+@**WG-Y** checkin by @**person2**:
+
+> Checkin text

+ 27 - 0
templates/final_review.tt

@@ -0,0 +1,27 @@
+## Announcements
+
+- Check the compiler calendar to see if there's an outstanding event to announce.
+  - Add those to the agenda properly linking issues and documents.
+
+## Nominate issues
+
+Check how packed the agenda looks like and if there's room for more, consider the following ...
+
+- [All Stable-to-beta regressions](https://github.com/rust-lang/rust/labels/regression-from-stable-to-beta)
+  - Check if there are relevant issues that are worth raising awareness.
+  - Assign if possible; if it remains unassigned, add it to agenda so we can assign during the meeting.
+- [All Stable-to-nightly regressions](https://github.com/rust-lang/rust/labels/regression-from-stable-to-nightly)
+  - Check if there are relevant issues that are worth raising awareness.
+  - Assign if possible; if it remains unassigned, add it to agenda so we can assign during the meeting.
+- [P-high issues](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3AT-compiler+label%3AP-high+)
+  - [unassigned](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3AT-compiler+label%3AP-high+no%3Aassignee)
+
+## Toolstate
+
+- Check [toolstate](https://rust-lang-nursery.github.io/rust-toolstate/) for outstanding tool breakage.
+  - Notify teams in the corresponding channels
+
+## Performance regressions
+
+- Check [perf regressions](http://perf.rust-lang.org/index.html).
+  - Notify involved actors.

+ 45 - 0
templates/nominations.tt

@@ -0,0 +1,45 @@
+## Stable nominations
+
+- [All stable nominations](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=+label%3Astable-nominated)
+    - Add T-compiler, libs-impl or T-rustdoc tag when it corresponds.
+    - Do a sanity check on them.
+
+### Issues
+
+{nominations.stable_nominated}
+
+## Beta nominations
+
+- [All beta nominations](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=label%3Abeta-nominated)
+    - Add T-compiler, libs-impl or T-rustdoc tag when it corresponds.
+    - Do a sanity check on them.
+
+### Issues
+
+- {nominations.beta_nominated}
+
+## I-nominated
+
+1. [All I-nominated](https://github.com/rust-lang/rust/labels/I-nominated)
+    - Add T-compiler or libs-impl tag when it corresponds.
+    - Do a sanity check on them.
+
+### Issues
+
+- {nominations.i_nominated}
+
+2. [I-nominated T-compiler](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3AI-nominated+label%3AT-compiler)
+    - Remove leftovers from last meeting.
+    - Do a sanity check on them.
+
+### Issues
+
+- {nominations.i_nominated_t_compiler}
+
+3. [I-nominated libs-impl](https://github.com/rust-lang/rust/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3AI-nominated+label%3Alibs-impl)
+    - Remove leftovers from last meeting.
+    - Do a sanity check on them.
+
+### Issues
+
+- {nominations.i_nominated_libs_impl}

+ 23 - 0
templates/prs_waiting_on_team.tt

@@ -0,0 +1,23 @@
+## PR's waiting for on team
+
+1. [All PR's waiting on team](https://github.com/rust-lang/rust/pulls?q=is%3Aopen+is%3Apr+label%3AS-waiting-on-team)
+    - Add T-compiler or libs-impl tag when it corresponds.
+    - Do a sanity check on them.
+
+### Issues
+
+- {prs_waiting_on_team.all}
+
+2. [PR's waiting on T-compiler](https://github.com/rust-lang/rust/pulls?q=is%3Aopen+is%3Apr+label%3AS-waiting-on-team+label%3AT-compiler)
+    - Explicitly nominate any that you think may be able to be resolved *quickly* in triage meeting.
+
+### Issues
+
+- {prs_waiting_on_team.t_compiler}
+
+3. [PR's waiting on libs-impl](https://github.com/rust-lang/rust/pulls?q=is%3Aopen+is%3Apr+label%3AS-waiting-on-team+label%3Alibs-impl)
+    - Explicitly nominate any that you think may be able to be resolved *quickly* in triage meeting.
+
+### Issues
+
+- {prs_waiting_on_team.libs_impl}

+ 28 - 0
templates/regressions.tt

@@ -0,0 +1,28 @@
+## Regressions
+
+1. [Beta regressions without P-label](https://github.com/rust-lang/rust/issues?q=is%3Aopen+label%3Aregression-from-stable-to-beta+-label%3AP-critical+-label%3AP-high+-label%3AP-medium+-label%3AP-low+-label%3AT-infra+-label%3AT-release)
+    - Prioritize.
+    - Ping appropriate people and/or [ICE-breakers](https://rustc-dev-guide.rust-lang.org/ice-breaker/about.html#tagging-an-issue-for-an-ice-breaker-group).
+    - Assign if possible; if it remains unassigned, add it to agenda so we can assign during the meeting.
+
+### Issues
+
+- {regressions.stable_to_beta}
+
+2. [Nightly regressions without P-label](https://github.com/rust-lang/rust/issues?q=is%3Aopen+label%3Aregression-from-stable-to-nightly+-label%3AP-critical+-label%3AP-high+-label%3AP-medium+-label%3AP-low+-label%3AT-infra+-label%3AT-release)
+    - Prioritize.
+    - Ping appropriate people and/or [ICE-breakers](https://rustc-dev-guide.rust-lang.org/ice-breaker/about.html#tagging-an-issue-for-an-ice-breaker-group).
+    - Assign if possible; if it remains unassigned, add it to agenda so we can assign during the meeting.
+
+### Issues
+
+- {regressions.stable_to_nightly}
+
+3. [Stable regressions without P-label](https://github.com/rust-lang/rust/issues?q=is%3Aopen+label%3Aregression-from-stable-to-stable+-label%3AP-critical+-label%3AP-high+-label%3AP-medium+-label%3AP-low+-label%3AT-infra+-label%3AT-release)
+    - Prioritize (once we have this under control).
+    - Ping appropriate people and/or [ICE-breakers](https://rustc-dev-guide.rust-lang.org/ice-breaker/about.html#tagging-an-issue-for-an-ice-breaker-group).
+    - Assign if possible; if it remains unassigned, add it to agenda so we can assign during the meeting.
+
+### Issues
+
+- {regressions.stable_to_stable}

+ 27 - 0
templates/unpri_i_prioritize.tt

@@ -0,0 +1,27 @@
+## Unprioritized I-prioritize
+
+1. [All unprioritized I-prioritize](https://github.com/rust-lang/rust/issues?q=is%3Aopen+is%3Aissue+-label%3AP-critical+-label%3AP-high+-label%3AP-medium+-label%3AP-low+label%3AI-prioritize)
+    - Add T-compiler or libs-impl tag when it corresponds.
+    - Do a sanity check on them.
+
+### Issues
+
+- {unpri_i_prioritize.all}
+
+2. [T-compiler](https://github.com/rust-lang/rust/issues?q=is%3Aopen+is%3Aissue+label%3AT-compiler+-label%3AP-critical+-label%3AP-high+-label%3AP-medium+-label%3AP-low+label%3AI-prioritize)
+    - Prioritize issues and remove nomination of the ones not worth discussing.
+    - Tag regressions accordingly.
+    - Ping appropriate people and/or [ICE-breakers](https://rustc-dev-guide.rust-lang.org/ice-breaker/about.html#tagging-an-issue-for-an-ice-breaker-group).
+
+### Issues
+
+- {unpri_i_prioritize.t_compiler}
+
+3. [libs-impl](https://github.com/rust-lang/rust/issues?q=is%3Aopen+is%3Aissue+label%3Alibs-impl+-label%3AP-critical+-label%3AP-high+-label%3AP-medium+-label%3AP-low+label%3AI-prioritize)
+    - Prioritize issues and remove nomination of the ones not worth discussing.
+    - Tag regressions accordingly.
+    - Ping appropriate people and/or [ICE-breakers](https://rustc-dev-guide.rust-lang.org/ice-breaker/about.html#tagging-an-issue-for-an-ice-breaker-group).
+
+### Issues
+
+- {unpri_i_prioritize.libs_impl}