config.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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. pub(crate) note: Option<NoteConfig>,
  32. pub(crate) mentions: Option<MentionsConfig>,
  33. }
  34. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  35. pub(crate) struct NominateConfig {
  36. // team name -> label
  37. pub(crate) teams: HashMap<String, String>,
  38. }
  39. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  40. pub(crate) struct PingConfig {
  41. // team name -> message
  42. // message will have the cc string appended
  43. #[serde(flatten)]
  44. teams: HashMap<String, PingTeamConfig>,
  45. }
  46. impl PingConfig {
  47. pub(crate) fn get_by_name(&self, team: &str) -> Option<(&str, &PingTeamConfig)> {
  48. if let Some((team, cfg)) = self.teams.get_key_value(team) {
  49. return Some((team, cfg));
  50. }
  51. for (name, cfg) in self.teams.iter() {
  52. if cfg.alias.contains(team) {
  53. return Some((name, cfg));
  54. }
  55. }
  56. None
  57. }
  58. }
  59. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  60. pub(crate) struct PingTeamConfig {
  61. pub(crate) message: String,
  62. #[serde(default)]
  63. pub(crate) alias: HashSet<String>,
  64. pub(crate) label: Option<String>,
  65. }
  66. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  67. pub(crate) struct AssignConfig {
  68. #[serde(default)]
  69. _empty: (),
  70. }
  71. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  72. pub(crate) struct NoteConfig {
  73. #[serde(default)]
  74. _empty: (),
  75. }
  76. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  77. pub(crate) struct MentionsConfig {
  78. #[serde(flatten)]
  79. pub(crate) paths: HashMap<String, MentionsPathConfig>,
  80. }
  81. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  82. pub(crate) struct MentionsPathConfig {
  83. pub(crate) message: Option<String>,
  84. #[serde(default)]
  85. pub(crate) cc: Vec<String>,
  86. }
  87. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  88. #[serde(rename_all = "kebab-case")]
  89. pub(crate) struct RelabelConfig {
  90. #[serde(default)]
  91. pub(crate) allow_unauthenticated: Vec<String>,
  92. }
  93. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  94. pub(crate) struct ShortcutConfig {
  95. #[serde(default)]
  96. _empty: (),
  97. }
  98. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  99. pub(crate) struct PrioritizeConfig {
  100. pub(crate) label: String,
  101. }
  102. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  103. pub(crate) struct AutolabelConfig {
  104. #[serde(flatten)]
  105. pub(crate) labels: HashMap<String, AutolabelLabelConfig>,
  106. }
  107. impl AutolabelConfig {
  108. pub(crate) fn get_by_trigger(&self, trigger: &str) -> Vec<(&str, &AutolabelLabelConfig)> {
  109. let mut results = Vec::new();
  110. for (label, cfg) in self.labels.iter() {
  111. if cfg.trigger_labels.iter().any(|l| l == trigger) {
  112. results.push((label.as_str(), cfg));
  113. }
  114. }
  115. results
  116. }
  117. }
  118. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  119. pub(crate) struct AutolabelLabelConfig {
  120. #[serde(default)]
  121. pub(crate) trigger_labels: Vec<String>,
  122. #[serde(default)]
  123. pub(crate) exclude_labels: Vec<String>,
  124. #[serde(default)]
  125. pub(crate) trigger_files: Vec<String>,
  126. }
  127. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  128. pub(crate) struct NotifyZulipConfig {
  129. #[serde(flatten)]
  130. pub(crate) labels: HashMap<String, NotifyZulipLabelConfig>,
  131. }
  132. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  133. pub(crate) struct NotifyZulipLabelConfig {
  134. pub(crate) zulip_stream: u64,
  135. pub(crate) topic: String,
  136. pub(crate) message_on_add: Option<String>,
  137. pub(crate) message_on_remove: Option<String>,
  138. pub(crate) message_on_close: Option<String>,
  139. pub(crate) message_on_reopen: Option<String>,
  140. #[serde(default)]
  141. pub(crate) required_labels: Vec<String>,
  142. }
  143. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  144. pub(crate) struct MajorChangeConfig {
  145. /// A username (typically a group, e.g. T-lang) to ping on Zulip for newly
  146. /// opened proposals.
  147. pub(crate) zulip_ping: String,
  148. /// This label allows an issue to participate in the major change process
  149. /// (i.e., creates a Zulip thread, tracks seconding, etc.)
  150. // This has a default primarily for backwards compatibility.
  151. #[serde(default = "MajorChangeConfig::enabling_label_default")]
  152. pub(crate) enabling_label: String,
  153. /// This is the label applied when issuing a `@rustbot second` command, it
  154. /// indicates that the proposal has moved into the 10 day waiting period.
  155. pub(crate) second_label: String,
  156. /// This is the label applied after the waiting period has successfully
  157. /// elapsed (currently not automatically applied; this must be done
  158. /// manually).
  159. // This has a default primarily for backwards compatibility.
  160. #[serde(default = "MajorChangeConfig::accept_label_default")]
  161. pub(crate) accept_label: String,
  162. /// This is the label to be added to newly opened proposals, so they can be
  163. /// discussed in a meeting.
  164. pub(crate) meeting_label: String,
  165. pub(crate) zulip_stream: u64,
  166. pub(crate) open_extra_text: Option<String>,
  167. }
  168. impl MajorChangeConfig {
  169. fn enabling_label_default() -> String {
  170. String::from("major-change")
  171. }
  172. fn accept_label_default() -> String {
  173. String::from("major-change-accepted")
  174. }
  175. }
  176. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  177. pub(crate) struct GlacierConfig {}
  178. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  179. pub(crate) struct CloseConfig {}
  180. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  181. pub(crate) struct ReviewSubmittedConfig {
  182. pub(crate) review_labels: Vec<String>,
  183. pub(crate) reviewed_label: String,
  184. }
  185. pub(crate) async fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, ConfigurationError> {
  186. if let Some(config) = get_cached_config(repo) {
  187. log::trace!("returning config for {} from cache", repo);
  188. config
  189. } else {
  190. log::trace!("fetching fresh config for {}", repo);
  191. let res = get_fresh_config(gh, repo).await;
  192. CONFIG_CACHE
  193. .write()
  194. .unwrap()
  195. .insert(repo.to_string(), (res.clone(), Instant::now()));
  196. res
  197. }
  198. }
  199. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  200. #[serde(rename_all = "kebab-case")]
  201. pub(crate) struct GitHubReleasesConfig {
  202. pub(crate) format: ChangelogFormat,
  203. pub(crate) project_name: String,
  204. pub(crate) changelog_path: String,
  205. pub(crate) changelog_branch: String,
  206. }
  207. fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
  208. let cache = CONFIG_CACHE.read().unwrap();
  209. cache.get(repo).and_then(|(config, fetch_time)| {
  210. if fetch_time.elapsed() < REFRESH_EVERY {
  211. Some(config.clone())
  212. } else {
  213. None
  214. }
  215. })
  216. }
  217. async fn get_fresh_config(
  218. gh: &GithubClient,
  219. repo: &str,
  220. ) -> Result<Arc<Config>, ConfigurationError> {
  221. let contents = gh
  222. .raw_file(repo, "master", CONFIG_FILE_NAME)
  223. .await
  224. .map_err(|e| ConfigurationError::Http(Arc::new(e)))?
  225. .ok_or(ConfigurationError::Missing)?;
  226. let config = Arc::new(toml::from_slice::<Config>(&contents).map_err(ConfigurationError::Toml)?);
  227. log::debug!("fresh configuration for {}: {:?}", repo, config);
  228. Ok(config)
  229. }
  230. #[derive(Clone, Debug)]
  231. pub enum ConfigurationError {
  232. Missing,
  233. Toml(toml::de::Error),
  234. Http(Arc<anyhow::Error>),
  235. }
  236. impl std::error::Error for ConfigurationError {}
  237. impl fmt::Display for ConfigurationError {
  238. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  239. match self {
  240. ConfigurationError::Missing => write!(
  241. f,
  242. "This repository is not enabled to use triagebot.\n\
  243. Add a `triagebot.toml` in the root of the master branch to enable it."
  244. ),
  245. ConfigurationError::Toml(e) => {
  246. write!(f, "Malformed `triagebot.toml` in master branch.\n{}", e)
  247. }
  248. ConfigurationError::Http(_) => {
  249. write!(f, "Failed to query configuration for this repository.")
  250. }
  251. }
  252. }
  253. }
  254. #[cfg(test)]
  255. mod tests {
  256. use super::*;
  257. #[test]
  258. fn sample() {
  259. let config = r#"
  260. [relabel]
  261. allow-unauthenticated = [
  262. "C-*"
  263. ]
  264. [assign]
  265. [note]
  266. [ping.compiler]
  267. message = """\
  268. So many people!\
  269. """
  270. label = "T-compiler"
  271. [ping.wg-meta]
  272. message = """\
  273. Testing\
  274. """
  275. [nominate.teams]
  276. compiler = "T-compiler"
  277. release = "T-release"
  278. core = "T-core"
  279. infra = "T-infra"
  280. [shortcut]
  281. "#;
  282. let config = toml::from_str::<Config>(&config).unwrap();
  283. let mut ping_teams = HashMap::new();
  284. ping_teams.insert(
  285. "compiler".to_owned(),
  286. PingTeamConfig {
  287. message: "So many people!".to_owned(),
  288. label: Some("T-compiler".to_owned()),
  289. alias: HashSet::new(),
  290. },
  291. );
  292. ping_teams.insert(
  293. "wg-meta".to_owned(),
  294. PingTeamConfig {
  295. message: "Testing".to_owned(),
  296. label: None,
  297. alias: HashSet::new(),
  298. },
  299. );
  300. let mut nominate_teams = HashMap::new();
  301. nominate_teams.insert("compiler".to_owned(), "T-compiler".to_owned());
  302. nominate_teams.insert("release".to_owned(), "T-release".to_owned());
  303. nominate_teams.insert("core".to_owned(), "T-core".to_owned());
  304. nominate_teams.insert("infra".to_owned(), "T-infra".to_owned());
  305. assert_eq!(
  306. config,
  307. Config {
  308. relabel: Some(RelabelConfig {
  309. allow_unauthenticated: vec!["C-*".into()],
  310. }),
  311. assign: Some(AssignConfig { _empty: () }),
  312. note: Some(NoteConfig { _empty: () }),
  313. ping: Some(PingConfig { teams: ping_teams }),
  314. nominate: Some(NominateConfig {
  315. teams: nominate_teams
  316. }),
  317. shortcut: Some(ShortcutConfig { _empty: () }),
  318. prioritize: None,
  319. major_change: None,
  320. glacier: None,
  321. close: None,
  322. autolabel: None,
  323. notify_zulip: None,
  324. github_releases: None,
  325. review_submitted: None,
  326. mentions: None,
  327. }
  328. );
  329. }
  330. }