config.rs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. use crate::changelogs::ChangelogFormat;
  2. use crate::github::GithubClient;
  3. use std::collections::{HashMap, HashSet};
  4. use std::fmt;
  5. use std::sync::{Arc, RwLock};
  6. use std::time::{Duration, Instant};
  7. use tracing as log;
  8. static CONFIG_FILE_NAME: &str = "triagebot.toml";
  9. const REFRESH_EVERY: Duration = Duration::from_secs(2 * 60); // Every two minutes
  10. lazy_static::lazy_static! {
  11. static ref CONFIG_CACHE:
  12. RwLock<HashMap<String, (Result<Arc<Config>, ConfigurationError>, Instant)>> =
  13. RwLock::new(HashMap::new());
  14. }
  15. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  16. #[serde(rename_all = "kebab-case")]
  17. pub(crate) struct Config {
  18. pub(crate) relabel: Option<RelabelConfig>,
  19. pub(crate) assign: Option<AssignConfig>,
  20. pub(crate) ping: Option<PingConfig>,
  21. pub(crate) nominate: Option<NominateConfig>,
  22. pub(crate) prioritize: Option<PrioritizeConfig>,
  23. pub(crate) major_change: Option<MajorChangeConfig>,
  24. pub(crate) glacier: Option<GlacierConfig>,
  25. pub(crate) close: Option<CloseConfig>,
  26. pub(crate) autolabel: Option<AutolabelConfig>,
  27. pub(crate) notify_zulip: Option<NotifyZulipConfig>,
  28. pub(crate) github_releases: Option<GitHubReleasesConfig>,
  29. pub(crate) review_submitted: Option<ReviewSubmittedConfig>,
  30. pub(crate) shortcut: Option<ShortcutConfig>,
  31. }
  32. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  33. pub(crate) struct NominateConfig {
  34. // team name -> label
  35. pub(crate) teams: HashMap<String, String>,
  36. }
  37. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  38. pub(crate) struct PingConfig {
  39. // team name -> message
  40. // message will have the cc string appended
  41. #[serde(flatten)]
  42. teams: HashMap<String, PingTeamConfig>,
  43. }
  44. impl PingConfig {
  45. pub(crate) fn get_by_name(&self, team: &str) -> Option<(&str, &PingTeamConfig)> {
  46. if let Some((team, cfg)) = self.teams.get_key_value(team) {
  47. return Some((team, cfg));
  48. }
  49. for (name, cfg) in self.teams.iter() {
  50. if cfg.alias.contains(team) {
  51. return Some((name, cfg));
  52. }
  53. }
  54. None
  55. }
  56. }
  57. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  58. pub(crate) struct PingTeamConfig {
  59. pub(crate) message: String,
  60. #[serde(default)]
  61. pub(crate) alias: HashSet<String>,
  62. pub(crate) label: Option<String>,
  63. }
  64. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  65. pub(crate) struct AssignConfig {
  66. #[serde(default)]
  67. _empty: (),
  68. }
  69. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  70. #[serde(rename_all = "kebab-case")]
  71. pub(crate) struct RelabelConfig {
  72. #[serde(default)]
  73. pub(crate) allow_unauthenticated: Vec<String>,
  74. }
  75. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  76. pub(crate) struct ShortcutConfig {
  77. #[serde(default)]
  78. _empty: (),
  79. }
  80. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  81. pub(crate) struct PrioritizeConfig {
  82. pub(crate) label: String,
  83. }
  84. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  85. pub(crate) struct AutolabelConfig {
  86. #[serde(flatten)]
  87. pub(crate) labels: HashMap<String, AutolabelLabelConfig>,
  88. }
  89. impl AutolabelConfig {
  90. pub(crate) fn get_by_trigger(&self, trigger: &str) -> Vec<(&str, &AutolabelLabelConfig)> {
  91. let mut results = Vec::new();
  92. for (label, cfg) in self.labels.iter() {
  93. if cfg.trigger_labels.iter().any(|l| l == trigger) {
  94. results.push((label.as_str(), cfg));
  95. }
  96. }
  97. results
  98. }
  99. }
  100. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  101. pub(crate) struct AutolabelLabelConfig {
  102. #[serde(default)]
  103. pub(crate) trigger_labels: Vec<String>,
  104. #[serde(default)]
  105. pub(crate) exclude_labels: Vec<String>,
  106. #[serde(default)]
  107. pub(crate) trigger_files: Vec<String>,
  108. }
  109. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  110. pub(crate) struct NotifyZulipConfig {
  111. #[serde(flatten)]
  112. pub(crate) labels: HashMap<String, NotifyZulipLabelConfig>,
  113. }
  114. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  115. pub(crate) struct NotifyZulipLabelConfig {
  116. pub(crate) zulip_stream: u64,
  117. pub(crate) topic: String,
  118. pub(crate) message_on_add: Option<String>,
  119. pub(crate) message_on_remove: Option<String>,
  120. pub(crate) message_on_close: Option<String>,
  121. pub(crate) message_on_reopen: Option<String>,
  122. #[serde(default)]
  123. pub(crate) required_labels: Vec<String>,
  124. }
  125. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  126. pub(crate) struct MajorChangeConfig {
  127. pub(crate) zulip_ping: String,
  128. pub(crate) second_label: String,
  129. pub(crate) meeting_label: String,
  130. pub(crate) zulip_stream: u64,
  131. pub(crate) open_extra_text: Option<String>,
  132. }
  133. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  134. pub(crate) struct GlacierConfig {}
  135. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  136. pub(crate) struct CloseConfig {}
  137. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  138. pub(crate) struct ReviewSubmittedConfig {
  139. pub(crate) review_labels: Vec<String>,
  140. pub(crate) reviewed_label: String,
  141. }
  142. pub(crate) async fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, ConfigurationError> {
  143. if let Some(config) = get_cached_config(repo) {
  144. log::trace!("returning config for {} from cache", repo);
  145. config
  146. } else {
  147. log::trace!("fetching fresh config for {}", repo);
  148. let res = get_fresh_config(gh, repo).await;
  149. CONFIG_CACHE
  150. .write()
  151. .unwrap()
  152. .insert(repo.to_string(), (res.clone(), Instant::now()));
  153. res
  154. }
  155. }
  156. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  157. #[serde(rename_all = "kebab-case")]
  158. pub(crate) struct GitHubReleasesConfig {
  159. pub(crate) format: ChangelogFormat,
  160. pub(crate) project_name: String,
  161. pub(crate) changelog_path: String,
  162. pub(crate) changelog_branch: String,
  163. }
  164. fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
  165. let cache = CONFIG_CACHE.read().unwrap();
  166. cache.get(repo).and_then(|(config, fetch_time)| {
  167. if fetch_time.elapsed() < REFRESH_EVERY {
  168. Some(config.clone())
  169. } else {
  170. None
  171. }
  172. })
  173. }
  174. async fn get_fresh_config(
  175. gh: &GithubClient,
  176. repo: &str,
  177. ) -> Result<Arc<Config>, ConfigurationError> {
  178. let contents = gh
  179. .raw_file(repo, "master", CONFIG_FILE_NAME)
  180. .await
  181. .map_err(|e| ConfigurationError::Http(Arc::new(e)))?
  182. .ok_or(ConfigurationError::Missing)?;
  183. let config = Arc::new(toml::from_slice::<Config>(&contents).map_err(ConfigurationError::Toml)?);
  184. log::debug!("fresh configuration for {}: {:?}", repo, config);
  185. Ok(config)
  186. }
  187. #[derive(Clone, Debug)]
  188. pub enum ConfigurationError {
  189. Missing,
  190. Toml(toml::de::Error),
  191. Http(Arc<anyhow::Error>),
  192. }
  193. impl std::error::Error for ConfigurationError {}
  194. impl fmt::Display for ConfigurationError {
  195. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  196. match self {
  197. ConfigurationError::Missing => write!(
  198. f,
  199. "This repository is not enabled to use triagebot.\n\
  200. Add a `triagebot.toml` in the root of the master branch to enable it."
  201. ),
  202. ConfigurationError::Toml(e) => {
  203. write!(f, "Malformed `triagebot.toml` in master branch.\n{}", e)
  204. }
  205. ConfigurationError::Http(_) => {
  206. write!(f, "Failed to query configuration for this repository.")
  207. }
  208. }
  209. }
  210. }
  211. #[cfg(test)]
  212. mod tests {
  213. use super::*;
  214. #[test]
  215. fn sample() {
  216. let config = r#"
  217. [relabel]
  218. allow-unauthenticated = [
  219. "C-*"
  220. ]
  221. [assign]
  222. [ping.compiler]
  223. message = """\
  224. So many people!\
  225. """
  226. label = "T-compiler"
  227. [ping.wg-meta]
  228. message = """\
  229. Testing\
  230. """
  231. [nominate.teams]
  232. compiler = "T-compiler"
  233. release = "T-release"
  234. core = "T-core"
  235. infra = "T-infra"
  236. [shortcut]
  237. "#;
  238. let config = toml::from_str::<Config>(&config).unwrap();
  239. let mut ping_teams = HashMap::new();
  240. ping_teams.insert(
  241. "compiler".to_owned(),
  242. PingTeamConfig {
  243. message: "So many people!".to_owned(),
  244. label: Some("T-compiler".to_owned()),
  245. alias: HashSet::new(),
  246. },
  247. );
  248. ping_teams.insert(
  249. "wg-meta".to_owned(),
  250. PingTeamConfig {
  251. message: "Testing".to_owned(),
  252. label: None,
  253. alias: HashSet::new(),
  254. },
  255. );
  256. let mut nominate_teams = HashMap::new();
  257. nominate_teams.insert("compiler".to_owned(), "T-compiler".to_owned());
  258. nominate_teams.insert("release".to_owned(), "T-release".to_owned());
  259. nominate_teams.insert("core".to_owned(), "T-core".to_owned());
  260. nominate_teams.insert("infra".to_owned(), "T-infra".to_owned());
  261. assert_eq!(
  262. config,
  263. Config {
  264. relabel: Some(RelabelConfig {
  265. allow_unauthenticated: vec!["C-*".into()],
  266. }),
  267. assign: Some(AssignConfig { _empty: () }),
  268. ping: Some(PingConfig { teams: ping_teams }),
  269. nominate: Some(NominateConfig {
  270. teams: nominate_teams
  271. }),
  272. shortcut: Some(ShortcutConfig { _empty: () }),
  273. prioritize: None,
  274. major_change: None,
  275. glacier: None,
  276. close: None,
  277. autolabel: None,
  278. notify_zulip: None,
  279. github_releases: None,
  280. review_submitted: None,
  281. }
  282. );
  283. }
  284. }