config.rs 8.2 KB

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