From fd2c1cb57f9b05b18e25dc31e6221e5c2538f779 Mon Sep 17 00:00:00 2001 From: apiraino Date: Wed, 3 Sep 2025 18:17:02 +0200 Subject: [PATCH] Add alias for compound labels Configure relabel command aliases from the `triagebot.toml`. In case a valid alias is parsed, the rest will be discarded, example: @rustbot label cmd-alias label1 is the same as: @rustbot label cmd-alias --- parser/src/command/relabel.rs | 3 +- src/config.rs | 88 ++++++++++++++++++++++++++++++++++- src/handlers/relabel.rs | 30 ++++++++---- 3 files changed, 111 insertions(+), 10 deletions(-) diff --git a/parser/src/command/relabel.rs b/parser/src/command/relabel.rs index ef5a305c2..79b521b67 100644 --- a/parser/src/command/relabel.rs +++ b/parser/src/command/relabel.rs @@ -19,7 +19,7 @@ pub enum LabelDelta { } #[derive(Debug, PartialEq, Eq, Clone)] -pub struct Label(String); +pub struct Label(pub String); #[derive(PartialEq, Eq, Debug)] pub enum ParseError { @@ -103,6 +103,7 @@ fn delta_empty() { } impl RelabelCommand { + /// Parse and validate command tokens pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result, Error<'a>> { let mut toks = input.clone(); diff --git a/src/config.rs b/src/config.rs index 0aca7553e..96bf280db 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use crate::changelogs::ChangelogFormat; use crate::github::{GithubClient, Repository}; +use parser::command::relabel::{Label, LabelDelta, RelabelCommand}; use std::collections::{HashMap, HashSet}; use std::fmt; use std::sync::{Arc, LazyLock, RwLock}; @@ -238,10 +239,62 @@ pub(crate) struct MentionsPathConfig { #[derive(PartialEq, Eq, Debug, serde::Deserialize)] #[serde(rename_all = "kebab-case")] -#[serde(deny_unknown_fields)] pub(crate) struct RelabelConfig { #[serde(default)] pub(crate) allow_unauthenticated: Vec, + // alias identifier -> labels + #[serde(flatten)] + pub(crate) configs: Option>, +} + +impl RelabelConfig { + pub(crate) fn retrieve_command_from_alias(&self, input: RelabelCommand) -> RelabelCommand { + match &self.configs { + Some(configs) => { + dbg!(&configs); + // get only the first token from the command + // extract the "alias" from the RelabelCommand + if input.0.len() > 0 { + let name = input.0.get(0).unwrap(); + let name = name.label().as_str(); + // check if this alias matches any RelabelRuleConfig key in our config + // extract the labels and build a new command + if configs.contains_key(name) { + let (_alias, cfg) = configs.get_key_value(name).unwrap(); + return cfg.to_command(); + } + } + } + None => { + return input; + } + }; + input + } +} + +#[derive(Default, PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +#[serde(deny_unknown_fields)] +pub(crate) struct RelabelRuleConfig { + /// Labels to be added + pub(crate) add_labels: Vec, + /// Labels to be removed + pub(crate) rem_labels: Vec, +} + +impl RelabelRuleConfig { + /// Translate a RelabelRuleConfig into a RelabelCommand for GitHub consumption + pub fn to_command(&self) -> RelabelCommand { + let mut deltas = Vec::new(); + for l in self.add_labels.iter() { + deltas.push(LabelDelta::Add(Label(l.into()))); + } + for l in self.rem_labels.iter() { + deltas.push(LabelDelta::Remove(Label(l.into()))); + } + RelabelCommand(deltas) + } } #[derive(PartialEq, Eq, Debug, serde::Deserialize)] @@ -756,6 +809,7 @@ mod tests { Config { relabel: Some(RelabelConfig { allow_unauthenticated: vec!["C-*".into()], + configs: Some(HashMap::new()) }), assign: Some(AssignConfig { warn_non_default_branch: WarnNonDefaultBranchConfig::Simple(false), @@ -926,4 +980,36 @@ mod tests { }) ); } + + #[test] + fn relabel_new_config() { + let config = r#" + [relabel] + allow-unauthenticated = ["ABCD-*"] + + [relabel.to-stable] + add-labels = ["regression-from-stable-to-stable"] + rem-labels = ["regression-from-stable-to-beta", "regression-from-stable-to-nightly"] + "#; + let config = toml::from_str::(&config).unwrap(); + + let mut relabel_configs = HashMap::new(); + relabel_configs.insert( + "to-stable".into(), + RelabelRuleConfig { + add_labels: vec!["regression-from-stable-to-stable".to_string()], + rem_labels: vec![ + "regression-from-stable-to-beta".to_string(), + "regression-from-stable-to-nightly".to_string(), + ], + }, + ); + + let expected_cfg = RelabelConfig { + allow_unauthenticated: vec!["ABCD-*".to_string()], + configs: Some(relabel_configs), + }; + + assert_eq!(config.relabel, Some(expected_cfg)); + } } diff --git a/src/handlers/relabel.rs b/src/handlers/relabel.rs index 31c5908e8..21a66cf42 100644 --- a/src/handlers/relabel.rs +++ b/src/handlers/relabel.rs @@ -24,9 +24,16 @@ pub(super) async fn handle_command( event: &Event, input: RelabelCommand, ) -> anyhow::Result<()> { - let mut results = vec![]; + let mut to_rem = vec![]; let mut to_add = vec![]; - for delta in &input.0 { + + // If the input matches a valid alias, read the [relabel] config. + // if any alias matches, extract the alias config (RelabelRuleConfig) and build a new RelabelCommand. + // Discard anything after the alias. + let new_input = config.retrieve_command_from_alias(input); + + // Parse input label command, checks permissions, built GitHub commands + for delta in &new_input.0 { let name = delta.label().as_str(); let err = match check_filter(name, config, is_member(&event.user(), &ctx.team).await) { Ok(CheckFilterResult::Allow) => None, @@ -53,14 +60,12 @@ pub(super) async fn handle_command( }); } LabelDelta::Remove(label) => { - results.push(( - label, - event.issue().unwrap().remove_label(&ctx.github, &label), - )); + to_rem.push(label); } } } + // Add new labels (if needed) if let Err(e) = event .issue() .unwrap() @@ -84,8 +89,14 @@ pub(super) async fn handle_command( return Err(e); } - for (label, res) in results { - if let Err(e) = res.await { + // Remove labels (if needed) + for label in to_rem { + if let Err(e) = event + .issue() + .unwrap() + .remove_label(&ctx.github, &label) + .await + { tracing::error!( "failed to remove {:?} from issue {}: {:?}", label, @@ -124,6 +135,8 @@ enum CheckFilterResult { DenyUnknown, } +/// Check if the team member is allowed to apply labels +/// configured in `allow_unauthenticated` fn check_filter( label: &str, config: &RelabelConfig, @@ -220,6 +233,7 @@ mod tests { ($($member:ident { $($label:expr => $res:ident,)* })*) => { let config = RelabelConfig { allow_unauthenticated: vec!["T-*".into(), "I-*".into(), "!I-*nominated".into()], + configs: None }; $($(assert_eq!( check_filter($label, &config, TeamMembership::$member),