Skip to content

Commit fd2c1cb

Browse files
committed
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
1 parent 7ab36ca commit fd2c1cb

File tree

3 files changed

+111
-10
lines changed

3 files changed

+111
-10
lines changed

parser/src/command/relabel.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub enum LabelDelta {
1919
}
2020

2121
#[derive(Debug, PartialEq, Eq, Clone)]
22-
pub struct Label(String);
22+
pub struct Label(pub String);
2323

2424
#[derive(PartialEq, Eq, Debug)]
2525
pub enum ParseError {
@@ -103,6 +103,7 @@ fn delta_empty() {
103103
}
104104

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

src/config.rs

Lines changed: 87 additions & 1 deletion
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};
@@ -238,10 +239,62 @@ pub(crate) struct MentionsPathConfig {
238239

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

247300
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -756,6 +809,7 @@ mod tests {
756809
Config {
757810
relabel: Some(RelabelConfig {
758811
allow_unauthenticated: vec!["C-*".into()],
812+
configs: Some(HashMap::new())
759813
}),
760814
assign: Some(AssignConfig {
761815
warn_non_default_branch: WarnNonDefaultBranchConfig::Simple(false),
@@ -926,4 +980,36 @@ mod tests {
926980
})
927981
);
928982
}
983+
984+
#[test]
985+
fn relabel_new_config() {
986+
let config = r#"
987+
[relabel]
988+
allow-unauthenticated = ["ABCD-*"]
989+
990+
[relabel.to-stable]
991+
add-labels = ["regression-from-stable-to-stable"]
992+
rem-labels = ["regression-from-stable-to-beta", "regression-from-stable-to-nightly"]
993+
"#;
994+
let config = toml::from_str::<Config>(&config).unwrap();
995+
996+
let mut relabel_configs = HashMap::new();
997+
relabel_configs.insert(
998+
"to-stable".into(),
999+
RelabelRuleConfig {
1000+
add_labels: vec!["regression-from-stable-to-stable".to_string()],
1001+
rem_labels: vec![
1002+
"regression-from-stable-to-beta".to_string(),
1003+
"regression-from-stable-to-nightly".to_string(),
1004+
],
1005+
},
1006+
);
1007+
1008+
let expected_cfg = RelabelConfig {
1009+
allow_unauthenticated: vec!["ABCD-*".to_string()],
1010+
configs: Some(relabel_configs),
1011+
};
1012+
1013+
assert_eq!(config.relabel, Some(expected_cfg));
1014+
}
9291015
}

src/handlers/relabel.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@ pub(super) async fn handle_command(
2424
event: &Event,
2525
input: RelabelCommand,
2626
) -> anyhow::Result<()> {
27-
let mut results = vec![];
27+
let mut to_rem = vec![];
2828
let mut to_add = vec![];
29-
for delta in &input.0 {
29+
30+
// If the input matches a valid alias, read the [relabel] config.
31+
// if any alias matches, extract the alias config (RelabelRuleConfig) and build a new RelabelCommand.
32+
// Discard anything after the alias.
33+
let new_input = config.retrieve_command_from_alias(input);
34+
35+
// Parse input label command, checks permissions, built GitHub commands
36+
for delta in &new_input.0 {
3037
let name = delta.label().as_str();
3138
let err = match check_filter(name, config, is_member(&event.user(), &ctx.team).await) {
3239
Ok(CheckFilterResult::Allow) => None,
@@ -53,14 +60,12 @@ pub(super) async fn handle_command(
5360
});
5461
}
5562
LabelDelta::Remove(label) => {
56-
results.push((
57-
label,
58-
event.issue().unwrap().remove_label(&ctx.github, &label),
59-
));
63+
to_rem.push(label);
6064
}
6165
}
6266
}
6367

68+
// Add new labels (if needed)
6469
if let Err(e) = event
6570
.issue()
6671
.unwrap()
@@ -84,8 +89,14 @@ pub(super) async fn handle_command(
8489
return Err(e);
8590
}
8691

87-
for (label, res) in results {
88-
if let Err(e) = res.await {
92+
// Remove labels (if needed)
93+
for label in to_rem {
94+
if let Err(e) = event
95+
.issue()
96+
.unwrap()
97+
.remove_label(&ctx.github, &label)
98+
.await
99+
{
89100
tracing::error!(
90101
"failed to remove {:?} from issue {}: {:?}",
91102
label,
@@ -124,6 +135,8 @@ enum CheckFilterResult {
124135
DenyUnknown,
125136
}
126137

138+
/// Check if the team member is allowed to apply labels
139+
/// configured in `allow_unauthenticated`
127140
fn check_filter(
128141
label: &str,
129142
config: &RelabelConfig,
@@ -220,6 +233,7 @@ mod tests {
220233
($($member:ident { $($label:expr => $res:ident,)* })*) => {
221234
let config = RelabelConfig {
222235
allow_unauthenticated: vec!["T-*".into(), "I-*".into(), "!I-*nominated".into()],
236+
configs: None
223237
};
224238
$($(assert_eq!(
225239
check_filter($label, &config, TeamMembership::$member),

0 commit comments

Comments
 (0)