config.rs 13 KB

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