From e39214acb22f1ed3614a078bf4c09afd84fd9c58 Mon Sep 17 00:00:00 2001 From: ltdk Date: Tue, 13 Oct 2026 13:25:12 -0400 Subject: [PATCH] Spicier agenda changes --- tools/agenda-generator/Cargo.toml | 1 - tools/agenda-generator/src/generator.rs | 385 ++++++++++++------------ tools/agenda-generator/src/main.rs | 29 +- 3 files changed, 198 insertions(+), 217 deletions(-) diff --git a/tools/agenda-generator/Cargo.toml b/tools/agenda-generator/Cargo.toml index 1f17410..67c1aa4 100644 --- a/tools/agenda-generator/Cargo.toml +++ b/tools/agenda-generator/Cargo.toml @@ -6,7 +6,6 @@ edition = "2024" [dependencies] chrono = { version = "0.4", features = ["serde"] } -clap = { version = "4", features = ["derive"] } color-eyre = "0.6" itertools = "0.12" rand = "0.8.5" diff --git a/tools/agenda-generator/src/generator.rs b/tools/agenda-generator/src/generator.rs index 4977613..be7bf94 100644 --- a/tools/agenda-generator/src/generator.rs +++ b/tools/agenda-generator/src/generator.rs @@ -1,7 +1,10 @@ use std::borrow::Borrow; use std::cmp::Reverse; use std::collections::{BTreeMap, BTreeSet}; +use std::env; use std::fmt::Write; +use std::process::Command; +use std::sync::LazyLock; use chrono::{Duration, NaiveDateTime}; use color_eyre::eyre::{Result, WrapErr}; @@ -26,15 +29,15 @@ struct FcpWithInfo { issue: FcpIssue, status_comment: IssueComment, #[serde(skip)] - checkboxes: Checkboxes, + checkboxes: UncheckedBoxes, } #[derive(Deserialize, Default)] -struct Checkboxes { - libs_unchecked: usize, - libs_and_former_fcp_unchecked: usize, - other_unchecked: BTreeMap>, - total_other_unchecked: usize, +struct UncheckedBoxes { + libs: usize, + libs_and_former_fcp: usize, + other: BTreeMap>, + total_other: usize, } #[derive(Deserialize)] @@ -107,11 +110,11 @@ macro_rules! format_with_anchor { } fn shorten(url: &str, kind: Shorten) -> String { - let (url, anchor) = url.split_once("#").unwrap_or((url, "")); + let (url, anchor) = url.split_once('#').unwrap_or((url, "")); if let Some(suffix) = url.strip_prefix("https://github.com/rust-lang/") - && let Some((repo, suffix)) = suffix.split_once("/") - && let Some((issue_kind @ ("issues" | "pull"), num)) = suffix.split_once("/") + && let Some((repo, suffix)) = suffix.split_once('/') + && let Some((issue_kind @ ("issues" | "pull"), num)) = suffix.split_once('/') { let (short, tf) = match repo { "rust" => ("", true), @@ -142,30 +145,65 @@ fn shorten(url: &str, kind: Shorten) -> String { } } +const REPOS_NEEDING_PRIORITIZATION: &[&str] = &["rust-lang/rust", "rust-lang/stdarch"]; +const REPOS_CODE: &[&str] = &["rust-lang/rust", "rust-lang/stdarch"]; +const REPOS_ALL: &[&str] = &[ + "rust-lang/rust", + "rust-lang/rfcs", + "rust-lang/goals", + "rust-lang/libs-team", + "rust-lang/stdarch", + "rust-lang/std-dev-guide", + "rust-lang/rust-forge", +]; + impl Generator { pub fn libs_agenda(mut self) -> Result { writeln!( &mut self.agenda, - "# Libs Meeting {} + "\ +# Libs Meeting {} ###### tags: `Libs Meetings` `Minutes` -**Meeting Link**: https://meet.jit.si/the-libs-agenda -**Meeting Link 2**: https://meet.jit.si/the-libs-agenda-2 -**Attendees**: ... +**Meeting Link**: (to be changed) + +## Attendees + +- ## Agenda +- Announcements (optional) - Nominated items -- Regressions +- Radar - FCPs (optional) - ACPs (optional) +- Tracking issues (optional) - Anything else? +## Announcements + +- LC updates? +- Other updates? + ", chrono::Utc::now().format("%Y-%m-%d") )?; + self.triage()?; + self.fcps()?; + self.acps()?; + self.tracking_issues()?; + + writeln!( + &mut self.agenda, + "_Generated by [fully-automatic-rust-libs-team-triage-meeting-agenda-generator](https://github.com/rust-lang/libs-team/tree/main/tools/agenda-generator)_", + )?; + Ok(self.agenda) + } + + fn triage(&mut self) -> Result<()> { writeln!( &mut self.agenda, "## Triage @@ -175,153 +213,48 @@ impl Generator { GithubQuery::new("Critical") .labels(&["T-libs", "P-critical"]) - .repo("rust-lang/rust") - .repo("rust-lang/rfcs") - .write(&mut self)?; + .repos(REPOS_CODE) + .write(&mut *self)?; - GithubQuery::new("Backports") + GithubQuery::new("Stable Backports") .labels(&["T-libs", "stable-nominated"]) + .exclude_labels(&["stable-accepted"]) + .state(State::Any) + .repos(&["rust-lang/rust"]) + .write(&mut *self)?; + + GithubQuery::new("Beta Backports") .labels(&["T-libs", "beta-nominated"]) .exclude_labels(&["beta-accepted"]) .state(State::Any) - .repo("rust-lang/rust") - .repo("rust-lang/rfcs") - .write(&mut self)?; + .repos(&["rust-lang/rust"]) + .write(&mut *self)?; - GithubQuery::new("Prioritization Requested") + GithubQuery::new("Prioritize") .labels(&["T-libs", "I-prioritize"]) - .repo("rust-lang/rust") - .repo("rust-lang/rfcs") - .write(&mut self)?; + .repos(REPOS_NEEDING_PRIORITIZATION) + .write(&mut *self)?; GithubQuery::new("Nominated") .labels(&["I-libs-nominated"]) - .repo("rust-lang/rust") - .repo("rust-lang/rfcs") - .repo("rust-lang/libs-team") - .repo("rust-lang/goals") - .repo("rust-lang/stdarch") - .repo("rust-lang/std-dev-guide") - .repo("rust-lang/rust-forge") - .write(&mut self)?; - - GithubQuery::new("Waiting on Team") - .labels(&["S-waiting-on-t-libs"]) - .repo("rust-lang/rust") - .repo("rust-lang/rfcs") - .repo("rust-lang/stdarch") - .repo("rust-lang/std-dev-guide") - .repo("rust-lang/rust-forge") - .write(&mut self)?; - - GithubQuery::new("needs decision") .labels(&["T-libs", "I-needs-decision"]) - .repo("rust-lang/rust") - .write(&mut self)?; - - GithubQuery::new("Regressions") - .labels(&["T-libs", "regression-untriaged"]) - .labels(&["T-libs", "regression-from-stable-to-stable"]) - .labels(&["T-libs", "regression-from-stable-to-beta"]) - .labels(&["T-libs", "regression-from-stable-to-nightly"]) - .exclude_labels(&["I-libs-nominated"]) - .exclude_labels(&["P-low"]) - .repo("rust-lang/rust") - .repo("rust-lang/rfcs") - .write(&mut self)?; - - self.fcps(String::from("T-libs"))?; - - writeln!( - &mut self.agenda, - "## ACPs - -", - )?; - - let new_proposals = GithubQuery::new("New") - .labels(&["api-change-proposal"]) - .exclude_labels(&["ACP-accepted"]) - .repo("rust-lang/libs-team") - .sort(Sort::Newest) - .take(10) - .rev(true) - .write(&mut self)?; - - GithubQuery::new("Stalled") - .labels(&["api-change-proposal"]) - .exclude_labels(&["ACP-accepted"]) - .repo("rust-lang/libs-team") - .sort(Sort::LeastRecentlyUpdated) - .take(10) - .skip(new_proposals.iter()) - .shuffle(true) - .write(&mut self)?; - - writeln!( - &mut self.agenda, - "_Generated by [fully-automatic-rust-libs-team-triage-meeting-agenda-generator](https://github.com/rust-lang/libs-team/tree/main/tools/agenda-generator)_", - )?; - Ok(self.agenda) - } - - pub fn error_handling_pg_agenda(mut self) -> Result { - writeln!( - &mut self.agenda, - "# Project Group Error Handling Meeting {} - -###### tags: `Error Handling` `Minutes` - -**Attendees**: ... - -## Agenda Items - -- [Open action items](https://hackmd.io/@rust-libs/Hyj7kRSld) -- Triage -- Individual Status Updates - -## Triage -", - chrono::Utc::now().format("%Y-%m-%d") - )?; + .labels(&["S-waiting-on-t-libs"]) + .repos(REPOS_ALL) + .write(&mut *self)?; - GithubQuery::new("Nominated") - .labels(&["PG-error-handling", "I-nominated"]) - .repo("rust-lang/rust") - .repo("rust-lang/project-error-handling") - .write(&mut self)?; - - GithubQuery::new("PG Error Handling") - .labels(&["PG-error-handling"]) - .repo("rust-lang/rust") - .repo("rust-lang/project-error-handling") - .write(&mut self)?; - - GithubQuery::new("Area Error Handling") - .labels(&["A-error-handling"]) - .repo("rust-lang/rust") - .write(&mut self)?; - - GithubQuery::new("PG Error Handling") - .repo("rust-lang/project-error-handling") - .write(&mut self)?; - - writeln!(&mut self.agenda, - "## Actions - -- [ ] Reply to all issues/PRs discussed in this meeting, or add them to the [open action items](https://hackmd.io/UrERZvi5RwyxfGvo-RtC6g). -", - )?; + GithubQuery::new("Radar") + .labels(&["I-libs-radar"]) + .exclude_labels(&["I-libs-nominated"]) + .exclude_labels(&["S-waiting-on-t-libs"]) + .repos(REPOS_ALL) + .write(&mut *self)?; - writeln!( - &mut self.agenda, - "_Generated by [fully-automatic-rust-libs-team-triage-meeting-agenda-generator](https://github.com/rust-lang/libs-team/tree/main/tools/agenda-generator)_" - )?; - Ok(self.agenda) + Ok(()) } - fn fcps(&mut self, label: String) -> Result<()> { - let fcps: Vec = reqwest::blocking::get("https://rfcbot.rs/api/all")?.json()?; + fn list_fcps() -> Result> { + let mut fcps: Vec = + reqwest::blocking::get("https://rfcbot.rs/api/all")?.json()?; let mut fcp_teams: FcpTeams = reqwest::blocking::get("https://team-api.infra.rust-lang.org/v1/rfcbot.json")? @@ -339,17 +272,7 @@ impl Generator { let teams: Teams = reqwest::blocking::get("https://team-api.infra.rust-lang.org/v1/teams.json")?.json()?; - self.write_fcps(label, fcps, fcp_teams, teams) - } - - fn write_fcps( - &mut self, - label: String, - mut fcps: Vec, - fcp_teams: FcpTeams, - teams: Teams, - ) -> Result<()> { - fcps.retain(|fcp| fcp.issue.labels.contains(&label)); + fcps.retain(|fcp| fcp.issue.labels.iter().any(|x| x == "T-libs")); // Don't filter out FCPs. if false { @@ -395,14 +318,14 @@ impl Generator { .unwrap_or_else(|| String::from("Z-other")); if let Some(name) = team_name.strip_prefix("A-") { if name == "libs" { - fcp.checkboxes.libs_unchecked += 1; + fcp.checkboxes.libs += 1; } - fcp.checkboxes.libs_and_former_fcp_unchecked += 1; + fcp.checkboxes.libs_and_former_fcp += 1; } else { - fcp.checkboxes.total_other_unchecked += 1; + fcp.checkboxes.total_other += 1; } fcp.checkboxes - .other_unchecked + .other .entry(team_name) .or_default() .insert(reviewer.clone()); @@ -412,26 +335,30 @@ impl Generator { fcps.sort_by_key(|fcp| { ( // move all cases where libs has nothing left to check, and there are no concerns, to the end - fcp.checkboxes.libs_and_former_fcp_unchecked == 0 && fcp.concerns.is_empty(), + fcp.checkboxes.libs_and_former_fcp == 0 && fcp.concerns.is_empty(), // then, sort by number of concerns ascending (starting at 0) fcp.concerns.len(), // prefer things that libs can check boxes for - Reverse(fcp.checkboxes.libs_and_former_fcp_unchecked > 0), + Reverse(fcp.checkboxes.libs_and_former_fcp > 0), // prefer things where libs checking boxes would achieve N-2 threshold - Reverse(fcp.checkboxes.total_other_unchecked <= 2), + Reverse(fcp.checkboxes.total_other <= 2), // sort by the number of remaining checkboxes, ascending - fcp.checkboxes.libs_and_former_fcp_unchecked + fcp.checkboxes.total_other_unchecked, + fcp.checkboxes.libs_and_former_fcp + fcp.checkboxes.total_other, // prioritize ones where libs has the most checkboxes of the total - Reverse(fcp.checkboxes.libs_unchecked), + Reverse(fcp.checkboxes.libs), // prioritize ones where also the former FCP team has checkboxes - Reverse(fcp.checkboxes.libs_and_former_fcp_unchecked), + Reverse(fcp.checkboxes.libs_and_former_fcp), ) }); + Ok(fcps) + } + + fn fcps(&mut self) -> Result<()> { writeln!(self.agenda, "## FCPs")?; writeln!(self.agenda)?; - for fcp in &fcps { + for fcp in Generator::list_fcps()? { let url = shorten( &format!( "https://github.com/{}/issues/{}#issuecomment-{}", @@ -441,14 +368,12 @@ impl Generator { ); writeln!(self.agenda, "#### {url} {}", escape(&fcp.issue.title))?; - if fcp.checkboxes.libs_and_former_fcp_unchecked == 0 - && fcp.checkboxes.total_other_unchecked == 0 - { + if fcp.checkboxes.libs_and_former_fcp == 0 && fcp.checkboxes.total_other == 0 { writeln!(self.agenda, "- has all checkboxes")?; } else { - for (team_name, team) in &fcp.checkboxes.other_unchecked { + for (team_name, team) in &fcp.checkboxes.other { let team_name = team_name - .split_once("-") + .split_once('-') .map_or(&**team_name, |(_, after)| after); let count = team.len(); write!(self.agenda, "- needs {count} {team_name}:")?; @@ -484,6 +409,62 @@ impl Generator { Ok(()) } + fn acps(&mut self) -> Result<()> { + writeln!(&mut self.agenda, "## ACPs")?; + writeln!(&mut self.agenda)?; + + let new_proposals = GithubQuery::new("New") + .labels(&["api-change-proposal"]) + .exclude_labels(&["ACP-accepted"]) + .repos(&["rust-lang/libs-team"]) + .sort(Sort::Newest) + .take(10) + .rev(true) + .write(&mut *self)?; + + GithubQuery::new("Stalled") + .labels(&["api-change-proposal"]) + .exclude_labels(&["ACP-accepted"]) + .repos(&["rust-lang/libs-team"]) + .sort(Sort::LeastRecentlyUpdated) + .take(10) + .skip(new_proposals.iter()) + .shuffle(true) + .write(&mut *self)?; + + Ok(()) + } + + fn tracking_issues(&mut self) -> Result<()> { + writeln!(&mut self.agenda, "## Tracking issues")?; + writeln!(&mut self.agenda)?; + + let popular_issues = GithubQuery::new("Popular") + .labels(&["T-libs", "C-tracking-issue"]) + .exclude_labels(&["I-libs-nominated"]) + .exclude_labels(&["I-libs-radar"]) + .exclude_labels(&["S-waiting-on-t-libs"]) + .repos(REPOS_CODE) + .sort(Sort::MostCommented) + .take(10) + .shuffle(true) + .write(&mut *self)?; + + GithubQuery::new("Stalled") + .labels(&["T-libs", "C-tracking-issue"]) + .exclude_labels(&["I-libs-nominated"]) + .exclude_labels(&["I-libs-radar"]) + .exclude_labels(&["S-waiting-on-t-libs"]) + .repos(REPOS_CODE) + .sort(Sort::LeastRecentlyUpdated) + .take(10) + .skip(popular_issues.iter()) + .shuffle(true) + .write(&mut *self)?; + + Ok(()) + } + fn write_issues(&mut self, issues: &[Issue]) -> Result<()> { for issue in issues.iter().rev() { write!( @@ -492,7 +473,7 @@ impl Generator { shorten(&issue.html_url, Shorten::Text) )?; for label in issue.labels.iter().filter(|s| s.starts_with("P-")) { - write!(self.agenda, " `{}`", label)?; + write!(self.agenda, " `{label}`")?; } writeln!(self.agenda, " {}", escape(&issue.title).trim())?; if issue @@ -501,9 +482,9 @@ impl Generator { .any(|l| l == "finished-final-comment-period") { write!(self.agenda, "FCP finished.")?; - for label in issue.labels.iter() { + for label in &issue.labels { if let Some(disposition) = label.strip_prefix("disposition-") { - write!(self.agenda, " Should be {}d?", disposition)?; + write!(self.agenda, " Should be {disposition}d?")?; } } writeln!(self.agenda)?; @@ -533,18 +514,18 @@ enum Sort { } impl Sort { - fn api_str(&self) -> &'static str { + fn api_str(self) -> &'static str { match self { Sort::Newest => "&sort=created&direction=desc", Sort::Oldest => "&sort=created&direction=asc", - Sort::MostCommented => "&sort=comments&direction=asc", - Sort::LeastCommented => "&sort=comments&direction=desc", + Sort::LeastCommented => "&sort=comments&direction=asc", + Sort::MostCommented => "&sort=comments&direction=desc", Sort::MostRecentlyUpdated => "&sort=updated&direction=desc", Sort::LeastRecentlyUpdated => "&sort=updated&direction=asc", } } - fn web_ui_str(&self) -> &'static str { + fn web_ui_str(self) -> &'static str { match self { Sort::Newest => "+sort:created-desc", Sort::Oldest => "+sort:created-asc", @@ -620,8 +601,8 @@ impl GithubQuery { self } - fn repo(&mut self, repo: &'static str) -> &mut Self { - self.repos.push(repo); + fn repos(&mut self, repos: &'static [&'static str]) -> &mut Self { + self.repos.extend_from_slice(repos); self } @@ -663,7 +644,7 @@ impl GithubQuery { for repo in &self.repos { for labels in &self.labels { let cs_labels = labels.join(","); - let mut endpoint = format!("repos/{}/issues?labels={}", repo, cs_labels); + let mut endpoint = format!("repos/{repo}/issues?labels={cs_labels}"); endpoint += self.state.api_str(); @@ -691,10 +672,10 @@ impl GithubQuery { let url_labels = labels .iter() - .map(|label| format!("label:{}", label)) + .map(|label| format!("label:{label}")) .join("+"); - let mut url = format!("https://github.com/{}/issues?q={}", repo, url_labels); + let mut url = format!("https://github.com/{repo}/issues?q={url_labels}"); url += self.state.web_ui_str(); @@ -767,13 +748,41 @@ fn escape(v: &str) -> String { s } +fn get_github_token() -> Result { + let token_err = match env::var("GITHUB_TOKEN") { + Ok(token) => return Ok(token), + Err(err) => err, + }; + let token = Command::new("gh") + .args(["auth", "token"]) + .output() + .wrap_err("gh auth token failed") + .wrap_err(token_err)? + .stdout; + Ok(str::from_utf8(&token) + .wrap_err("gh auth token returned invalid UTF-8")? + .trim() + .to_owned()) +} + +static GITHUB_TOKEN: LazyLock> = LazyLock::new(|| match get_github_token() { + Ok(mut token) => { + token.insert_str(0, "token "); + Some(token) + } + Err(err) => { + println!("{err}"); + None + } +}); + fn github_api(endpoint: &str) -> Result { - let url = format!("https://api.github.com/{}", endpoint); + let url = format!("https://api.github.com/{endpoint}"); let mut client = reqwest::blocking::Client::new() .get(&url) .header(USER_AGENT, "rust-lang libs agenda maker"); - if let Ok(token) = std::env::var("GITHUB_TOKEN") { - client = client.header(AUTHORIZATION, format!("token {}", token)); + if let Some(token) = &*GITHUB_TOKEN { + client = client.header(AUTHORIZATION, token); } let response = client.send()?; let response = response.text()?; diff --git a/tools/agenda-generator/src/main.rs b/tools/agenda-generator/src/main.rs index 06f57a0..a2f3fa5 100644 --- a/tools/agenda-generator/src/main.rs +++ b/tools/agenda-generator/src/main.rs @@ -3,37 +3,10 @@ use generator::Generator; mod generator; -use clap::{Parser, ValueEnum}; - -#[derive(Debug, Parser)] -pub struct Args { - #[clap(long, short, default_value = "Libs")] - pub agenda: AgendaKind, -} - -impl Args { - pub fn from_args() -> Args { - ::parse() - } -} - -#[derive(Clone, Debug, ValueEnum)] -#[clap(rename_all = "verbatim")] -pub enum AgendaKind { - Libs, - PgEh, -} - fn main() -> eyre::Result<()> { color_eyre::install()?; - let args = Args::from_args(); - let generator = Generator::default(); - let agenda = match args.agenda { - AgendaKind::Libs => generator.libs_agenda()?, - AgendaKind::PgEh => generator.error_handling_pg_agenda()?, - }; // note: newline included - print!("{}", agenda); + print!("{}", Generator::default().libs_agenda()?); Ok(()) }