Skip to content

Commit 31f0e2f

Browse files
committed
Add alias for compound labels
Configure relabel command aliases from the `triagebot.toml`. When a valid alias is parsed, it will be replaced with the labels configured. Example configuration: ``` [relabel.cmd-alias] add-labels = ["Foo", "Bar"] rem-labels = ["Baz"] ``` The command `@rustbot label cmd-alias` will translate to: ``` @rustbot label +Foo +Bar -Baz ``` The command `@rustbot label -cmd-alias` will translate to: ``` @rustbot label +Baz -Foo -Bar ``` Note: self-canceling labels will be omitted. The command `@rustbot label cmd-alias +Bar` will translate to: ``` @rustbot label -Foo -Bar ```
1 parent 692de91 commit 31f0e2f

File tree

3 files changed

+104
-10
lines changed

3 files changed

+104
-10
lines changed

parser/src/command/relabel.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ fn delta_empty() {
102102
}
103103

104104
impl RelabelCommand {
105+
/// Parse and validate command tokens
105106
pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> {
106107
let mut toks = input.clone();
107108

src/config.rs

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::changelogs::ChangelogFormat;
22
use crate::github::{GithubClient, Repository};
3+
use parser::command::relabel::{Label, LabelDelta, RelabelCommand};
34
use std::collections::{HashMap, HashSet};
45
use std::fmt;
56
use std::sync::{Arc, LazyLock, RwLock};
@@ -250,10 +251,62 @@ pub(crate) struct MentionsEntryConfig {
250251

251252
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
252253
#[serde(rename_all = "kebab-case")]
253-
#[serde(deny_unknown_fields)]
254254
pub(crate) struct RelabelConfig {
255255
#[serde(default)]
256256
pub(crate) allow_unauthenticated: Vec<String>,
257+
// alias identifier -> labels
258+
#[serde(flatten)]
259+
pub(crate) configs: Option<HashMap<String, RelabelRuleConfig>>,
260+
}
261+
262+
impl RelabelConfig {
263+
pub(crate) fn retrieve_command_from_alias(&self, input: RelabelCommand) -> RelabelCommand {
264+
match &self.configs {
265+
Some(configs) => {
266+
dbg!(&configs);
267+
// get only the first token from the command
268+
// extract the "alias" from the RelabelCommand
269+
if input.0.len() > 0 {
270+
let name = input.0.get(0).unwrap();
271+
let name = name.label().as_str();
272+
// check if this alias matches any RelabelRuleConfig key in our config
273+
// extract the labels and build a new command
274+
if configs.contains_key(name) {
275+
let (_alias, cfg) = configs.get_key_value(name).unwrap();
276+
return cfg.to_command();
277+
}
278+
}
279+
}
280+
None => {
281+
return input;
282+
}
283+
};
284+
input
285+
}
286+
}
287+
288+
#[derive(Default, PartialEq, Eq, Debug, serde::Deserialize)]
289+
#[serde(rename_all = "kebab-case")]
290+
#[serde(deny_unknown_fields)]
291+
pub(crate) struct RelabelRuleConfig {
292+
/// Labels to be added
293+
pub(crate) add_labels: Vec<String>,
294+
/// Labels to be removed
295+
pub(crate) rem_labels: Vec<String>,
296+
}
297+
298+
impl RelabelRuleConfig {
299+
/// Translate a RelabelRuleConfig into a RelabelCommand for GitHub consumption
300+
pub fn to_command(&self) -> RelabelCommand {
301+
let mut deltas = Vec::new();
302+
for l in self.add_labels.iter() {
303+
deltas.push(LabelDelta::Add(Label(l.into())));
304+
}
305+
for l in self.rem_labels.iter() {
306+
deltas.push(LabelDelta::Remove(Label(l.into())));
307+
}
308+
RelabelCommand(deltas)
309+
}
257310
}
258311

259312
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -761,11 +814,11 @@ mod tests {
761814
762815
[mentions."src/"]
763816
cc = ["@someone"]
764-
817+
765818
[mentions."target/"]
766819
message = "This is a message."
767820
cc = ["@someone"]
768-
821+
769822
[mentions."#[rustc_attr]"]
770823
type = "content"
771824
message = "This is a message."
@@ -835,6 +888,7 @@ mod tests {
835888
Config {
836889
relabel: Some(RelabelConfig {
837890
allow_unauthenticated: vec!["C-*".into()],
891+
configs: Some(HashMap::new())
838892
}),
839893
assign: Some(AssignConfig {
840894
warn_non_default_branch: WarnNonDefaultBranchConfig::Simple(false),
@@ -1092,5 +1146,36 @@ Multi text body with ${mcp_issue} and ${mcp_title}
10921146
})
10931147
})
10941148
);
1149+
1150+
fn relabel_new_config() {
1151+
let config = r#"
1152+
[relabel]
1153+
allow-unauthenticated = ["ABCD-*"]
1154+
1155+
[relabel.to-stable]
1156+
add-labels = ["regression-from-stable-to-stable"]
1157+
rem-labels = ["regression-from-stable-to-beta", "regression-from-stable-to-nightly"]
1158+
"#;
1159+
let config = toml::from_str::<Config>(&config).unwrap();
1160+
1161+
let mut relabel_configs = HashMap::new();
1162+
relabel_configs.insert(
1163+
"to-stable".into(),
1164+
RelabelRuleConfig {
1165+
add_labels: vec!["regression-from-stable-to-stable".to_string()],
1166+
rem_labels: vec![
1167+
"regression-from-stable-to-beta".to_string(),
1168+
"regression-from-stable-to-nightly".to_string(),
1169+
],
1170+
},
1171+
);
1172+
1173+
let expected_cfg = RelabelConfig {
1174+
allow_unauthenticated: vec!["ABCD-*".to_string()],
1175+
configs: Some(relabel_configs),
1176+
};
1177+
1178+
assert_eq!(config.relabel, Some(expected_cfg));
1179+
}
10951180
}
10961181
}

src/handlers/relabel.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,18 @@ pub(super) async fn handle_command(
2626
event: &Event,
2727
input: RelabelCommand,
2828
) -> anyhow::Result<()> {
29-
let Some(issue) = event.issue() else {
30-
return user_error!("Can only add and remove labels on an issue");
31-
};
29+
let mut to_rem = vec![];
30+
let mut to_add = vec![];
31+
32+
// If the input matches a valid alias, read the [relabel] config.
33+
// if any alias matches, extract the alias config (RelabelRuleConfig) and build a new RelabelCommand.
34+
// Discard anything after the alias.
35+
let new_input = config.retrieve_command_from_alias(input);
3236

33-
// Check label authorization for the current user
34-
for delta in &input.0 {
35-
let name = delta.label() as &str;
36-
let err = match check_filter(name, config, is_member(event.user(), &ctx.team).await) {
37+
// Parse input label command, checks permissions, built GitHub commands
38+
for delta in &new_input.0 {
39+
let name = delta.label().as_str();
40+
let err = match check_filter(name, config, is_member(&event.user(), &ctx.team).await) {
3741
Ok(CheckFilterResult::Allow) => None,
3842
Ok(CheckFilterResult::Deny) => {
3943
Some(format!("Label {name} can only be set by Rust team members"))
@@ -44,6 +48,7 @@ pub(super) async fn handle_command(
4448
)),
4549
Err(err) => Some(err),
4650
};
51+
4752
if let Some(err) = err {
4853
// bail-out and inform the user why
4954
return user_error!(err);
@@ -103,6 +108,8 @@ enum CheckFilterResult {
103108
DenyUnknown,
104109
}
105110

111+
/// Check if the team member is allowed to apply labels
112+
/// configured in `allow_unauthenticated`
106113
fn check_filter(
107114
label: &str,
108115
config: &RelabelConfig,
@@ -232,6 +239,7 @@ mod tests {
232239
($($member:ident { $($label:expr => $res:ident,)* })*) => {
233240
let config = RelabelConfig {
234241
allow_unauthenticated: vec!["T-*".into(), "I-*".into(), "!I-*nominated".into()],
242+
configs: None
235243
};
236244
$($(assert_eq!(
237245
check_filter($label, &config, TeamMembership::$member),

0 commit comments

Comments
 (0)